Перейти к основному содержанию

Работа с несколькими источниками данных, базами данных, схемами и настройка репликации

Неофициальный Бета-перевод

Эта страница переведена PageTurner AI (бета). Не одобрена официально проектом. Нашли ошибку? Сообщить о проблеме →

Использование нескольких источников данных

Для подключения к нескольким базам данных через разные источники данных достаточно создать несколько экземпляров DataSource:

import { DataSource } from "typeorm"

const db1DataSource = new DataSource({
type: "mysql",
host: "localhost",
port: 3306,
username: "root",
password: "admin",
database: "db1",
entities: [__dirname + "/entities/*{.js,.ts}"],
synchronize: true,
})

const db2DataSource = new DataSource({
type: "mysql",
host: "localhost",
port: 3306,
username: "root",
password: "admin",
database: "db2",
entities: [__dirname + "/entities/*{.js,.ts}"],
synchronize: true,
})

Использование нескольких баз данных в одном источнике данных

Чтобы работать с несколькими базами данных в рамках одного источника данных, укажите имя базы данных для каждой сущности:

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity({ database: "secondDB" })
export class User {
@PrimaryGeneratedColumn()
id: number

@Column()
firstName: string

@Column()
lastName: string
}
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity({ database: "thirdDB" })
export class Photo {
@PrimaryGeneratedColumn()
id: number

@Column()
url: string
}

Сущность User будет создана в базе данных secondDB, а Photo — в thirdDB. Все остальные сущности будут созданы в базе данных по умолчанию, указанной в настройках источника данных.

Для выборки данных из другой базы данных достаточно указать сущность:

const users = await dataSource
.createQueryBuilder()
.select()
.from(User, "user")
.addFrom(Photo, "photo")
.andWhere("photo.userId = user.id")
.getMany() // userId is not a foreign key since its cross-database request

Этот код сгенерирует примерно такой SQL-запрос (зависит от типа БД):

SELECT * FROM "secondDB"."user" "user", "thirdDB"."photo" "photo"
WHERE "photo"."userId" = "user"."id"

Также можно указать путь к таблице вместо сущности:

const users = await dataSource
.createQueryBuilder()
.select()
.from("secondDB.user", "user")
.addFrom("thirdDB.photo", "photo")
.andWhere("photo.userId = user.id")
.getMany() // userId is not a foreign key since its cross-database request

Эта функция поддерживается только в MySQL и MSSQL.

Использование нескольких схем в одном источнике данных

Для работы с несколькими схемами просто укажите параметр schema для каждой сущности:

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity({ schema: "secondSchema" })
export class User {
@PrimaryGeneratedColumn()
id: number

@Column()
firstName: string

@Column()
lastName: string
}
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity({ schema: "thirdSchema" })
export class Photo {
@PrimaryGeneratedColumn()
id: number

@Column()
url: string
}

Сущность User будет создана в схеме secondSchema, а Photo — в thirdSchema. Все остальные сущности будут созданы в схеме по умолчанию, указанной в настройках источника данных.

Для выборки данных из другой схемы достаточно указать сущность:

const users = await dataSource
.createQueryBuilder()
.select()
.from(User, "user")
.addFrom(Photo, "photo")
.andWhere("photo.userId = user.id")
.getMany() // userId is not a foreign key since its cross-database request

Этот код сгенерирует примерно такой SQL-запрос (зависит от типа БД):

SELECT * FROM "secondSchema"."question" "question", "thirdSchema"."photo" "photo"
WHERE "photo"."userId" = "user"."id"

Также можно указать путь к таблице вместо сущности:

const users = await dataSource
.createQueryBuilder()
.select()
.from("secondSchema.user", "user") // in mssql you can even specify a database: secondDB.secondSchema.user
.addFrom("thirdSchema.photo", "photo") // in mssql you can even specify a database: thirdDB.thirdSchema.photo
.andWhere("photo.userId = user.id")
.getMany()

Эта функция поддерживается только в PostgreSQL и MSSQL. В MSSQL можно комбинировать базы данных и схемы, например:

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity({ database: "secondDB", schema: "public" })
export class User {
@PrimaryGeneratedColumn()
id: number

@Column()
firstName: string

@Column()
lastName: string
}

Репликация

Вы можете настроить репликацию чтения/записи через TypeORM. Пример настроек репликации:

const datasource = new DataSource({
type: "mysql",
logging: true,
replication: {
master: {
host: "server1",
port: 3306,
username: "test",
password: "test",
database: "test",
},
slaves: [
{
host: "server2",
port: 3306,
username: "test",
password: "test",
database: "test",
},
{
host: "server3",
port: 3306,
username: "test",
password: "test",
database: "test",
},
],
},
})

При наличии реплик TypeORM по умолчанию направляет все возможные запросы на ведомые серверы.

  • все запросы, выполняемые методами find или SelectQueryBuilder, используют случайную реплику (slave)

  • все запросы на запись через update, create, InsertQueryBuilder, UpdateQueryBuilder и т.д. используют ведущий сервер (master)

  • все сырые запросы (raw queries) через вызов .query() используют ведущий сервер (master)

  • все операции обновления схемы выполняются через ведущий сервер (master)

Явное указание цели запроса

По умолчанию TypeORM направляет запросы на чтение на случайную реплику, а на запись — на ведущий сервер. Это означает, что после добавления настроек replication, все существующие исполнители запросов без явного указания режима репликации начнут использовать ведомые серверы. Это улучшает масштабируемость, но если для некоторых запросов требуются актуальные данные, необходимо явно указать режим репликации при создании исполнителя запросов.

Для явного использования ведущего сервера (master) при чтении укажите ReplicationMode при создании QueryRunner:

const masterQueryRunner = dataSource.createQueryRunner("master")
try {
const postsFromMaster = await dataSource
.createQueryBuilder(Post, "post", masterQueryRunner) // you can either pass QueryRunner as an optional argument with query builder
.setQueryRunner(masterQueryRunner) // or use setQueryRunner which sets or overrides query builder's QueryRunner
.getMany()
} finally {
await masterQueryRunner.release()
}

Для использования реплики в сырых запросах укажите режим slave при создании исполнителя запросов:

const slaveQueryRunner = dataSource.createQueryRunner("slave")
try {
const userFromSlave = await slaveQueryRunner.query(
"SELECT * FROM users WHERE id = $1",
[userId],
slaveQueryRunner,
)
} finally {
return slaveQueryRunner.release()
}

Важно: Вручную созданные экземпляры QueryRunner должны явно освобождаться. Если не освобождать исполнители запросов, они будут удерживать соединение из пула, блокируя его для других запросов.

Изменение цели чтения по умолчанию

Если вы не хотите, чтобы чтение по умолчанию шло на реплики (slave), измените поведение через параметр defaultMode: "master" в настройках репликации:

const datasource = new DataSource({
type: "mysql",
logging: true,
replication: {
// set the default destination for read queries as the master instance
defaultMode: "master",
master: {
host: "server1",
port: 3306,
username: "test",
password: "test",
database: "test",
},
slaves: [
{
host: "server2",
port: 3306,
username: "test",
password: "test",
database: "test",
},
],
},
})

В этом режиме запросы по умолчанию не направляются на реплики, и для их использования потребятся явные вызовы .createQueryRunner("slave").

Если вы впервые добавляете настройки репликации в существующее приложение, это хороший вариант, чтобы гарантировать отсутствие немедленных изменений в поведении, и вместо этого вы можете постепенно внедрять чтение с реплик для каждого обработчика запросов (query runner) отдельно.

Поддерживаемые драйверы

Репликация поддерживается драйверами соединений MySQL, PostgreSQL, SQL Server, Cockroach, Oracle и Spanner.

Репликация MySQL поддерживает дополнительные параметры конфигурации:

{
replication: {
master: {
host: "server1",
port: 3306,
username: "test",
password: "test",
database: "test"
},
slaves: [{
host: "server2",
port: 3306,
username: "test",
password: "test",
database: "test"
}, {
host: "server3",
port: 3306,
username: "test",
password: "test",
database: "test"
}],

/**
* If true, PoolCluster will attempt to reconnect when connection fails. (Default: true)
*/
canRetry: true,

/**
* If connection fails, node's errorCount increases.
* When errorCount is greater than removeNodeErrorCount, remove a node in the PoolCluster. (Default: 5)
*/
removeNodeErrorCount: 5,

/**
* If connection fails, specifies the number of milliseconds before another connection attempt will be made.
* If set to 0, then node will be removed instead and never re-used. (Default: 0)
*/
restoreNodeTimeout: 0,

/**
* Determines how slaves are selected:
* RR: Select one alternately (Round-Robin).
* RANDOM: Select the node by random function.
* ORDER: Select the first node available unconditionally.
*/
selector: "RR"
}
}