💽

TypeScript | TypeORM で Replication を指定して DB に接続する

2023/09/28に公開

はじめに

AWS Aurora PostgreSQL のリードレプリカを有効活用できるように、
TypeORM のデータソースで Replication を指定してみたいと思います。

Replication setup を試す

ドキュメントを参考に、検証していきます。

https://orkhan.gitbook.io/typeorm/docs/multiple-data-sources

データベースの作成

事前に以下のデータベースを作成しておきます。

  • test_db : ライターインスタンスを想定
  • test_db_ro : リーダーインスタンスを想定

プロジェクトを作成する

Quick Start を参考に、検証用のプロジェクトを作成します。

まずは、以下のコマンドを実行します。

npx typeorm init --name TypeormProject --database postgres

実行すると、以下のような構成でプロジェクトが作成されます。

% tree TypeormProject/ -I node_modules
TypeormProject/
├── README.md
├── package-lock.json
├── package.json
├── src
│   ├── data-source.ts
│   ├── entity
│   │   └── User.ts
│   ├── index.ts
│   └── migration
└── tsconfig.json
src/index.ts
import { AppDataSource } from "./data-source"
import { User } from "./entity/User"

AppDataSource.initialize().then(async () => {

    console.log("Inserting a new user into the database...")
    const user = new User()
    user.firstName = "Timber"
    user.lastName = "Saw"
    user.age = 25
    await AppDataSource.manager.save(user)
    console.log("Saved a new user with id: " + user.id)

    console.log("Loading users from the database...")
    const users = await AppDataSource.manager.find(User)
    console.log("Loaded users: ", users)

    console.log("Here you can setup and run express / fastify / any other framework.")

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

データソースを変更する

test_db に接続できるように、データソースを変更します。

data-source.ts
import "reflect-metadata"
import { DataSource } from "typeorm"
import { User } from "./entity/User"

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

npm start を実行して、動作するか確認してみます。

% npm start

> TypeormProject2@0.0.1 start
> ts-node src/index.ts

Inserting a new user into the database...
Saved a new user with id: 1
Loading users from the database...
Loaded users:  [ User { id: 1, firstName: 'Timber', lastName: 'Saw', age: 25 } ]
Here you can setup and run express / fastify / any other framework.

正常に動作することが確認できました。

replication を指定する

正常に動作することが分かったので、
data-source.ts を変更し、replication 設定を加えます。

masterslaves を、それぞれ指定すれば OK です。
上述した通り、以下のような構成を想定しています。

  • test_db: ライターインスタンスを想定
  • test_db_ro: リーダーインスタンスを想定
data-source.ts
import "reflect-metadata"
import { DataSource } from "typeorm"
import { User } from "./entity/User"

export const AppDataSource = new DataSource({
    type: "postgres",
-   host: "localhost",
-   port: 5432,
-   username: "postgres",
-   password: "postgres",
-   database: "test_db",
+   replication: {
+       master: {
+           host: "localhost",
+           port: 5432,
+           username: "postgres",
+           password: "postgres",
+           database: "test_db",
+       },
+       slaves: [{
+           host: "localhost",
+           port: 5432,
+           username: "postgres",
+           password: "postgres",
+           database: "test_db_ro",
+       }],
+   },
    synchronize: true,
    logging: false,
    entities: [User],
    migrations: [],
    subscribers: [],
})

この状態で npm start を実行すると、エラーが発生します。

% npm start

> TypeormProject2@0.0.1 start
> ts-node src/index.ts

Inserting a new user into the database...
Saved a new user with id: 2
Loading users from the database...
QueryFailedError: relation "user" does not exist

Saved a new user with id: 2 というログが出力されたこと、そして QueryFailedError: relation "user" does not exist というログが出力されたことから、INSERT は master に、SELECT は slave に接続されたことが予想されます。

ここで、test_db を復元して test_db_ro を再作成し接続してみます。

% npm start

> TypeormProject@0.0.1 start
> ts-node src/index.ts

Inserting a new user into the database...
Saved a new user with id: 3
Loading users from the database...
Loaded users:  [
  User { id: 1, firstName: 'Timber', lastName: 'Saw', age: 25 },
  User { id: 2, firstName: 'Timber', lastName: 'Saw', age: 25 }
]
Here you can setup and run express / fastify / any other framework.

test_db_ro には復元した時点のデータが 2 件なので、
id: 3 のデータがないことが分かります。

今回は 同期していない 異なるデータベースなので、このような結果になりますが、
Aurora のリードレプリカであれば、期待した結果が得られるはずです。

これで読み出しの負荷をリードレプリカに逃がすことで、
データベースサーバーの負荷を軽減できるでしょう🙌🎉

自動接続の仕様

All schema update and write operations are performed using master server. All simple queries performed by find methods or select query builder are using a random slave instance. All queries performed by query method are performed using the master instance.

スキーマの更新と書き込み操作はすべてマスター・サーバを使用して実行される。find メソッドや select クエリビルダで実行される単純なクエリはすべて、ランダムなスレーブインスタンスを使用します。クエリメソッドで実行されるすべてのクエリは、マスタインスタンスを使用して実行されます。

Multiple data sources, databases, schemas and replication setup

自動で切り替わるのは非常に便利ですね👀✨

明示的に master/slave を指定する

上記コードでは、自動で接続先が切り替わったことが分かると思いますが、
もちろん、明示的に接続先を指定することも可能です。

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

AppDataSource.initialize().then(async () => {

    console.log("Inserting a new user into the database...")
    const user = new User()
    user.firstName = "Timber"
    user.lastName = "Saw"
    user.age = 25
    await AppDataSource.manager.save(user)
    console.log("Saved a new user with id: " + user.id)

    console.log("Loading users from the database...")

    // master に接続する
    // ライターインスタンスへの接続を想定
    const masterQueryRunner = AppDataSource.createQueryRunner("master")
    try {
        const usersFromMaster = await AppDataSource
            .createQueryBuilder(User, "user", masterQueryRunner)
            .setQueryRunner(masterQueryRunner)
            .getMany()
        console.log("Loaded users from master: ", usersFromMaster)
    } finally {
        // 必ず release する
        await masterQueryRunner.release()
    }

    // slave に接続する
    // リーダーインスタンスへの接続を想定
    const slaveQueryRunner = AppDataSource.createQueryRunner("slave")
    try {
        const usersFromSlave = await AppDataSource
            .createQueryBuilder(User, "user", slaveQueryRunner)
            .setQueryRunner(slaveQueryRunner)
            .getMany()
        console.log("Loaded users from slave: ", usersFromSlave)
    } finally {
        // 必ず release する
        await slaveQueryRunner.release()
    }

    console.log("Here you can setup and run express / fastify / any other framework.")
}).catch(error => console.log(error))

master が 4 件で、slave が 2 件という期待通りの結果が得られました。

% npm start

> TypeormProject@0.0.1 start
> ts-node src/index.ts

Inserting a new user into the database...
Saved a new user with id: 4
Loading users from the database...
Loaded users from master:  [
  User { id: 1, firstName: 'Timber', lastName: 'Saw', age: 25 },
  User { id: 2, firstName: 'Timber', lastName: 'Saw', age: 25 },
  User { id: 3, firstName: 'Timber', lastName: 'Saw', age: 25 },
  User { id: 4, firstName: 'Timber', lastName: 'Saw', age: 25 }
]
Loaded users from slave:  [
  User { id: 1, firstName: 'Timber', lastName: 'Saw', age: 25 },
  User { id: 2, firstName: 'Timber', lastName: 'Saw', age: 25 }
]

まとめ

リーダーインスタンスとして見立てたデータベースを使って、
TypeORM のデータソースで Replication を指定できることが確認できました。

Replication を活用し、データベースサーバーの負荷を分散させていきましょう🥳🥳

注意事項

再接続できない問題があるようです。
必要に応じて対策するようにしましょう。

https://dev.classmethod.jp/articles/typeorm-mysql-poolcluster-reconnect/

参照

コラボスタイル Developers

Discussion