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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/546387a3-bce8-6b20-2a92-8ec431eea7af.png)


# TypeORMのCLIを使った環境構築

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/db4d6e49-1bbb-044a-c543-25f51bd3829f.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"
   }
}
```

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

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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/b28c4040-0ee0-dcae-c134-6961f42d4a04.png)


サーバーを起動。

```
> yarn start
```

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

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

まずは全件取得。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/dd76e366-429f-d4c5-5ea1-1f21362af0e1.png)

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/4e91f161-aa4d-c021-70f3-c7a203473fdc.png)

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

DBにも登録されている。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/e55dc8f8-a7b5-5d04-b8c5-62a48b314314.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/9c6ea039-aed4-fd49-3ae9-86dcf2d55be9.png)

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/02c415a3-9060-eaff-9d0f-5e0d0063fd2e.png)

1件だけ返ってきた。

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/2b1c9dfe-822d-bc6d-d7ea-16e1e20da21e.png)

データが更新された。

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/1f082b9e-ec02-b284-b72e-f87ee94ec6f0.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/45d13fc5-5137-470e-8f01-1a304c3645e0.png)

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

# さいごに

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

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

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