Nest не зависит от базы данных, что позволяет легко интегрироваться с любой базой данных SQL или NoSQL. У вас есть несколько доступных вариантов, в зависимости от ваших предпочтений. На самом общем уровне подключение Nest к базе данных - это просто вопрос загрузки соответствующего драйвера Node.js для базы данных, точно так же, как это делается с Express или Fastify.
Вы также можете напрямую использовать любую библиотеку или ORM для интеграции баз данных Node.js общего назначения, например, MikroORM, Sequelize, Knex. js, TypeORM и Prisma, чтобы работать на более высоком уровне абстракции.
Для удобства Nest обеспечивает тесную интеграцию с TypeORM и Sequelize "из коробки" с помощью пакетов @nestjs/typeorm
и @nestjs/sequelize
соответственно, которые мы рассмотрим в текущей главе, и Mongoose с помощью @nestjs/mongoose
. Эти интеграции предоставляют дополнительные возможности, специфичные для NestJS, такие как инъекция модели/репозитория, тестируемость и асинхронная конфигурация, чтобы сделать доступ к выбранной вами базе данных еще проще.
Интеграция с TypeORM
Для интеграции с базами данных SQL и NoSQL Nest предоставляет пакет @nestjs/typeorm
. Nest использует TypeORM, потому что это наиболее зрелый объектно-реляционный маппер (ORM), доступный для TypeScript. Поскольку он написан на TypeScript, он хорошо интегрируется с фреймворком Nest.
Чтобы начать его использовать, сначала установите необходимые зависимости. В этой главе мы продемонстрируем использование популярной реляционной СУБД MySQL, но TypeORM обеспечивает поддержку многих реляционных баз данных, таких как PostgreSQL, Oracle, Microsoft SQL Server, SQLite и даже NoSQL баз данных, таких как MongoDB. Процедура, которую мы рассмотрим в этой главе, будет одинаковой для любой базы данных, поддерживаемой TypeORM. Вам просто нужно будет установить соответствующие клиентские библиотеки API для выбранной вами базы данных.
$ npm install --save @nestjs/typeorm typeorm mysql2
После завершения процесса установки мы можем импортировать TypeOrmModule
в корневой AppModule
.
app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [],
synchronize: true,
}),
],
})
export class AppModule {}
Установка
synchronize: true
не должна использоваться в продакшне - иначе вы можете потерять данные на проде.
Метод forRoot()
поддерживает все свойства конфигурации, открываемые функцией createConnection()
из пакета TypeORM. Кроме того, существует несколько дополнительных свойств конфигурации, описанных ниже.
retryAttempts | Количество попыток подключения к базе данных (по умолчанию: 10 ) |
retryDelay | Задержка между попытками повторного подключения (мс)(по умолчанию: 3000 ) |
autoLoadEntities | Если true , сущности будут загружаться автоматически (по умолчанию: false ) |
keepConnectionAlive | Если true , соединение не будет закрываться при завершении работы приложения (по умолчанию: false ) |
В качестве альтернативы, вместо передачи объекта конфигурации в forRoot()
, мы можем создать файл ormconfig.json
в корневом каталоге проекта.
{
"type": "mysql",
"host": "localhost",
"port": 3306,
"username": "root",
"password": "root",
"database": "test",
"entities": ["dist/**/*.entity{.ts,.js}"],
"synchronize": true
}
Затем мы можем вызвать forRoot()
без каких-либо опций:
app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [TypeOrmModule.forRoot()],
})
export class AppModule {}
Статические glob-пути (например,
dist/**/*.entity{ .ts,.js}
) не будут правильно работать с webpack.
Обратите внимание, что файл
ormconfig.json
загружается библиотекойtypeorm
. Таким образом, все дополнительные свойства, описанные выше (которые поддерживаются с помощью методаforRoot()
- например,autoLoadEntities
иretryDelay
) не будут работать. К счастью, TypeORM предоставляет функциюgetConnectionOptions
, которая считывает параметры подключения из файлаormconfig
или переменных окружения. С ее помощью вы все еще можете использовать конфигурационный файл и установить специфические для Nest опции, как показано ниже:TypeOrmModule.forRootAsync({ useFactory: async () => Object.assign(await getConnectionOptions(), { autoLoadEntities: true, }), });
Как только это будет сделано, объекты TypeORM Connection
и EntityManager
будут доступны для внедрения, например, во всем проекте (без необходимости импортировать какие-либо модули):
app.module.ts
import { Connection } from 'typeorm';
@Module({
imports: [TypeOrmModule.forRoot(), UsersModule],
})
export class AppModule {
constructor(private connection: Connection) {}
}
Паттерн репозиторий
TypeORM поддерживает шаблон проектирования репозиторий, поэтому каждая сущность имеет свой собственный репозиторий. Эти хранилища могут быть получены из соединения с базой данных.
Чтобы продолжить пример, нам нужна хотя бы одна сущность. Давайте определим сущность User
.
user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column({ default: true })
isActive: boolean;
}
Файл сущности
User
находится в директорииusers
. Этот каталог содержит все файлы, относящиеся к модулюUsersModule
. Вы можете решить, где хранить файлы модели, однако мы рекомендуем создавать их рядом с их доменом, в соответствующем каталоге модуля.
Чтобы начать использовать сущность User
, нам нужно сообщить о ней TypeORM, вставив ее в массив entities
в опциях метода модуля forRoot()
(если вы не используете статический glob-путь):
app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users/user.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [User],
synchronize: true,
}),
],
})
export class AppModule {}
Далее рассмотрим UsersModule
:
users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
Этот модуль использует метод forFeature()
для определения того, какие хранилища зарегистрированы в текущей области видимости. И теперь, мы можем внедрить UsersRepository
в UsersService
с помощью декоратора @InjectRepository()
:
users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
findAll(): Promise<User[]> {
return this.usersRepository.find();
}
findOne(id: string): Promise<User> {
return this.usersRepository.findOne(id);
}
async remove(id: string): Promise<void> {
await this.usersRepository.delete(id);
}
}
Не забудьте импортировать
UsersModule
в корневойAppModule
.
Если вы хотите использовать репозиторий вне модуля, который импортирует TypeOrmModule.forFeature
, вам нужно будет реэкспортировать сгенерированные им провайдеры. Вы можете сделать это, экспортировав весь модуль, как показано ниже:
users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
exports: [TypeOrmModule]
})
export class UsersModule {}
Теперь, если мы импортируем UsersModule
в UserHttpModule
, мы можем использовать @InjectRepository(User)
в провайдерах последнего модуля.
users-http.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users.module';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
@Module({
imports: [UsersModule],
providers: [UsersService],
controllers: [UsersController]
})
export class UserHttpModule {}
Отношения
Отношения - это ассоциации, установленные между двумя или более таблицами. Отношения основаны на общих полях каждой таблицы, часто с использованием первичных и внешних ключей.
Существует три типа отношений:
One-to-one | Каждая строка в первичной таблице имеет одну и только одну связанную строку во внешней таблице. Для определения этого типа связи используйте декоратор @OneToOne() . |
One-to-many / Many-to-one | Каждая строка в первичной таблице имеет одну или несколько связанных строк во внешней таблице. Для определения этого типа отношения используйте декораторы @OneToMany() и @ManyToOne() . |
Many-to-many | Каждая строка в первичной таблице имеет много связанных строк во внешней таблице, а каждая запись во внешней таблице имеет много связанных строк в первичной таблице. Используйте декоратор @ManyToMany() для определения этого типа отношений. |
Чтобы определить отношения в сущностях, используйте соответствующие декораторы. Например, чтобы определить, что каждый User
может иметь несколько фотографий, используйте декоратор @OneToMany()
.
user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { Photo } from '../photos/photo.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column({ default: true })
isActive: boolean;
@OneToMany(type => Photo, photo => photo.user)
photos: Photo[];
}
Автоматическая загрузка сущностей
Ручное добавление сущностей в массив entities
опций соединения с базой может быть утомительным. Кроме того, обращение к сущностям из корневого модуля нарушает границы домена приложения и приводит к утечке деталей реализации в другие части приложения. Для решения этой проблемы можно использовать статические glob-пути (например, dist/**/*.entity{ .ts,.js}
).
Обратите внимание, однако, что glob-пути не поддерживаются webpack, поэтому если вы собираете приложение в рамках монорепозитория, вы не сможете их использовать. Для решения этой проблемы предлагается альтернативное решение. Для автоматической загрузки сущностей установите свойство autoLoadEntities
объекта конфигурации (передаваемого в метод forRoot()
) в значение true
, как показано ниже:
app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
...
autoLoadEntities: true,
}),
],
})
export class AppModule {}
При указании этой опции каждая сущность, зарегистрированная через метод forFeature()
, будет автоматически добавлена в массив entities
объекта конфигурации.
Обратите внимание, что сущности, которые не зарегистрированы через метод
forFeature()
, а только ссылаются на сущность (через отношения), не будут включены с помощью настройкиautoLoadEntities
.
Разделение определения сущности
Вы можете определить сущность и ее столбцы прямо в модели, используя декораторы. Но некоторые люди предпочитают определять сущности и их колонки в отдельных файлах, используя "схемы сущностей".
import { EntitySchema } from 'typeorm';
import { User } from './user.entity';
export const UserSchema = new EntitySchema<User>({
name: 'User',
target: User,
columns: {
id: {
type: Number,
primary: true,
generated: true,
},
firstName: {
type: String,
},
lastName: {
type: String,
},
isActive: {
type: Boolean,
default: true,
},
},
relations: {
photos: {
type: 'one-to-many',
target: 'Photo', // the name of the PhotoSchema
},
},
});
Если вы указали опцию
target
, значение опцииname
должно совпадать с именем целевого класса. Если вы не указываетеtarget
, вы можете использовать любое имя.
Nest позволяет вам использовать экземпляр EntitySchema
везде, где ожидается Entity
, например:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserSchema } from './user.schema';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
imports: [TypeOrmModule.forFeature([UserSchema])],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
Транзакции
Транзакция базы данных символизирует единицу работы, выполняемую в системе управления базами данных над базой данных и обрабатываемую последовательным и надежным образом независимо от других транзакций. Транзакция обычно представляет собой любое изменение в базе данных.
Существует множество различных стратегий для обработки транзакций TypeORM. Мы рекомендуем использовать класс QueryRunner
, поскольку он дает полный контроль над транзакцией.
Сначала нам нужно внедрить объект Connection
в класс обычным способом:
@Injectable()
export class UsersService {
constructor(private connection: Connection) {}
}
Класс
Connection
импортирован из пакетаtypeorm
.
Теперь мы можем использовать этот объект для создания транзакции.
async createMany(users: User[]) {
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await queryRunner.manager.save(users[0]);
await queryRunner.manager.save(users[1]);
await queryRunner.commitTransaction();
} catch (err) {
// since we have errors lets rollback the changes we made
await queryRunner.rollbackTransaction();
} finally {
// you need to release a queryRunner which was manually instantiated
await queryRunner.release();
}
}
Обратите внимание, что
connection
используется только для созданияQueryRunner
. Однако, чтобы протестировать этот класс, потребуется смокать весь объектConnection
(который содержит несколько методов). Поэтому мы рекомендуем использовать вспомогательный фабричный класс (например,QueryRunnerFactory
) и определить интерфейс с ограниченным набором методов, необходимых для поддержания транзакций. Такая техника позволяет довольно просто подделывать эти методы.
В качестве альтернативы можно использовать подход в стиле callback с методом transaction
объекта Connection
.
async createMany(users: User[]) {
await this.connection.transaction(async manager => {
await manager.save(users[0]);
await manager.save(users[1]);
});
}
Использование декораторов для управления транзакцией (@Transaction()
и @TransactionManager()
) не рекомендуется.
Подписчики
С помощью TypeORM subscribers вы можете прослушивать события определенных сущностей.
import {
Connection,
EntitySubscriberInterface,
EventSubscriber,
InsertEvent,
} from 'typeorm';
import { User } from './user.entity';
@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
constructor(connection: Connection) {
connection.subscribers.push(this);
}
listenTo() {
return User;
}
beforeInsert(event: InsertEvent<User>) {
console.log(`BEFORE USER INSERTED: `, event.entity);
}
}
Подписчики событий не могут быть request-scoped.
Теперь добавьте класс UserSubscriber
в массив providers
:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { UserSubscriber } from './user.subscriber';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService, UserSubscriber],
controllers: [UsersController],
})
export class UsersModule {}
Миграции
Миграции Migrations предоставляют возможность постепенного обновления схемы базы данных для синхронизации ее с моделью данных приложения с сохранением существующих данных в базе данных. Для создания, запуска и возврата миграций TypeORM предоставляет специальный CLI.
Классы миграции отделены от исходного кода приложения Nest. Их жизненный цикл поддерживается TypeORM CLI. Поэтому вы не сможете использовать инъекцию зависимостей и другие специфические возможности Nest с помощью миграций.
Несколько баз данных
Некоторые проекты требуют подключения к нескольким базам данных. Этого также можно добиться с помощью данного модуля. Чтобы работать с несколькими соединениями, сначала их надо создать. В этом случае именование соединений становится обязательным.
Предположим, у вас есть сущность Album
, хранящаяся в собственной базе данных.
const defaultOptions = {
type: 'postgres',
port: 5432,
username: 'user',
password: 'password',
database: 'db',
synchronize: true,
};
@Module({
imports: [
TypeOrmModule.forRoot({
...defaultOptions,
host: 'user_db_host',
entities: [User],
}),
TypeOrmModule.forRoot({
...defaultOptions,
name: 'albumsConnection',
host: 'album_db_host',
entities: [Album],
}),
],
})
export class AppModule {}
Если вы не зададите
name
для соединения, его имя будет установлено вdefault
. Обратите внимание, что у вас не должно быть несколько соединений без имени или с одинаковым именем, иначе они будут переопределены.
На данный момент у вас есть сущности User
и Album
, зарегистрированные со своим собственным соединением. При такой настройке вам нужно указать методу TypeOrmModule.forFeature()
и декоратору @InjectRepository()
, какое соединение должно использоваться. Если вы не передадите никакого имени соединения, будет использоваться соединение default
.
@Module({
imports: [
TypeOrmModule.forFeature([User]),
TypeOrmModule.forFeature([Album], 'albumsConnection'),
],
})
export class AppModule {}
Вы также можете инжектировать Connection
или EntityManager
для данного соединения:
@Injectable()
export class AlbumsService {
constructor(
@InjectConnection('albumsConnection')
private connection: Connection,
@InjectEntityManager('albumsConnection')
private entityManager: EntityManager,
) {}
}
Также можно инжектировать любое Connection
к провайдерам:
@Module({
providers: [
{
provide: AlbumsService,
useFactory: (albumsConnection: Connection) => {
return new AlbumsService(albumsConnection);
},
inject: [getConnectionToken('albumsConnection')],
},
],
})
export class AlbumsModule {}
Тестирование
Когда речь идет о модульном тестировании приложения, мы обычно хотим избежать подключения к базе данных, чтобы сохранить независимость наших тестовых наборов и максимально ускорить процесс их выполнения. Но наши классы могут зависеть от репозиториев, которые извлекаются из экземпляра соединения. Как нам с этим справиться? Решение заключается в создании макетов репозиториев. Для этого мы создали custom providers. Каждый зарегистрированный репозиторий автоматически представляется токеном <EntityName>Repository
, где EntityName
- это имя класса вашей сущности.
Пакет @nestjs/typeorm
предоставляет функцию getRepositoryToken()
, которая возвращает подготовленный токен на основе заданной сущности.
@Module({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: mockRepository,
},
],
})
export class UsersModule {}
Теперь в качестве UsersRepository
будет использоваться замещающий mockRepository
. Всякий раз, когда какой-либо класс запрашивает UsersRepository
с помощью декоратора @InjectRepository()
, Nest будет использовать зарегистрированный объект mockRepository
.
Пользовательский репозиторий
TypeORM предоставляет возможность пользовательских репозиториев. Пользовательские репозитории позволяют расширить базовый класс репозитория и обогатить его несколькими специальными методами. Чтобы узнать больше об этой возможности, посетите эту страницу. Помните, что пользовательские хранилища находятся вне системы инъекции зависимостей NestJS, поэтому вы не можете инъектировать в них какие-либо значения.
Чтобы создать пользовательское хранилище, используйте декоратор @EntityRepository()
и расширьте класс Repository
.
@EntityRepository(Author)
export class AuthorRepository extends Repository<Author> {}
Оба класса
@EntityRepository()
иRepository
импортируются из пакетаtypeorm
.
После создания класса следующим шагом будет передача ответственности за инстанцирование в Nest. Для этого мы должны передать класс AuthorRepository
методу TypeOrm.forFeature()
.
@Module({
imports: [TypeOrmModule.forFeature([AuthorRepository])],
controller: [AuthorController],
providers: [AuthorService],
})
export class AuthorModule {}
После этого просто инжектируйте репозиторий с помощью следующей конструкции:
@Injectable()
export class AuthorService {
constructor(private authorRepository: AuthorRepository) {}
}
Асинхронная конфигурация
Вам может понадобиться передавать параметры модуля репозитория асинхронно, а не статически. В этом случае используйте метод forRootAsync()
, который предоставляет несколько способов работы с асинхронной конфигурацией.
Один из подходов заключается в использовании фабричной функции:
TypeOrmModule.forRootAsync({
useFactory: () => ({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
});
Наша фабрика ведет себя как любой другой asynchronous provider (например, она может быть async
и способна инжектировать зависимости через inject
).
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'mysql',
host: configService.get('HOST'),
port: +configService.get<number>('PORT'),
username: configService.get('USERNAME'),
password: configService.get('PASSWORD'),
database: configService.get('DATABASE'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
inject: [ConfigService],
});
В качестве альтернативы можно использовать синтаксис useClass
:
TypeOrmModule.forRootAsync({
useClass: TypeOrmConfigService,
});
Приведенная выше конструкция инстанцирует TypeOrmConfigService
внутри TypeOrmModule
и использует его для предоставления объекта опций путем вызова createTypeOrmOptions()
. Обратите внимание, что это означает, что TypeOrmConfigService
должен реализовать интерфейс TypeOrmOptionsFactory
, как показано ниже:
@Injectable()
class TypeOrmConfigService implements TypeOrmOptionsFactory {
createTypeOrmOptions(): TypeOrmModuleOptions {
return {
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
};
}
}
Чтобы предотвратить создание TypeOrmConfigService
внутри TypeOrmModule
и использовать провайдер, импортированный из другого модуля, вы можете использовать синтаксис useExisting
.
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useExisting: ConfigService,
});
Эта конструкция работает так же, как useClass
, с одним критическим отличием - TypeOrmModule
будет искать импортированные модули для повторного использования существующего ConfigService
вместо инстанцирования нового.
Убедитесь, что свойство
name
определено на том же уровне, что и свойствоuseFactory
,useClass
илиuseValue
. Это позволит Nest правильно зарегистрировать соединение под соответствующим названием инъекции.
Пользовательская фабрика соединений
В сочетании с асинхронной конфигурацией с помощью useFactory
, useClass
или useExisting
, вы можете опционально указать функцию connectionFactory
, которая позволит вам предоставить собственное соединение TypeORM вместо того, чтобы позволить TypeOrmModule
создать соединение.
connectionFactory
получает TypeORM ConnectionOptions
, настроенные во время асинхронной конфигурации с помощью useFactory
, useClass
или useExisting
, и возвращает Promise
, резолвящий (resolved) TypeORM Connection
.
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
// Используйте useFactory, useClass, или useExisting
// для настройки ConnectionOptions.
useFactory: (configService: ConfigService) => ({
type: 'mysql',
host: configService.get('HOST'),
port: +configService.get<number>('PORT'),
username: configService.get('USERNAME'),
password: configService.get('PASSWORD'),
database: configService.get('DATABASE'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
// connectionFactory получает настроенные параметры подключения (ConnectionOptions)
// и возвращает Promise<Connection>.
connectionFactory: async (options) => {
const connection = await createConnection(options);
return connection;
},
});
Функция
createConnection
импортируется из пакетаtypeorm
.
Интеграция с Sequelize
Альтернативой использованию TypeORM является использование ORM Sequelize с пакетом @nestjs/sequelize
. Кроме того, мы используем пакет sequelize-typescript, который предоставляет набор дополнительных декораторов для декларативного определения сущностей.
Чтобы начать его использовать, сначала установите необходимые зависимости. В этой главе мы продемонстрируем использование популярной реляционной СУБД MySQL, но Sequelize обеспечивает поддержку многих реляционных баз данных, таких как PostgreSQL, MySQL, Microsoft SQL Server, SQLite и MariaDB. Процедура, которую мы рассмотрим в этой главе, будет одинаковой для любой базы данных, поддерживаемой Sequelize. Вам просто нужно будет установить соответствующие клиентские библиотеки API для выбранной вами базы данных.
$ npm install --save @nestjs/sequelize sequelize sequelize-typescript mysql2
$ npm install --save-dev @types/sequelize
После завершения процесса установки мы можем импортировать SequelizeModule
в корневой AppModule
.
app.module.ts
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
@Module({
imports: [
SequelizeModule.forRoot({
dialect: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
models: [],
}),
],
})
export class AppModule {}
Метод forRoot()
поддерживает все свойства конфигурации, предоставляемые конструктором Sequelize (подробнее (opens new window)). Кроме того, существует несколько дополнительных свойств конфигурации, описанных ниже.
retryAttempts | Количество попыток подключения к базе данных (по умолчанию: 10 ) |
retryDelay | Задержка между повторными попытками подключения (мс) (по умолчанию: 3000 )) |
autoLoadModels | Если true , модели будут загружаться автоматически (по умолчанию: false ) |
keepConnectionAlive | Если true , соединение не будет закрываться при завершении работы приложения (по умолчанию: false ) |
synchronize | Если true , автоматически загруженные модели будут синхронизироваться (по умолчанию: true ) |
Как только это будет сделано, объект Sequelize
будет доступен для внедрения во всем проекте (без необходимости импортировать какие-либо модули), например:
app.service.ts
import { Injectable } from '@nestjs/common';
import { Sequelize } from 'sequelize-typescript';
@Injectable()
export class AppService {
constructor(private sequelize: Sequelize) {}
}
Модели
Sequelize реализует паттерн Active Record. В этом шаблоне вы используете классы моделей непосредственно для взаимодействия с базой данных. Чтобы продолжить пример, нам нужна хотя бы одна модель. Давайте определим модель User
.
user.model.ts
import { Column, Model, Table } from 'sequelize-typescript';
@Table
export class User extends Model {
@Column
firstName: string;
@Column
lastName: string;
@Column({ defaultValue: true })
isActive: boolean;
}
Файл модели User
находится в директории users
. Эта директория содержит все файлы, связанные с модулем UsersModule
. Вы можете решить, где хранить файлы моделей, однако мы рекомендуем создавать их рядом с их доменом, в соответствующем каталоге модуля.
Чтобы начать использовать модель User
, нам нужно сообщить о ней Sequelize, вставив ее в массив models
в опциях метода модуля forRoot()
:
app.module.ts
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './users/user.model';
@Module({
imports: [
SequelizeModule.forRoot({
dialect: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
models: [User],
}),
],
})
export class AppModule {}
Далее рассмотрим UsersModule
:
users.module.ts
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './user.model';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
imports: [SequelizeModule.forFeature([User])],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
Этот модуль использует метод forFeature()
для определения того, какие модели зарегистрированы в текущей области видимости. Имея это, мы можем внедрить UserModel
в UsersService
, используя декоратор @InjectModel()
:
users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { User } from './user.model';
@Injectable()
export class UsersService {
constructor(
@InjectModel(User)
private userModel: typeof User,
) {}
async findAll(): Promise<User[]> {
return this.userModel.findAll();
}
findOne(id: string): Promise<User> {
return this.userModel.findOne({
where: {
id,
},
});
}
async remove(id: string): Promise<void> {
const user = await this.findOne(id);
await user.destroy();
}
}
Не забудьте импортировать
UsersModule
в корневойAppModule
.
Если вы хотите использовать репозиторий вне модуля, который импортирует SequelizeModule.forFeature
, вам нужно будет реэкспортировать сгенерированные им провайдеры.
Вы можете сделать это, экспортировав весь модуль, как показано ниже:
users.module.ts
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './user.entity';
@Module({
imports: [SequelizeModule.forFeature([User])],
exports: [SequelizeModule]
})
export class UsersModule {}
Теперь, если мы импортируем UsersModule
в UserHttpModule
, мы можем использовать @InjectModel(User)
в провайдерах последнего модуля.
users-http.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users.module';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
@Module({
imports: [UsersModule],
providers: [UsersService],
controllers: [UsersController]
})
export class UserHttpModule {}
Отношения
Отношения - это ассоциации, установленные между двумя или более таблицами. Отношения основаны на общих полях каждой таблицы, часто с использованием первичных и внешних ключей.
Существует три типа отношений:
One-to-one | Каждая строка в первичной таблице имеет одну и только одну связанную строку во внешней таблице. |
One-to-many / Many-to-one | Каждая строка в первичной таблице имеет одну или несколько связанных строк во внешней таблице. |
Many-to-many | Каждая строка в первичной таблице имеет много связанных строк во внешней таблице, а каждая запись во внешней таблице имеет много связанных строк в первичной таблице. |
Чтобы определить отношения в сущностях, используйте соответствующие декораторы. Например, чтобы определить, что каждый User
может иметь несколько фотографий, используйте декоратор @HasMany()
.
user.entity.ts
import { Column, Model, Table, HasMany } from 'sequelize-typescript';
import { Photo } from '../photos/photo.model';
@Table
export class User extends Model {
@Column
firstName: string;
@Column
lastName: string;
@Column({ defaultValue: true })
isActive: boolean;
@HasMany(() => Photo)
photos: Photo[];
}
Автоматическая загрузка моделей
Ручное добавление моделей в массив models
, опций подключения, может быть утомительным. Кроме того, обращение к моделям из корневого модуля нарушает границы домена приложения и приводит к утечке деталей реализации в другие части приложения. Чтобы решить эту проблему, автоматически загружайте модели, установив свойства autoLoadModels
и synchronize
объекта конфигурации (передаваемого в метод forRoot()
) в значение true
, как показано ниже:
app.module.ts
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
@Module({
imports: [
SequelizeModule.forRoot({
...
autoLoadModels: true,
synchronize: true,
}),
],
})
export class AppModule {}
При указании этой опции каждая модель, зарегистрированная через метод forFeature()
, будет автоматически добавлена в массив models
объекта конфигурации.
Обратите внимание, что модели, которые не зарегистрированы через метод
forFeature()
, а только ссылаются из модели (через ассоциацию), не будут включены.
Транзакции
Транзакция базы данных символизирует единицу работы, выполняемую в системе управления базами данных над базой данных и обрабатываемую последовательным и надежным образом независимо от других транзакций. Транзакция обычно представляет собой любое изменение в базе данных.
Существует множество различных стратегий обработки Sequelize transactions. Ниже приведен пример реализации управляемой транзакции (автоколлбэк).
Во-первых, нам нужно внедрить объект Sequelize
в класс обычным способом:
@Injectable()
export class UsersService {
constructor(private sequelize: Sequelize) {}
}
Sequelize
импортирован из пакетаsequelize-typescript
.
Теперь мы можем использовать этот объект для создания транзакции.
async createMany() {
try {
await this.sequelize.transaction(async t => {
const transactionHost = { transaction: t };
await this.userModel.create(
{ firstName: 'Abraham', lastName: 'Lincoln' },
transactionHost,
);
await this.userModel.create(
{ firstName: 'John', lastName: 'Boothe' },
transactionHost,
);
});
} catch (err) {
// Транзакция была отклонена
// err - это то, что отклонила цепочка промисов, возвращенная в обратный вызов транзакции
}
}
Обратите внимание, что экземпляр
Sequelize
используется только для начала транзакции. Однако, чтобы протестировать этот класс, потребуется смокать весь объектSequelize
(который раскрывает несколько методов). Поэтому мы рекомендуем использовать вспомогательный фабричный класс (например,TransactionRunner
) и определить интерфейс с ограниченным набором методов, необходимых для поддержания транзакций. Такая техника позволяет довольно просто мокать эти методы.
Миграции
Миграции Migrations обеспечивают способ постепенного обновления схемы базы данных для синхронизации ее с моделью данных приложения, сохраняя при этом существующие данные в базе данных. Для создания, запуска и возврата миграций Sequelize предоставляет специальный CLI.
Классы миграции отделены от исходного кода приложения Nest. Их жизненный цикл поддерживается Sequelize CLI. Поэтому вы не сможете использовать инъекцию зависимостей и другие специфические для Nest возможности с помощью миграций.
Несколько баз данных
Некоторые проекты требуют подключения к нескольким базам данных. Этого также можно добиться с помощью данного модуля. Чтобы работать с несколькими соединениями, сначала создайте соединения. В этом случае именование соединений становится обязательным.
Предположим, у вас есть сущность Album
, хранящаяся в собственной базе данных.
const defaultOptions = {
dialect: 'postgres',
port: 5432,
username: 'user',
password: 'password',
database: 'db',
synchronize: true,
};
@Module({
imports: [
SequelizeModule.forRoot({
...defaultOptions,
host: 'user_db_host',
models: [User],
}),
SequelizeModule.forRoot({
...defaultOptions,
name: 'albumsConnection',
host: 'album_db_host',
models: [Album],
}),
],
})
export class AppModule {}
Если вы не зададите
name
для соединения, его имя будет установлено вdefault
. Обратите внимание, что у вас не должно быть несколько соединений без имени или с одинаковым именем, иначе они будут переопределены.
На данный момент у вас есть модели User
и Album
, зарегистрированные с собственным соединением. При такой настройке вам нужно указать методу SequelizeModule.forFeature()
и декоратору @InjectModel()
, какое соединение следует использовать. Если вы не передадите никакого имени соединения, будет использоваться соединение default
.
@Module({
imports: [
SequelizeModule.forFeature([User]),
SequelizeModule.forFeature([Album], 'albumsConnection'),
],
})
export class AppModule {}
Вы также можете инжектировать экземпляр Sequelize
для данного соединения:
@Injectable()
export class AlbumsService {
constructor(
@InjectConnection('albumsConnection')
private sequelize: Sequelize,
) {}
}
Также можно инжектировать любой экземпляр Sequelize
в провайдеры:
@Module({
providers: [
{
provide: AlbumsService,
useFactory: (albumsSequelize: Sequelize) => {
return new AlbumsService(albumsSequelize);
},
inject: [getConnectionToken('albumsConnection')],
},
],
})
export class AlbumsModule {}
Тестирование
Когда речь идет о модульном тестировании приложения, мы обычно хотим избежать подключения к базе данных, чтобы сохранить независимость наших тестовых наборов и максимально ускорить процесс их выполнения. Но наши классы могут зависеть от моделей, которые извлекаются из экземпляра соединения. Как нам с этим справиться? Решение заключается в создании имитационных моделей. Для этого мы создаем пользовательские провайдеры. Каждая зарегистрированная модель автоматически представляется токеном <ModelName>Model
, где ModelName
- это имя класса вашей модели.
Пакет @nestjs/sequelize
раскрывает функцию getModelToken()
, которая возвращает подготовленный токен на основе заданной модели.
@Module({
providers: [
UsersService,
{
provide: getModelToken(User),
useValue: mockModel,
},
],
})
export class UsersModule {}
Теперь в качестве UserModel
будет использоваться замещающая mockModel
. Всякий раз, когда какой-либо класс будет запрашивать UserModel
с помощью декоратора @InjectModel()
, Nest будет использовать зарегистрированный объект mockModel
.
Асинхронная конфигурация
Вы можете захотеть передавать параметры SequelizeModule
асинхронно, а не статически. В этом случае используйте метод forRootAsync()
, который предоставляет несколько способов работы с асинхронной конфигурацией.
Один из подходов заключается в использовании фабричной функции:
SequelizeModule.forRootAsync({
useFactory: () => ({
dialect: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
models: [],
}),
});
Наша фабрика ведет себя как любой другой asynchronous provider (например, она может быть async
и способна инжектировать зависимости через inject
).
SequelizeModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
dialect: 'mysql',
host: configService.get('HOST'),
port: +configService.get('PORT'),
username: configService.get('USERNAME'),
password: configService.get('PASSWORD'),
database: configService.get('DATABASE'),
models: [],
}),
inject: [ConfigService],
});
В качестве альтернативы можно использовать синтаксис useClass
:
SequelizeModule.forRootAsync({
useClass: SequelizeConfigService,
});
Приведенная выше конструкция инстанцирует SequelizeConfigService
внутри SequelizeModule
и использует его для предоставления объекта опций путем вызова createSequelizeOptions()
. Обратите внимание, что это означает, что SequelizeConfigService
должен реализовать интерфейс SequelizeOptionsFactory
, как показано ниже:
@Injectable()
class SequelizeConfigService implements SequelizeOptionsFactory {
createSequelizeOptions(): SequelizeModuleOptions {
return {
dialect: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
models: [],
};
}
}
Чтобы предотвратить создание SequelizeConfigService
внутри SequelizeModule
и использовать провайдер, импортированный из другого модуля, вы можете использовать синтаксис useExisting
.
SequelizeModule.forRootAsync({
imports: [ConfigModule],
useExisting: ConfigService,
});
Эта конструкция работает так же, как useClass
, с одним критическим отличием - SequelizeModule
будет искать импортированные модули для повторного использования существующего ConfigService
вместо инстанцирования нового.
0 комментариев