Help us understand the problem. What is going on with this article?

TypeORM環境をCLIで構築

背景

前回、sequelizeを利用したコードのtypescript化を試みて、modelの呼び出し元はtypescriptっぽく書くようにできたけど、モデル自体はtypescript化できずに、中途半端な感じで終わってしまった。

https://qiita.com/yusuke-ka/items/244d3dfafb578fd84b1a

sequelizeは、元々typescriptをサポートしていなかったため、typescriptと少し相性が悪いとの記事もいくつか見つけた。

そこで今回は最近伸びてきているTypeORMを試してみようと思う。
こちらは元々typescriptが前提となっているORMなので、typescript化で苦労することはなさそう。

googleトレンドで見ると、sequelizeに追いつく勢いで伸びてきている。
(むしろtypescriptに限定すれば、既にTypeORMの方が人気がある気がする)

image.png

TypeORMのCLIを使った環境構築

DBは以前インストールしたpostgresql(windows)を利用する。
https://qiita.com/yusuke-ka/items/448843020c0406363ba5#%E6%BA%96%E5%82%99

pgadmin4でデータベースインスタンスだけ作っておく。

image.png

データベース名("typeorm"とした)を入力して作成。

ここからは、コード エディタ(VS Code)上での作業。
まずはベースとなるnode環境を作る。

> mkdir typeorm
> cd typeorm
> yarn init

検証なので、とりあえず全部デフォルト設定。

続いて、TypeORMのインストール。
また、DBはpostgresqlを使うのでpg(node-postgres)もインストール。

> yarn add typeorm
> yarn add pg

TypeORMのCLIがyarnで簡単に使えるようにpackage.jsonに以下を追加しておく。

package.json
{
  ...
  "scripts": {
    "typeorm-cli": "typeorm"
  },
  ...
}

TypeORMのCLIを使って、express、postgresqlのTypeORM環境を一気に構築。

> yarn typeorm-cli init --express --database postgres

自動で以下のようなフォルダ/ファイルが生成される。

|- typeorm/
 |- src/
  |- controller/
    |- UserController.ts
  |- entry/
    |- User.ts
  |- migration/
  |- index.ts
  |- routes.ts 
 |- ormconfig.json
 |- tsconfig.json

設定ファイル(ormconfig.json)で接続先DBに合わせて設定を変更。

ormconfig.json
{
  "type": "postgres",
  "host": "localhost",
  "port": 5432,
  "username": "postgres",
  "password": "postgres",
  "database": "typeorm",
  "synchronize": true,
  "logging": false,
  "entities": ["src/entity/**/*.ts"],
  "migrations": ["src/migration/**/*.ts"],
  "subscribers": ["src/subscriber/**/*.ts"],
  "cli": {
    "entitiesDir": "src/entity",
    "migrationsDir": "src/migration",
    "subscribersDir": "src/subscriber"
  }
}

今回は以下だけ変更。

ormconfig.json
 ...
 "username": "postgres",
 "password": "postgres",
 "database": "typeorm",
 ...

これでTypeORMの環境構築は完了。簡単ですね。

TypeORMのCLIで自動構築された環境の確認

CLIで作成すると、サンプルとして"User"というモデルを操作するコードが自動で入っている。

自動生成されたモデル(src/entry/User.ts)は以下のようになっている。

User.ts
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";

@Entity()
export class User {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    firstName: string;

    @Column()
    lastName: string;

    @Column()
    age: number;

}

モデルを定義するときは、これを真似して<モデル名>.tsをentryフォルダ以下に配置すればよい模様。

続いて、コントローラー(src/controller/UserController.ts)は以下のようになっていた。

UserController.ts
import {getRepository} from "typeorm";
import {NextFunction, Request, Response} from "express";
import {User} from "../entity/User";

export class UserController {

    private userRepository = getRepository(User);

    async all(request: Request, response: Response, next: NextFunction) {
        return this.userRepository.find();
    }

    async one(request: Request, response: Response, next: NextFunction) {
        return this.userRepository.findOne(request.params.id);
    }

    async save(request: Request, response: Response, next: NextFunction) {
        return this.userRepository.save(request.body);
    }

    async remove(request: Request, response: Response, next: NextFunction) {
        let userToRemove = await this.userRepository.findOne(request.params.id);
        await this.userRepository.remove(userToRemove);
    }

}

ここではモデルに対する操作を書く感じですね。
全件取得、1件取得、保存(作成/更新)、削除の機能がサンプルとして実装されている。

getRepository(Hoge)で取得されるRepositoryをTypeORMが提供してくれているので、このRepositoryのメソッド(findやsaveなど)を呼び出して、DBにアクセスしているっぽい。

次は、index.ts。expressを使用する感じで自動生成されている。

index.ts
import "reflect-metadata";
import {createConnection} from "typeorm";
import * as express from "express";
import * as bodyParser from "body-parser";
import {Request, Response} from "express";
import {Routes} from "./routes";
import {User} from "./entity/User";

createConnection().then(async connection => {

    // create express app
    const app = express();
    app.use(bodyParser.json());

    // register express routes from defined application routes
    Routes.forEach(route => {
        (app as any)[route.method](route.route, (req: Request, res: Response, next: Function) => {
            const result = (new (route.controller as any))[route.action](req, res, next);
            if (result instanceof Promise) {
                result.then(result => result !== null && result !== undefined ? res.send(result) : undefined);

            } else if (result !== null && result !== undefined) {
                res.json(result);
            }
        });
    });

    // setup express app here
    // ...

    // start express server
    app.listen(3000);

    // insert new users for test
    await connection.manager.save(connection.manager.create(User, {
        firstName: "Timber",
        lastName: "Saw",
        age: 27
    }));
    await connection.manager.save(connection.manager.create(User, {
        firstName: "Phantom",
        lastName: "Assassin",
        age: 24
    }));

    console.log("Express server has started on port 3000. Open http://localhost:3000/users to see results");

}).catch(error => console.log(error));

routes.tsに定義されているRouteをforEachで回しているようなので、新しいAPIを追加するときには、このコード(index.ts)は変更せずに、routes.tsに定義を追加すれば良さそう。

最後のほうにある
// insert new users for test
以下はサーバー起動時にテストデータを入れているコードのようなので、実際に利用する際には消しておいた方がよさそう。

というか必要ないので、さっそく消しておく。

index.ts
import "reflect-metadata";
import {createConnection} from "typeorm";
import * as express from "express";
import * as bodyParser from "body-parser";
import {Request, Response} from "express";
import {Routes} from "./routes";

createConnection().then(async connection => {

    // create express app
    const app = express();
    app.use(bodyParser.json());

    // register express routes from defined application routes
    Routes.forEach(route => {
        (app as any)[route.method](route.route, (req: Request, res: Response, next: Function) => {
            const result = (new (route.controller as any))[route.action](req, res, next);
            if (result instanceof Promise) {
                result.then(result => result !== null && result !== undefined ? res.send(result) : undefined);

            } else if (result !== null && result !== undefined) {
                res.json(result);
            }
        });
    });

    // setup express app here
    // ...

    // start express server
    app.listen(3000);

    console.log("Express server has started on port 3000. ");

}).catch(error => console.log(error));

routes.tsを見てみる。これも自動生成されている。

routes.ts
import {UserController} from "./controller/UserController";

export const Routes = [{
    method: "get",
    route: "/users",
    controller: UserController,
    action: "all"
}, {
    method: "get",
    route: "/users/:id",
    controller: UserController,
    action: "one"
}, {
    method: "post",
    route: "/users",
    controller: UserController,
    action: "save"
}, {
    method: "delete",
    route: "/users/:id",
    controller: UserController,
    action: "remove"
}];

サンプルとして、全件取得、1件取得、追加(更新)、削除の4つのAPIが定義されている模様。

最後に設定系のファイルを見てみる。

typeORMの設定ファイル(ormconfig.json)は、先ほど見たので、package.jsonから。

package.json
{
   "name": "typeorm",
   "version": "1.0.0",
   "main": "index.js",
   "license": "MIT",
   "scripts": {
      "typeorm-cli": "typeorm",
      "start": "ts-node src/index.ts"
   },
   "dependencies": {
      "typeorm": "0.2.25",
      "reflect-metadata": "^0.1.10",
      "pg": "^7.3.0",
      "express": "^4.15.4",
      "body-parser": "^1.18.1"
   },
   "devDependencies": {
      "ts-node": "3.3.0",
      "@types/node": "^8.0.29",
      "typescript": "3.3.3333"
   }
}

scriptsstartが追加されている。これでサーバーを実行する模様。
あとは、必要な依存が自動的に追加されている。

tsconfig.jsonも確認。

tsconfig.json
{
   "compilerOptions": {
      "lib": [
         "es5",
         "es6"
      ],
      "target": "es5",
      "module": "commonjs",
      "moduleResolution": "node",
      "outDir": "./build",
      "emitDecoratorMetadata": true,
      "experimentalDecorators": true,
      "sourceMap": true
   }
}

typeORMのREADMEに書いてある設定が自動的に入っているようです。
https://github.com/typeorm/typeorm#installation

TypeORMのサンプルコードを実行してみる。

TypeORMのコードを実行してみようと思う。

まずは、モデルをDBに反映してみる。

migration:generateを実行すると、DBの内容とモデルを比較してマイグレーションファイルを作成してくれるようなので、これを実行してみる。

> yarn typeorm-cli migration:generate -n UserMigration

エラーが発生した。

yarn run v1.22.4
$ typeorm migration:generate -n UserMigration
Error during migration generation:
D:\study\orm\typeorm\src\entity\User.ts:1
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";
^^^^^^

SyntaxError: Cannot use import statement outside a module
...

本家サイトのここを参考にしてやってみる。
https://github.com/typeorm/typeorm/blob/master/docs/using-cli.md#installing-cli

package.jsonのscriptsを以下のように変更(typeormのscriptを追加)。
(ts-nodeは自動でインストールされているはずだけど、もし入っていなければ、インストールする必要があるかも)

   ...
   "scripts": {
      "typeorm-cli": "typeorm",
      "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js",
      "start": "ts-node src/index.ts"
   },
   ...

再度実行してみる。
(今度は、yarn typeorm-cliではなく、yarn typeorm)

> yarn typeorm migration:generate -n UserMigration

今度は成功。

yarn run v1.22.4
$ node --require ts-node/register ./node_modules/typeorm/cli.js migration:generate -n UserMigration
Migration D:\study\orm\typeorm/src/migration/1593226939171-UserMigration.ts has been generated successfully.
Done in 2.03s.

こんな感じでマイグレーションファイルが生成された。

1593226939171-UserMigration.ts
import {MigrationInterface, QueryRunner} from "typeorm";

export class UserMigration1593226939171 implements MigrationInterface {
    name = 'UserMigration1593226939171'

    public async up(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`CREATE TABLE "user" ("id" SERIAL NOT NULL, "firstName" character varying NOT NULL, "lastName" character varying NOT NULL, "age" integer NOT NULL, CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`);
    }

    public async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`DROP TABLE "user"`);
    }

}

DBに反映させてみる。

> yarn typeorm migration:run

userテーブルが作成された。

image.png

サーバーを起動。

> yarn start

今回もchromeの拡張ツール「Advanced REST client」で動作確認。
http://localhost:3000/users に各種リクエストを送ってみる。

routes.tsに定義されているRouteを見てURLを指定。

まずは全件取得。

image.png

何も登録してないので、結果は空の配列。

つづいて、ユーザーを登録してみる。

image.png

application/jsonで指定したパラメータでユーザーが登録された。

DBにも登録されている。

image.png

同様にもう一人ユーザーを追加した後、再度全件取得してみる。

image.png

今度は配列が空じゃない状態で返ってきた。

パスにIDを指定して取得。

image.png

1件だけ返ってきた。

データ更新。
パラメータにID(integer)を指定してPOSTリクエストを送る。

image.png

データが更新された。

最後に、削除を試してみる。

image.png

DBからデータが消えているようだが、レスポンスが返ってこない。

index.tsでresultがundefinedの時にレスポンスを返していないのが原因かと思われる。

index.ts
    ...
    // register express routes from defined application routes
    Routes.forEach(route => {
        (app as any)[route.method](route.route, (req: Request, res: Response, next: Function) => {
            const result = (new (route.controller as any))[route.action](req, res, next);
            if (result instanceof Promise) {
                result.then(result => result !== null && result !== undefined ? res.send(result) : undefined); // ← ココ

            } else if (result !== null && result !== undefined) {
                res.json(result);
            }
        });
    });
    ...

試しに200を返すように書き換えてみる。

index.ts
    ...
    // register express routes from defined application routes
    Routes.forEach(route => {
        (app as any)[route.method](route.route, (req: Request, res: Response, next: Function) => {
            const result = (new (route.controller as any))[route.action](req, res, next);
            if (result instanceof Promise) {
                result.then(result => result !== null && result !== undefined ? res.send(result) : res.send(200)); // ← ココ

            } else if (result !== null && result !== undefined) {
                res.json(result);
            }
        });
    });
    ...

他のIDを指定して削除を再実行。

image.png

今度はレスポンスが返ってきた。
修正が正しいかどうかは置いといて、原因は特定できた。

さいごに

今回はTypeORMの環境構築を試してみた。

いくつかハマりポイントはあったもののsequelizeをtypescript化するよりは遥かに楽だったし、モデルもコントローラーもすべてtypescriptで書けそうなので、sequelizeの時のように中途半端な感じにならないのも良さげ。
(もう少し時間をかけて調べれば、sequelizeもいい感じにtypescript化できる方法があるのかもしれないけど…)

sequelizeほど成熟していない感はあるものの、typescriptで書くことが前提ならTypeORMの方がやりやすい気がします。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした