Как настроить источник данных TypeORM в проекте NestJS
Привет! С тех пор как я начал работать с NestJS, я искал надежный способ управления базой данных с помощью TypeORM. Сегодня я расскажу о своем пути и шагах, которые я предпринял, чтобы всё это настроить.
Итак, прежде чем мы погрузимся в работу, давайте попробуем понять, что такое TypeORM и NestJS.
Что такое TypeORM?
TypeORM – это инструмент объектно-реляционного отображения (ORM), который упрощает работу с базами данных в приложениях на Node.js и TypeScript. Он поддерживает различные базы данных, такие как MySQL, PostgreSQL, SQLite и другие, позволяя разработчикам использовать концепции объектно-ориентированного программирования вместо того, чтобы работать с низкоуровневыми SQL-запросами.
TypeORM также предоставляет такие возможности, как миграция схем, построение запросов и управление связями между таблицами.
Что такое NestJS?
NestJS – это прогрессивный фреймворк Node.js, предназначенный для создания эффективных, надежных и масштабируемых приложений на стороне сервера. Он использует возможности TypeScript, позволяя разработчикам писать структурированный, удобный в обслуживании код.
NestJS использует модульную архитектуру, позволяющую организовывать код в модули, контроллеры, сервисы и провайдеры. Он обеспечивает встроенную поддержку таких функций, как инъекция зависимостей, промежуточное ПО и GraphQL, что делает его популярным выбором для создания современных веб-приложений и API.
Кроме того, NestJS легко интегрируется с другими библиотеками и фреймворками, включая TypeORM, что позволяет оптимизировать рабочие процессы разработки. По умолчанию он использует надежный фреймворк HTTP-сервера Express, а также может быть настроен на использование других фреймворков HTTP-сервера Node.js.
Хорошо, это уже много, верно? Что ж, прежде чем двигаться дальше, давайте попробуем разобрать фразу «NestJS - это прогрессивный фреймворк Node.js», которая просто означает, что NestJS использует новейшие возможности языка JavaScript и серверных фреймворков, тем самым обеспечивая разработчикам гибкость в написании кода на наиболее подходящем языке для их проектов.
Предварительные требования к реализации этого руководства
- Node.js. Минимум версия 18
- npm. Минимум версия 8
- Postgresql
- Базовая ознакомленность с Typescirpt и NestJS
- Pgadmin 4
Как настроить проект NestJS
Выполните следующие команды, чтобы установить ваш проект NestJS:
npm i -g @nestjs/cli # install nestj cli globally
nest new simple-crm # start a new nestjs project
После установки запустите сервер разработки:
npm run start:dev # start the app in watch mode
Теперь давайте протестируем наш проект, чтобы убедиться, что nest-cli
правильно настроил весь кодовый код, отправив запрос get
на корневой URL.
Отлично! Наш проект запущен и работает.
Как настроить источник данных TypeORM для обеспечения постоянства данных
npm install --save @nestjs/typeorm typeorm # nestjs typeorm drivers
npm install --save pg # typeorm postgressql driver
Давайте создадим базу данных для проекта из интерфейса Pgadmin 4.
Откройте интерфейс Pgadmin 4 и щелкните правой кнопкой мыши на вкладке Databases, чтобы создать новую базу данных, как показано ниже.
Убедитесь, что база данных успешно создана.
Отлично, пришло время добавить базу данных в наше приложение NestJS с помощью TypeORM.
Создайте новую папку datasource
в папке src/
вашего приложения, как показано ниже.
Создайте новый файл typeorm.module.ts
в папке datasource
и добавьте в него следующий код:
import { DataSource } from 'typeorm';
import { Global, Module } from '@nestjs/common';
@Global() // makes the module available globally for other modules once imported in the app modules
@Module({
imports: [],
providers: [
{
provide: DataSource, // add the datasource as a provider
inject: [],
useFactory: async () => {
// using the factory function to create the datasource instance
try {
const dataSource = new DataSource({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'ayo',
password: 'haywon',
database: 'simple-crm_db',
synchronize: true,
entities: [`${__dirname}/../**/**.entity{.ts,.js}`], // this will automatically load all entity file in the src folder
});
await dataSource.initialize(); // initialize the data source
console.log('Database connected successfully');
return dataSource;
} catch (error) {
console.log('Error connecting to database');
throw error;
}
},
},
],
exports: [DataSource],
})
export class TypeOrmModule {}
Добавьте модуль TypeORM в массив импортов модулей App
, как показано ниже:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from './datasource/typeorm.module';
@Module({
imports: [TypeOrmModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Затем сохраните и подтвердите в консоли успешное подключение к базе данных.
Если вы видите, что база данных успешно подключена, то вы молодцы! В противном случае вернитесь к предыдущим шагам, чтобы проверить, правильно ли вы следовали настройкам.
Теперь мы можем продолжить использовать наш сервис datasource
с помощью TypeORM.
Давайте создадим модуль users
, контроллер, провайдер и структуру для взаимодействия с нашей только что подключенной базой данных.
nest g module users && nest g service users && nest g controller users
Приведенная выше команда сгенерирует модуль users
, сервис и контроллер и обновит app.module.ts
с модулем users
.
Добавьте следующий код в файл users.entity.ts
и перезапустите сервер разработки, чтобы создать таблицу пользователей в базе данных.
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('user')
export class UserEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
username: string;
@Column()
password: string;
}
Проверьте интерфейс Pgadmin 4 и убедитесь, что TypeORM автоматически загрузил UserEntity
и создал таблицу user
в вашей базе данных, как показано ниже.
Возможно, вам понадобится обновить базу данных, если вы не увидите её в первый раз.
Теперь давайте реализуем наш первый обработчик службы users
, добавьте следующий код в файл users.service.ts
:
import {
HttpException,
HttpStatus,
Injectable,
InternalServerErrorException,
Logger,
} from '@nestjs/common';
import { DataSource } from 'typeorm';
import { UserEntity } from './users.entity';
export interface CreateUser {
username: string;
password: string;
}
@Injectable()
export class UsersService {
private userRepository;
private logger = new Logger();
// inject the Datasource provider
constructor(private dataSource: DataSource) {
// get users table repository to interact with the database
this.userRepository = this.dataSource.getRepository(UserEntity);
}
// create handler to create new user and save to the database
async createUser(createUser: CreateUser): Promise<UserEntity> {
try {
const user = await this.userRepository.create(createUser);
return await this.userRepository.save(user);
} catch (err) {
if (err.code == 23505) {
this.logger.error(err.message, err.stack);
throw new HttpException('Username already exists', HttpStatus.CONFLICT);
}
this.logger.error(err.message, err.stack);
throw new InternalServerErrorException(
'Something went wrong, Try again!',
);
}
}
}
Мы добавили метод createUser
для обработки создания пользователя, когда POST-запрос отправляется с требуемым телом запроса в контроллер конечной точки, использующий метод службы createUser
.
В качестве аргумента функция принимает объект createUser
с типом интерфейса CreateUser
. Обычно это должен быть объект DTO (Data Transfer Object) для структуры и проверки типов данных, но поскольку это выходит за рамки данного руководства, мы используем интерфейс только для формы данных.
Мы вызвали метод create хранилища userRepository
и присвоили его возврат переменной user
для хранения только что созданного объекта user
. Затем мы вызвали метод save
, чтобы сохранить объект в базе данных.
Теперь давайте воспользуемся обработчиком сервиса createUser
в контроллере users
, который обрабатывает POST-запрос на создание нового пользователя.
import { Body, Controller, Post } from '@nestjs/common';
import { CreateUser, UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private userService: UsersService) {}
@Post('/create')
// handles the post request to /users/create endpoint to create new user
async signUp(@Body() user: CreateUser) {
return await this.userService.createUser(user);
}
}
Протестируйте только что созданную конечную точку, отправив POST-запрос на http://localhost:3000/users/create
с именем пользователя и паролем в качестве тела запроса.
Хорошо, давайте проверим базу данных, чтобы убедиться, что все в порядке, потому что мы уже получили код состояния ответа 201
, которого должно быть достаточно, чтобы понять, что наше приложение нормально взаимодействует с базой данных, используя источник данных TypeORM.
Расширение репозитория DataSource для пользовательских методов
Если вы хотите оптимизировать запросы к базе данных, внедрить новые операции манипулирования данными или интегрироваться со сторонними сервисами, расширение репозитория DataSource с помощью пользовательских методов может стать решающим фактором для беспрепятственного взаимодействия с базой данных.
Здесь мы рассмотрим преимущества пользовательских методов и предоставим пошаговое руководство по их внедрению в ваши приложения NestJS. Итак, давайте погрузимся в работу и раскроем весь потенциал репозитория DataSource!
Вот некоторые из основных преимуществ пользовательских методов репозитория:
Индивидуальная функциональность: Пользовательские методы позволяют разработчикам внедрять специфические функциональные возможности, которые недоступны в стандартном хранилище DataSource. Настраивая репозиторий DataSource с помощью пользовательских методов, разработчики могут решать уникальные задачи, выполнять операции манипулирования данными, агрегировать их или оптимизировать, что необходимо для их проекта.
Оптимизированная производительность: Пользовательские методы могут быть разработаны для оптимизации запросов к базе данных, получения данных и операций манипулирования данными, что приводит к повышению производительности и эффективности. Используя пользовательские методы, разработчики могут реализовать оптимизированные алгоритмы, механизмы кэширования или оптимизацию запросов с учетом конкретных потребностей и характеристик своих приложений.
Улучшенная переиспользуемость и ремонтопригодность кода: Пользовательские методы способствуют повторному использованию кода, инкапсулируя конкретную логику, алгоритмы или операции в компоненты многократного использования. Модулируя пользовательские методы, разработчики могут поддерживать более чистые, организованные и удобные кодовые базы, что облегчает управление, отладку и совершенствование репозитория DataSource в долгосрочной перспективе.
Ну вот и все, переходим к действию. Итак, у нас есть простое CRM-приложение, связанное с управлением пользователями. Давайте добавим пользовательский метод репозитория, который поможет нам фильтровать пользователей по имени пользователя.
Чтобы реализовать это, создадим модуль datasource
и службу datasource
. Мы создадим эти файлы в соответствии с принципами модульности архитектурного паттерна NestJS.
Создайте файлы в ранее созданной папке datasource
и добавьте следующий код:
// datasource.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from './typeorm.module';
import { DataSourceService } from './datasource.service';
@Module({
imports: [TypeOrmModule],
providers: [DataSourceService],
exports: [DataSourceService],
})
export class DataSourceModule {}
// datasource.service.ts
import { Injectable } from '@nestjs/common';
import { UserEntity } from 'src/users/users.entity';
import { DataSource } from 'typeorm';
export interface UsernameQuery {
username: string;
}
@Injectable()
export class DataSourceService {
constructor(private dataSource: DataSource) {}
// extend userRepository to add custom methods
userCustomRepository = this.dataSource.getRepository(UserEntity).extend({
async filterUser(usernameQuery: UsernameQuery): Promise<UserEntity[]> {
const { username } = usernameQuery;
console.log(username);
// initialize a query builder for the userrepository
const query = this.createQueryBuilder('user');
// filter user where username is like the passed username
query.where('(LOWER(user.username) LIKE LOWER(:username))', {
username: `%${username}%`,
});
return await query.getMany();
},
});
}
Из приведенного выше кода datasource.service.ts
мы расширили UserRepository
, вызвав метод getRepository
на сервисе dataSource
и передав UserEntity
в качестве аргумента, чтобы получить хранилище для конкретной таблицы.
Затем мы вызвали метод extend
на userRepository
, полученный в результате getRepository
, чтобы добавить наш пользовательский метод. В наш метод extend
мы передали объект, который будет содержать все наши пользовательские методы для пользовательского хранилища, которое мы определили как userCustomRepository
. Здесь мы просто добавили только один пользовательский метод в наше пользовательское хранилище, а именно filterUser
. Он запускает запрос фильтрации таблицы пользователей по указанному имени пользователя.
Поскольку наш DataSourseService
является инжектируемым, мы можем внедрить его в наш UserService
и использовать только что созданный метод filterUser
после добавления модуля DataSourceModule
в массив imports
модуля user
следующим образом.
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { DataSourceModule } from 'src/datasource/datasource.module';
@Module({
imports: [DataSourceModule], // add the DataSourceModule to the import array
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
Давайте используем метод filter
из CustomUserRepository
в нашем UserService
для фильтрации пользователей по любому имени пользователя, переданному в качестве аргумента запроса при отправке запроса.
// users.service.ts
import {
HttpException,
HttpStatus,
Injectable,
InternalServerErrorException,
Logger,
} from '@nestjs/common';
import { DataSource } from 'typeorm';
import { UserEntity } from './users.entity';
import {
DataSourceService,
UsernameQuery,
} from 'src/datasource/datasource.service';
export interface CreateUser {
username: string;
password: string;
}
@Injectable()
export class UsersService {
private userRepository;
private customUserRepository;
private logger = new Logger();
// inject the Datasource provider
constructor(
private dataSource: DataSource,
private dataSourceService: DataSourceService, // inject our datasource service
) {
// get users table repository to interact with the database
this.userRepository = this.dataSource.getRepository(UserEntity);
// assigning the dataSourceService userCustomRepository to the class customUserRepository
this.customUserRepository = this.dataSourceService.userCustomRepository;
}
// create handler to create new user and save to the database
async createUser(createUser: CreateUser): Promise<UserEntity> {
try {
const user = await this.userRepository.create(createUser);
return await this.userRepository.save(user);
} catch (err) {
if (err.code == 23505) {
this.logger.error(err.message, err.stack);
throw new HttpException('Username already exists', HttpStatus.CONFLICT);
}
this.logger.error(err.message, err.stack);
throw new InternalServerErrorException(
'Something went wrong, Try again!',
);
}
}
// the userService filterByUsername handler
async filterByUsername(usernameQuery: UsernameQuery): Promise<UserEntity[]> {
try {
// calling the customUserRepository filterUser custom method
return await this.customUserRepository.filterUser(usernameQuery);
} catch (err) {
this.logger.error(err.message, err.stack);
throw new InternalServerErrorException(
'Something went wrong, Try again!',
);
}
}
}
Из приведенного выше кода мы внедрили наш пользовательский DataSourceService
, добавив следующее в конструктор класса private dataSourceService: DataSourceService,
.
Сервис filterByUsername
обрабатывает запрос, который мы используем в нашем пользовательском методе filterUser
await
this.customUserRepository.filterUser(usernameQuery);
, который возвращает обещание.
Теперь давайте используем этот обработчик сервиса в нашем контроллере пользователя для фильтрации пользователей по их имени пользователя.
// users.controller.ts
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { CreateUser, UsersService } from './users.service';
import { UserEntity } from './users.entity';
import { UsernameQuery } from 'src/datasource/datasource.service';
@Controller('users')
export class UsersController {
constructor(private userService: UsersService) {}
@Post('/create')
// handles the post request to /users/create endpoint to create new user
async signUp(@Body() user: CreateUser): Promise<UserEntity> {
return await this.userService.createUser(user);
}
@Get('') // get request handler that returns the filtered results of the users table
async filterUser(
@Query() usernameQuery: UsernameQuery // extracts the username query param for the endpoint url,
): Promise<UserEntity[]> {
return await this.userService.filterByUsername(usernameQuery);
}
}
Протестируйте конечную точку фильтрации.
Здесь мы получили список, содержащий один объект user
с именем пользователя, похожим на имя пользователя, которое мы передали в качестве запроса.
Заключение
Вуаля! Вот и все, теперь вы готовы приступить к работе с NestJS, TypeORM и DataSource.
Спасибо за чтение!
Если вы нашли эту статью полезной, пожалуйста, поделитесь ею с друзьями и коллегами! Следите за новыми материалами, и давайте продолжать учиться и развиваться вместе.