みなさんは、APIを開発する際にデータベースとのやり取りをどのように実装していますか?
多くの開発者は、以下の3つの方法のいずれかを採用しているでしょう。
-
SQLを直接記述
クエリを手動で書いてデータベースにアクセスする。細かい制御ができるが、記述が煩雑になる。 -
関数にSQL操作をまとめる
よく使う操作を関数化し、再利用しやすくすることで、コードの整理と効率化を図る。 -
ORM(オブジェクト・リレーショナル・マッピング)を使用
データベース操作を抽象化し、オブジェクト指向の考え方でデータを扱う。
ここでは、なぜORMという技術が誕生し、多くの開発者がこれを活用するのかを、歴史的背景から掘り下げてみよう。
シリーズ TypeScriptで学ぶプログラミング言語の世界
Part1 手続型からオブジェクト指向へ
Part3 プログラミングパラダイムの進化と革命:機械語からマルチパラダイムへ...新しいプログラミング言語に出会ってみよう!
他のシリーズ記事
TypeScriptを知らない人は以下の記事から.
上の記事も〇〇チートシートとしてシリーズ化しているのでぜひご覧ください.git/ghコマンドの概念,SQL,Go言語/Gormなどの概念理解や手引きなどを記載しています.
情報処理技術者試験合格への道 [IP・SG・FE・AP]
情報処理技術者試験の単語集です.
IAM AWS User クラウドサービスをフル活用しよう!
AWSのサービスを例にしてバックエンドとインフラ開発の手法を説明するシリーズです.
ORMヒストリー:1990年代から2000年代初頭
1990年代から2000年代初頭にかけて、企業や開発者たちは、データを効率的に管理するため、関係データベース管理システム(RDBMS)を広く利用していた。たとえば、MySQL、PostgreSQL、Oracleといったデータベースがその代表例だ。
一方、アプリケーションは、JavaやC++などのオブジェクト指向プログラミング言語で開発されていた。しかし、ここで大きな問題が発生した。
オブジェクト・リレーショナルインピーダンスミスマッチ
急に長い単語が出てきましたね.とりあえずオブジェクトの考えを復習していきましょう.
プログラム内では、データは「オブジェクト」として扱われる。例えば、ユーザー情報をUser
クラスとして以下のように管理する。
class User {
id: number;
name: string;
email: string;
}
しかし、データベースでは、情報は「テーブル」として保存される。たとえば、users
というテーブルは以下のような形だ。
id | name | |
---|---|---|
1 | Alice | alice@example.com |
2 | Bob | bob@example.com |
3 | Charlie | charlie@example.com |
ここで問題となるのは、プログラム内のオブジェクトとデータベースのテーブルが異なる形でデータを管理している点だ。この違いにより、データをやり取りする際に面倒な変換作業が必要になる。
この問題は「オブジェクト・リレーショナルインピーダンスミスマッチ(Object-Relational Impedance Mismatch)」と呼ばれる。具体的には、以下のような課題が生じる。
-
データの変換が面倒
プログラム内のUser
オブジェクトとデータベースのusers
テーブルの間でデータを変換する必要がある。この変換を毎回手作業で行うのは非常に煩雑だ。 -
クエリの記述が増える
データを取得するたびに、手動でSQLクエリを書く必要がある。例えば、ユーザー情報を取得するには以下のようなクエリが必要だ。select id, name, email from users where id = 1;
-
メンテナンスが大変
データベースのスキーマが変更されると、プログラム内のクエリもすべて修正しなければならない。これにより、メンテナンスの負担が増加する。
ORMとは?その仕組みとメリット
ORM(Object Relational Mapping:オブジェクト・リレーショナル・マッピング) は、プログラムのオブジェクトとデータベースのテーブルをマッピング(対応付け)する技術である。これにより、開発者はSQL文を書くことなく、プログラム内のオブジェクトを操作する感覚でデータベースとやり取りできる。また,複数のテーブルにまたがるデータ取得や更新操作は、複雑なSQLクエリが必要になる。ORMはこれをプログラムのメソッド呼び出しでシンプルにする。
-
従来のSQLクエリ
select * from users where id = 1;
-
ORMを使用した場合
const user = await userRepository.findOneBy({ id: 1 });
ORMの主なメリット
1. データベース操作の抽象化
SQLの記述が不要になるため、プログラムがデータベースに依存しなくなる。異なるデータベースに移行する場合でも、最小限の変更で済む。
2. CRUD操作の自動化
ORMはデフォルトでCRUD(Create, Read, Update, Delete)操作をサポートしている。開発者は新たにSQLクエリを記述する必要がなく、メソッド呼び出しだけでデータ操作が可能となる。
3. メンテナンス性の向上
データベースのスキーマ変更も、コード上のオブジェクト定義を変更するだけで対応できるため、メンテナンスが非常に容易になる。
4. SQLインジェクション対策
ORMでは、クエリパラメータのエスケープ処理が内部的に行われるため、SQLインジェクションのリスクが大幅に軽減される。たとえば、以下のような不正な入力による攻撃を防ぐことができる。
userRepository.findOneBy({ name: "'; DROP TABLE users;" });
5. デフォルトで安全なクエリ生成
プレースホルダを自動的に使用するため、手動でSQLを組み立てる際に発生しやすいエラーやセキュリティホールを回避できる。
ORMを使用することで、SQLの知識がなくてもデータベースを操作できるため、開発の効率が向上する。また、データベースの種類に依存しないため、MySQLからPostgreSQLなど別のRDBMSに移行する際も、コードの大部分を変更せずに済む。
ORMが使用される具体的なシナリオ
ORMが特に有効に働くケースをいくつか挙げる。
-
シナリオ1: Webアプリケーションの開発
Webアプリケーションでは、ユーザー管理、商品の管理、注文履歴の管理など、多くのデータベース操作が必要になる。ORMを使えば、これらのCRUD操作が簡単に行えるため、開発スピードが大幅に向上する。 -
シナリオ2: スタートアップやプロトタイプ開発
スタートアップやプロトタイプ開発では、仕様変更が頻繁に発生する。ORMを使っておけば、データベーススキーマの変更も容易に行えるため、柔軟に対応できる。 -
シナリオ3: 複数のデータベースに対応する必要があるプロジェクト
たとえば、開発段階ではMySQLを使い、本番環境ではスケーラビリティの観点からPostgreSQLに切り替える場合、ORMを使用していれば、接続設定を変更するだけで移行が可能になる。
ORMのデメリットと注意点
もちろん、ORMにもデメリットがあるため、プロジェクトの要件に応じて適切に選択する必要がある。
-
1. 大量データの処理時のパフォーマンスの低下
大規模なデータセットを扱う際、ORMの抽象化によりパフォーマンスが低下することがある。特に複雑なクエリの場合、ORMが生成するSQLは最適化されていないことが多い。そのため、パフォーマンスが求められる場面では、直接SQLを書いた方が効率的な場合もある。 -
2. 習得に時間がかかる
初めてORMを使う場合、その概念や設定方法に慣れるまで時間がかかる。また、内部でどのようなSQLクエリが生成されているのか理解しづらいことがあり、デバッグが難しい場合がある。 -
3. デバッグが難しい
ORMを使用すると、データベースへのアクセスがプログラムのメソッド呼び出しで抽象化されるため、エラーが発生した際にSQLの問題を特定しにくくなる。そのため、特に本番環境では、生成されるクエリを監視する仕組みが必要になる。
TypeScriptとTypeORMでどう記述するのか
まず、TypeScriptプロジェクトに必要なパッケージをインストールする。
npm init -y
npm install typescript ts-node-dev @types/node --save-dev
npm install typeorm reflect-metadata mysql2
npm install express @types/express
今回はDockerで環境を構築する部分は省略する
1. データベース接続方法を記述
次に、TypeORMの設定ファイルを作成する。以下がdata-source.ts
の例である。
import "reflect-metadata";
import { DataSource } from "typeorm";
import { User } from "./entity/User";
export const AppDataSource = new DataSource({
type: "mysql",
host: "localhost",
port: 3306,
username: "user",
password: "password",
database: "mydatabase",
synchronize: true,
logging: false,
entities: [User],
});
2. エンティティの定義
エンティティ(実質ドメインモデル)はデータベースのテーブルと対応するクラスである。以下はUser
エンティティの例だ。
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
email: string;
@Column({ default: false })
isActive: boolean;
}
3. コントローラーの実装
以下がuserController.ts
の内容である。ユーザーのCRUD操作を実装している。
import { Request, Response } from "express";
import { AppDataSource } from "../data-source";
import { User } from "../entity/User";
const userRepository = AppDataSource.getRepository(User);
export const getUsers = async (req: Request, res: Response) => {
const users = await userRepository.find();
res.json(users);
};
export const getUser = async (req: Request, res: Response) => {
const user = await userRepository.findOneBy({ id: parseInt(req.params.id) });
if (!user) {
return res.status(404).json({ message: "User not found" });
}
res.json(user);
};
export const createUser = async (req: Request, res: Response) => {
const { name, email } = req.body;
const newUser = userRepository.create({ name, email });
const savedUser = await userRepository.save(newUser);
res.status(201).json(savedUser);
};
export const updateUser = async (req: Request, res: Response) => {
const { id } = req.params;
const { name, email } = req.body;
const user = await userRepository.findOneBy({ id: parseInt(id) });
if (!user) return res.status(404).json({ message: "User not found" });
user.name = name;
user.email = email;
const updatedUser = await userRepository.save(user);
res.json(updatedUser);
};
export const deleteUser = async (req: Request, res: Response) => {
const { id } = req.params;
const result = await userRepository.delete(id);
if (result.affected === 0) return res.status(404).json({ message: "User not found" });
res.status(204).send();
};
4. ルーティングの設定
ユーザー関連のAPIエンドポイントを定義するためのルーターを作成する。userRoutes.ts
の内容は以下の通りだ。
import express from "express";
import { getUsers, getUser, createUser, updateUser, deleteUser } from "../controllers/userController";
const router = express.Router();
router.get("/", getUsers);
router.get("/:id", getUser);
router.post("/", createUser);
router.put("/:id", updateUser);
router.delete("/:id", deleteUser);
export default router;
5. APIのエントリーポイントの実装
Expressを使用してAPIサーバーを構築する。以下はサーバーのエントリーポイントである。
import express from "express";
import { AppDataSource } from "./data-source";
import userRouter from "./routes/userRoutes";
const app = express();
app.use(express.json());
AppDataSource.initialize()
.then(() => {
console.log("データベース接続成功");
app.use("/users", userRouter);
app.listen(3000, () => console.log("サーバー起動: http://localhost:3000"));
})
.catch((error) => console.error("データベース接続エラー:", error));
{
"name": "typescript-typeorm-api",
"version": "1.0.0",
"description": "API using TypeScript, Express, and TypeORM",
"main": "index.js",
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"typeorm": "typeorm-ts-node-commonjs"
},
"dependencies": {
"express": "^4.18.2",
"mysql2": "^3.6.0",
"typeorm": "^0.3.17",
"reflect-metadata": "^0.1.13"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/node": "^18.15.11",
"ts-node-dev": "^2.0.0",
"typescript": "^5.2.2"
},
"keywords": [],
"author": "",
"license": "ISC"
}
6. 動作確認
サーバーを起動し、APIエンドポイントを確認する。
npm run dev
- ユーザー一覧を取得:
GET /users
- ユーザーを取得:
GET /users/:id
- ユーザーを新規作成:
POST /users
- ユーザー情報を更新:
PUT /users/:id
- ユーザーを削除:
DELETE /users/:id
TypeORMを使用することで、データベース操作がシンプルかつ効率的になる。ORMを利用することで、データベースの変更にも柔軟に対応でき、SQLインジェクションのリスクも軽減される。ただし、パフォーマンスや学習コストに注意が必要だ。プロジェクトの規模や要件に応じて、適切にORMを活用することで、効率的なAPI開発が実現できるだろう。
ORMは、オブジェクト指向プログラミングとリレーショナルデータベースのギャップを埋めるための強力なツールである。開発効率の向上、セキュリティの強化、データベースの移行の容易さといったメリットがあり、多くのWebアプリケーション開発において広く使用されている。しかし、パフォーマンスや学習コスト、デバッグの難しさといったデメリットも存在するため、プロジェクトの規模や要件に応じてORMの採用を慎重に判断することが重要だ。
ORMはあくまでツールであり、目的や状況に応じて適切に使いこなすことで、その真価を発揮する。初心者にとっては難しく感じるかもしれないが、理解を深めることで開発作業が格段に楽になるはずである。ぜひ、プロジェクトでの導入を検討してみてほしい。