TypeORMで生成されるSQLをCLIを使って確認してみた

2023.03.30

TypeORMを使っていると、このメソッドで実際にはどんなSQLが生成されているのだろう?と気になることがあります。

TypeORMでは、設定でlogging: true, logger: ‘file’と指定することでSQLログをファイルに出力することはできます。しかし実際のコードを動かさなければならないとなると、「実装中でコンパイルエラーが出ている」「動かすために少し手間や時間がいる」「周辺のコードも動いてしまう」など都合が悪い場合もあります。

そこで、手軽にTypeORMを動かして生成されるSQLを確認できる方法はないかと調べていたところ、当ブログに以下の記事がありました。

TypeORM をサクッと試せる Docker 環境を TypeORM CLI を使って構築する方法 | DevelopersIO

この記事では、上記の記事を参考にしてTypeORM CLIを使って手軽に色々試してみることができる環境を構築し、実際に生成されるSQLを確認できるようにしてみます。

前提

この記事で出てくるモジュール類のバージョンは以下のようになります。

  • Node.js 14.17.4
  • TypeORM 0.3.12
  • ts-node 10.7.0

環境構築

プロジェクトフォルダを作成し、以下コマンドを実行します。

npx typeorm init --docker

Done! Start playing with a new project! と表示されれば成功です。フォルダ内にファイル群が作成されます。

README.mdにこのプロジェクトを動かすためのコマンドが記載されているので、順番に実行していきます。

Steps to run this project:

1. Run `npm i` command
2. Run `docker-compose up` command
3. Run `npm start` command

このとき少し注意点があります。

まず、このプロジェクトはデフォルトで5432ポートで起動します。既に5432ポートを使っていて別のポート番号で起動したい場合は、docker-compose.ymlでポートの設定を変更します。(以下は5434ポートに変更した例)

ports:
  - "5434:5432"

併せて、TypeScriptのコード上でDBに接続するときの設定も変更します。src/data-source.tsでポートの設定を変更します。

export const AppDataSource = new DataSource({
    type: "postgres",
    host: "localhost",
    port: 5434,
    username: "test",
    password: "test",
    database: "test",
    synchronize: true,
    logging: false,
    entities: [User],
    migrations: [],
    subscribers: [],
})

また、2番目のコマンドを実行するとき、書かれたコマンドをそのまま実行するとコンテナが制御を握ってしまい、そのターミナルではコマンドが打てなくなります。

別のターミナルを使えば問題ないですが、一つのターミナルで使いたいという場合は-dオプションをつけてバックグラウンドで実行されるようにします。

docker-compose up -d

npm startを実行して、SQLクライアントソフトで接続できれば環境構築は完了です。(ユーザ名やパスワードなど接続情報はdata-source.tsに記載のある通りです)

ちなみに、終了するコマンドは以下です。

docker-compose down

SQLがログに出力されるようにする

src/data-source.tslogging: falseという記述がすでにあるので、trueに変更します。また、ファイルに出力したいのでロガーとしてファイルを設定します。

export const AppDataSource = new DataSource({
    type: "postgres",
    host: "localhost",
    port: 5434,
    username: "test",
    password: "test",
    database: "test",
    synchronize: true,
    logging: true,
    logger: 'file',
    entities: [User],
    migrations: [],
    subscribers: [],
})

これでnpm startしてみます。

ormlogs.logというファイルが作成され、中を見てみるとSELECT文などが出力されています。

これで、実行したTypeORMのメソッドがどんなSQLを生成するのかログで確認することができるようになりました。

実際に試してみる

index.tsを以下のように編集します。

import { AppDataSource } from "./data-source"
import { User } from "./entity/User"

AppDataSource.initialize().then(async () => {
    const user = new User()
    user.firstName = "user1"
    user.lastName = "user1"
    user.age = 25
    await AppDataSource.manager.save(user)
}).catch(error => console.log(error))

この状態で起動すると、以下のログが出力されます。単純なINSERT文が発行されています。

[2023-03-30T06:51:15.168Z][QUERY]: START TRANSACTION
[2023-03-30T06:51:15.173Z][QUERY]: INSERT INTO "user"("firstName", "lastName", "age") VALUES ($1, $2, $3) RETURNING "id" -- PARAMETERS: ["user1","user1",25]
[2023-03-30T06:51:15.224Z][QUERY]: COMMIT

save関数を連続して2回実行されるようにしてみます。save関数はデータがDBになければ登録、すでにあれば更新します。

import { AppDataSource } from "./data-source"
import { User } from "./entity/User"

AppDataSource.initialize().then(async () => {
    const user = new User()
    user.firstName = "user1"
    user.lastName = "user1"
    user.age = 25
    await AppDataSource.manager.save(user)
    await AppDataSource.manager.save(user)
}).catch(error => console.log(error))

ログを見てみると、

[2023-03-30T07:02:44.544Z][QUERY]: START TRANSACTION
[2023-03-30T07:02:44.549Z][QUERY]: INSERT INTO "user"("firstName", "lastName", "age") VALUES ($1, $2, $3) RETURNING "id" -- PARAMETERS: ["user1","user1",25]
[2023-03-30T07:02:44.597Z][QUERY]: COMMIT
[2023-03-30T07:02:44.612Z][QUERY]: SELECT "User"."id" AS "User_id", "User"."firstName" AS "User_firstName", "User"."lastName" AS "User_lastName", "User"."age" AS "User_age" FROM "user" "User" WHERE "User"."id" IN ($1) -- PARAMETERS: [514]

2回目のsave関数の実行時にまずSELECTをしてデータが存在しているかを確かめ、変更がないのでUPDATE文を発行せずに終わっていることがわかります。

2回目のsave実行前に値を一部変更してみます。

import { AppDataSource } from "./data-source"
import { User } from "./entity/User"

AppDataSource.initialize().then(async () => {
    const user = new User()
    user.firstName = "user1"
    user.lastName = "user1"
    user.age = 25
    await AppDataSource.manager.save(user)
    user.age = 26
    await AppDataSource.manager.save(user)
}).catch(error => console.log(error))

ログを見ると、SELECTをしてデータが存在しているかを確かめ、変更があるのでUPDATE文を発行していることがわかります。

[2023-03-30T07:07:13.621Z][QUERY]: START TRANSACTION
[2023-03-30T07:07:13.626Z][QUERY]: INSERT INTO "user"("firstName", "lastName", "age") VALUES ($1, $2, $3) RETURNING "id" -- PARAMETERS: ["user1","user1",25]
[2023-03-30T07:07:13.677Z][QUERY]: COMMIT
[2023-03-30T07:07:13.694Z][QUERY]: SELECT "User"."id" AS "User_id", "User"."firstName" AS "User_firstName", "User"."lastName" AS "User_lastName", "User"."age" AS "User_age" FROM "user" "User" WHERE "User"."id" IN ($1) -- PARAMETERS: [515]
[2023-03-30T07:07:13.702Z][QUERY]: START TRANSACTION
[2023-03-30T07:07:13.709Z][QUERY]: UPDATE "user" SET "age" = $1 WHERE "id" IN ($2) -- PARAMETERS: [26,515]
[2023-03-30T07:07:13.754Z][QUERY]: COMMIT

2つのsaveを同一トランザクション内で処理するようにしてみます。

import { AppDataSource } from "./data-source"
import { User } from "./entity/User"

AppDataSource.initialize().then(async () => {
    await AppDataSource.manager.transaction(async (transactionManager) => {
        const user = new User()
        user.firstName = "user1"
        user.lastName = "user1"
        user.age = 25
        await transactionManager.save(user)
        user.age = 26
        await transactionManager.save(user)
    })
}).catch(error => console.log(error))

トランザクション内でINSERT、SELECT、UPDATEが行われているということがわかります。

[2023-03-30T07:09:28.212Z][QUERY]: START TRANSACTION
[2023-03-30T07:09:28.218Z][QUERY]: INSERT INTO "user"("firstName", "lastName", "age") VALUES ($1, $2, $3) RETURNING "id" -- PARAMETERS: ["user1","user1",25]
[2023-03-30T07:09:28.276Z][QUERY]: SELECT "User"."id" AS "User_id", "User"."firstName" AS "User_firstName", "User"."lastName" AS "User_lastName", "User"."age" AS "User_age" FROM "user" "User" WHERE "User"."id" IN ($1) -- PARAMETERS: [516]
[2023-03-30T07:09:28.286Z][QUERY]: UPDATE "user" SET "age" = $1 WHERE "id" IN ($2) -- PARAMETERS: [26,516]
[2023-03-30T07:09:28.290Z][QUERY]: COMMIT

おわりに

この記事で試したのは一例ですが、書いたコードが実際にはどんなSQL文を生成しているのかを知ることはバグや想定外の動きを防ぐためにも大事だと思います。

実際のコードを動かしづらいという場面で、数回コマンドを叩くだけでTypeORMが動く環境を構築できるのはありがたいなと感じました。

この記事がどなたかの参考になれば幸いです。