درس 04 NestJS – مدل و الگوی مخزن

کد منبع
درس 04
- معرفی مختصر یکی از ORM های محبوب – Sequelize
- با الگوی ActiveRecord آشنا شوید
- نحوه ایجاد طرح واره پایگاه داده با Sequelize در NestJS
1. NestJS را برای کار با MySQL از طریق Sequelize پیکربندی کنید
ابتدا باید پایگاه داده را ایجاد کنید
نام پایگاه داده : nestjs_tutorial_2023
CREATE DATABASE `nestjs_tutorial_2023` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_bin';
- با استفاده از Sequelize به MYSQL متصل شوید
- با استفاده از TypeORM به PostgreSQL متصل شوید
- با استفاده از Mongoose به MongoDB متصل شوید
1.1. با استفاده از Sequelize به MYSQL متصل شوید
بسته ها را نصب کنید
npm install --save @nestjs/sequelize sequelize sequelize-typescript mysql2
npm install --save-dev @types/sequelize
npm install --save-dev sequelize-cli
npx sequelize-cli init
در این قسمت می خواهم 3 مرحله اصلی کار با بانک های اطلاعاتی را به شما معرفی کنم:
- [x] ارتباط برقرار کنید
- [x] مدل را اعلام کنید
- [x] ساختار دایرکتوری مهاجرت پایگاه داده – مقداردهی اولیه، به روز رسانی طرح پایگاه داده
اتصال را تنظیم کنید
// src/app.module.ts
import { Module } from "@nestjs/common";
import { ServeStaticModule } from "@nestjs/serve-static";
import { join } from "path";
import { PetModule } from "./pet/pet.module";
import { SequelizeModule } from "@nestjs/sequelize";
@Module({
imports: [
// public folder
ServeStaticModule.forRoot({
rootPath: join(process.cwd(), "public"),
serveRoot: "/public",
}),
PetModule,
SequelizeModule.forRoot({
dialect: "mysql",
host: "localhost",
port: 3306,
username: "root",
password: "123456",
database: "nestjs_tutorial_2023",
models: [],
}),
],
providers: [],
})
export class AppModule {}
اظهارنامه مدل
مدل در ORM:
- این نگاشت یک موجودیت در پایگاه داده است، از طریق ORM فقط باید با متدهای Model کار کنیم، بقیه عبارات SQL/NoSQL هستند که توسط ORM مدیریت می شوند. مزیت این است که برنامه نویسی آسان تر و سازگارتر می شود. نکته منفی این است که گاهی اوقات اجرای برخی پرس و جوها کند و دشوار است. با این حال، در آن صورت ORM همچنان از آنها برای انجام پرس و جوهای SQL/NoSQL سنتی پشتیبانی می کند.
"use strict";
import { PetCategory } from "src/pet/models/pet-category.model";
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
/**
* Add altering commands here.
*
* Example:
* await queryInterface.createTable('users', { id: Sequelize.INTEGER });
*/
await queryInterface.createTable("pet_categories", {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true,
},
name: {
type: Sequelize.STRING(60),
allowNull: false,
},
createAt: {
type: Sequelize.DATE,
defaultValue: Sequelize.fn("NOW"),
},
updatedAt: {
type: Sequelize.DATE,
defaultValue: Sequelize.fn("NOW"),
},
});
},
async down(queryInterface, Sequelize) {
/**
* Add reverting commands here.
*
* Example:
* await queryInterface.dropTable('users');
*/
await queryInterface.dropTable("pet_categories");
},
};
پیکربندی را برای استفاده از مدل و استفاده از پایگاه داده uri تنظیم کنید
// src/app.module.ts
import { Module } from "@nestjs/common";
import { ServeStaticModule } from "@nestjs/serve-static";
import { join } from "path";
import { PetModule } from "./pet/pet.module";
import { SequelizeModule } from "@nestjs/sequelize";
import models from "./pet/models";
@Module({
imports: [
// public folder
ServeStaticModule.forRoot({
rootPath: join(process.cwd(), "public"),
serveRoot: "/public",
}),
PetModule,
SequelizeModule.forRoot({
uri: "mysql://root:123456@localhost/nestjs_tutorial_2023",
dialect: "mysql",
models: models,
}),
],
providers: [],
})
export class AppModule {}
ساختار دایرکتوری مهاجرت پایگاه داده – مقداردهی اولیه، به روز رسانی طرح پایگاه داده
توضیحی درباره اینکه چرا باید از sequelize-cli برای به روز رسانی پایگاه داده استفاده کنیم.
در طول توسعه پروژه، زمانی که یک جدول داده وجود دارد که نیاز به افزودن/حذف یا به روز رسانی ستون ها در جدول دارد. مستقیمترین راه عبارتهای SQL است، اگرچه هنوز میتوانیم مدیریت کنیم که کدام عبارت اجرا شده یا نه و با اعضای تیم یا افرادی که مسئول استقرار محصولات در محیط تولید هستند به اشتراک گذاشته شود.
در ORM های مدرن امروزی، برای یکسان سازی نحوه و استانداردسازی، معمولاً از cli این ORM ها مربوط به ایجاد/ویرایش پایگاه داده ها، جداول استفاده می کنیم.
و مهاجرت را از طریق خطوط فرمان انجام دهید. ساختار جداول داده از طریق کد پیاده سازی می شود – برای سازگاری با مدل مدیریت شده توسط ORM.
پس از اجرای دستور پیش فرض sequelize-cli، ساختار دایرکتوری پیش فرض زیر را خواهیم داشت.
npx sequelize-cli init
config
database.json
db
models
seeders
migrations
sequelizerc
// .sequelizerc
const path = require("path");
module.exports = {
config: path.resolve("config", "database.json"),
"models-path": path.resolve("db", "models"),
"seeders-path": path.resolve("db", "seeders"),
"migrations-path": path.resolve("db", "migrations"),
};
با این حال، برای مطابقت با ساختار فعلی پروژه، باید کمی به صورت زیر تنظیم کنیم
src
database
config
config.ts
migrations
*.ts
seeds
*.ts
sequelizerc
const path = require("path");
module.exports = {
config: path.resolve("./dist/database/config/config.js"),
"seeders-path": path.resolve("./dist/database/seeders"),
"migrations-path": path.resolve("./dist/database/migrations"),
};
در اینجا به جای اجرای مستقیم نسخه منبع، بیلد آنها را اجرا می کنیم
// config.ts
module.exports = {
production: {
url: "mysql://root:123456@localhost/nestjs_tutorial_2023",
dialect: "mysql",
},
};
// src/pet/models/pet-category.model
import { Column, DataType, Model, Table } from "sequelize-typescript";
@Table({
tableName: "pet_categories",
})
export class PetCategory extends Model {
@Column({
primaryKey: true,
type: DataType.UUID,
defaultValue: DataType.UUIDV4,
})
id?: string;
@Column({
type: DataType.STRING(60),
allowNull: false,
})
name: string;
}
// src\database\migrations\20230704043449-create-pet-category-table.ts
"use strict";
import { PetCategory } from "src/pet/models/pet-category.model";
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
/**
* Add altering commands here.
*
* Example:
* await queryInterface.createTable('users', { id: Sequelize.INTEGER });
*/
await queryInterface.createTable("pet_categories", {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true,
},
name: {
type: Sequelize.STRING(60),
allowNull: false,
},
});
},
async down(queryInterface, Sequelize) {
/**
* Add reverting commands here.
*
* Example:
* await queryInterface.dropTable('users');
*/
await queryInterface.dropTable("pet_categories");
},
};
توجه ویژه برای مدل در اینجا
از آنجایی که مدل PetCategory در مثال از Model از sequelise به ارث می برد، برخی از فیلدهای از پیش تعریف شده را به ارث می برد، اگرچه در کد مدل PetCategory ما آنها را مشاهده نمی کنیم.
بنابراین، هنگام ایجاد یک اسکریپت مهاجرت برای PetCategory، باید به این دو ستون پیش فرض توجه کنید.
export declare abstract class Model<TModelAttributes extends {} = any, TCreationAttributes extends {} = TModelAttributes> extends OriginModel<TModelAttributes, TCreationAttributes> {
id?: number | any;
createAt?: Date | any;
updatedAt?: Date | any;
deletedAt?: Date | any;
version?: number | any;
static isInitialized: boolean;
برخی از دستورات رایج
$ npx sequelize-cli --help
Sequelize CLI [Node: 16.19.1, CLI: 6.6.1, ORM: 6.32.1]
sequelize <command>
Commands:
sequelize db:migrate Run pending migrations
sequelize db:migrate:schema:timestamps:add Update migration table to have timestamps
sequelize db:migrate:status List the status of all migrations
sequelize db:migrate:undo Reverts a migration
sequelize db:migrate:undo:all Revert all migrations ran
sequelize db:seed Run specified seeder
sequelize db:seed:undo Deletes data from the database
sequelize db:seed:all Run every seeder
sequelize db:seed:undo:all Deletes data from the database
sequelize db:create Create database specified by configuration
sequelize db:drop Drop database specified by configuration
sequelize init Initializes project
sequelize init:config Initializes configuration
sequelize init:migrations Initializes migrations
sequelize init:models Initialize models
sequelize init:seeders Initializes seeders
sequelize migration:generate Generates a new migration file
sequelize migration:create Generates a new migration file
sequelize model:generate Generates a model and its migration
sequelize model:create Generates a model and its migration
sequelize seed:generate Generates a new seed file
sequelize seed:create Generates a new seed file
من می خواهم یک دستور مثال برای ایجاد یک فایل مهاجرت، سپس ساخت و اجرا
# create migration
npx sequelize-cli migration:create --name create-pet-category-table --migrations-path ./src/database/migrations
# build
npm run build
# run migration
npx sequelize-cli db:migrate --env production
توجه داشته باشید که اگر env مشخص نشده باشد، پیش فرض توسعه است. همانطور که در فایل کانفیگ بالا ما فقط یک محیط را به عنوان تولید تنظیم کردیم. و تنظیمات با متغیرهای محیطی جایگزین خواهند شد.
پس از اجرای migrate، اکنون دیتابیس را که خواهیم دید بررسی کنید
در مرحله بعد، ما شروع به آزمایش برخی از روش های اصلی مدل می کنیم: افزودن، به روز رسانی، حذف، جستجو
یک نکته کوچک هنگام ادامه درس با مثال Pet Category فعلی، باید مدل PetCategory را کمی به روز کنیم، به جای ستون عنوان -> برای سازگاری با پایگاه داده در این زمان به نام ستون تغییر می کند.
PetCategory را اضافه کنید
import { PetCategory } from "src/pet/models/pet-category.model";
await PetCategory.create({ ...object });
@Controller("admin/pet-categories")
export class ManagePetCategoryController {
@Post("create")
@Render("pet/admin/manage-pet-category/create")
@FormDataRequest()
async create(@Body() createPetCategoryDto: CreatePetCategoryDto) {
const data = {
mode: "create",
};
// validation
const object = plainToInstance(CreatePetCategoryDto, createPetCategoryDto);
// ...
// set value and show success message
Reflect.set(data, "values", object);
// create PetCategory
const newPetCategory = await PetCategory.create({ ...object });
Reflect.set(
data,
"success",
`Pet Category : ${newPetCategory.id} - ${newPetCategory.name} has been created!`
);
// success
return { data };
}
}
پس از اجرای http://localhost:3000/admin/pet-categories/create نتایج زیر را دریافت می کنیم
علاوه بر این، Sequelize همچنین از شما برای پیکربندی پشتیبانی میکند تا بتوانید جزئیات عبارات sql تولید شده توسط ORM را مشاهده کنید. مقداری در تنظیمات مربوط به اتصال پایگاه داده در ماژول برنامه را تغییر دهید.
SequelizeModule.forRoot({
uri: 'mysql://root:123456@localhost/nestjs_tutorial_2023',
dialect: 'mysql',
models: models,
logging: console.log,
}),
جستجوی PetCategory – لیست
@Controller('admin/pet-categories')
export class ManagePetCategoryController {
@Get('')
@Render('pet/admin/manage-pet-category/list')
async getList() {
const petCategories = await PetCategory.findAll();
return {
petCategories,
};
}
<%- include('layouts/admin/header'); %>
<section class="col-12">
<div class="card">
<div class="card-body">
<h5 class="card-title">List Pet Categories</h5>
<div class="table-responsive">
<table class="table table-light table-striped">
<thead>
<tr>
<th scope="col" style="width: 360px">ID</th>
<th scope="col">Name</th>
</tr>
</thead>
<tbody>
<% petCategories.forEach(petCategory => { %>
<tr class="">
<td><%= petCategory.id %></td>
<td><%= petCategory.name %></td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
</section>
<%- include('layouts/admin/footer'); %>
// find all
const petCategories = await PetCategory.findAll();
// delete
await PetCategory.destroy({ where: { id } });
// create
const newPetCategory = await PetCategory.create({ ...object });
// find by primary key
const petCategory = await PetCategory.findByPk(id);
// update
await petCategory.update(object);
کد منبع کنترلرها و نماهای ManagePetCategory را به روز کنید
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Redirect,
Render,
} from '@nestjs/common';
import { CreatePetCategoryDto } from 'src/pet/dtos/pet-dto';
import { plainToInstance } from 'class-transformer';
import { validate, ValidationError } from 'class-validator';
import { FormDataRequest } from 'nestjs-form-data';
import { PetCategory } from 'src/pet/models/pet-category.model';
import { Response } from 'express';
const transformError = (error: ValidationError) => {
const { property, constraints } = error;
return {
property,
constraints,
};
};
@Controller('admin/pet-categories')
export class ManagePetCategoryController {
@Get('')
@Render('pet/admin/manage-pet-category/list')
async getList() {
const petCategories = await PetCategory.findAll();
return {
petCategories,
};
}
@Post('delete/:id')
@Redirect('/admin/pet-categories/')
async deleteOne(@Param() { id }: { id: string }) {
await PetCategory.destroy({ where: { id } });
}
@Get('create')
@Render('pet/admin/manage-pet-category/create')
view_create() {
// a form
return {
data: {
mode: 'create',
},
};
}
@Post('create')
@Render('pet/admin/manage-pet-category/create')
@FormDataRequest()
async create(@Body() createPetCategoryDto: CreatePetCategoryDto) {
const data = {
mode: 'create',
};
// validation
const object = plainToInstance(CreatePetCategoryDto, createPetCategoryDto);
const errors = await validate(object, {
stopAtFirstError: true,
});
if (errors.length > 0) {
Reflect.set(data, 'error', 'Please correct all fields!');
const responseError = {};
errors.map((error) => {
const rawError = transformError(error);
Reflect.set(
responseError,
rawError.property,
Object.values(rawError.constraints)[0],
);
});
Reflect.set(data, 'errors', responseError);
return { data };
}
// set value and show success message
Reflect.set(data, 'values', object);
// create PetCategory
const newPetCategory = await PetCategory.create({ ...object });
Reflect.set(
data,
'success',
`Pet Category : ${newPetCategory.id} - ${newPetCategory.name} has been created!`,
);
// success
return { data };
}
@Get(':id')
@Render('pet/admin/manage-pet-category/create')
async getDetail(@Param() { id }: { id: string }) {
const data = {
mode: 'edit',
};
const petCategory = await PetCategory.findByPk(id);
Reflect.set(data, 'values', petCategory);
return { data };
}
@Post(':id')
@Render('pet/admin/manage-pet-category/create')
@FormDataRequest()
async updateOne(
@Param() { id }: { id: string },
@Body() createPetCategoryDto: CreatePetCategoryDto,
) {
const data = {
mode: 'edit',
};
const petCategory = await PetCategory.findByPk(id);
// validation
const object = plainToInstance(CreatePetCategoryDto, createPetCategoryDto);
const errors = await validate(object, {
stopAtFirstError: true,
});
if (errors.length > 0) {
Reflect.set(data, 'error', 'Please correct all fields!');
const responseError = {};
errors.map((error) => {
const rawError = transformError(error);
Reflect.set(
responseError,
rawError.property,
Object.values(rawError.constraints)[0],
);
});
Reflect.set(data, 'errors', responseError);
return { data };
}
// set value and show success message
await petCategory.update(object);
Reflect.set(data, 'values', petCategory);
return { data };
}
}
بازدیدها – list.html
<%- include('layouts/admin/header'); %>
<section class="col-12">
<div class="card">
<div class="card-body">
<h5 class="card-title">List Pet Categories</h5>
<div class="pb-4">
<a
class="btn btn-primary"
href="/admin/pet-categories/create"
role="button"
>New Pet Category</a
>
</div>
<div class="table-responsive">
<table class="table table-light table-striped">
<thead>
<tr>
<th scope="col" style="width: 360px">ID</th>
<th scope="col">Name</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<% petCategories.forEach(petCategory => { %>
<tr class="">
<td><%= petCategory.id %></td>
<td><%= petCategory.name %></td>
<td>
<a
href="/admin/pet-categories/<%= petCategory.id %>"
title="Edit"
>Edit</a
>
<form
action="/admin/pet-categories/delete/<%= petCategory.id %>"
method="post"
>
<button type="submit">Delete</button>
</form>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
</div>
</section>
<%- include('layouts/admin/footer'); %>
نماها – ایجاد/ویرایش
<%- include('layouts/admin/header'); %>
<section class="col-6">
<form method="post" enctype="multipart/form-data">
<div class="card">
<div class="card-body">
<h5 class="card-title">
<% if (data.mode === 'create') { %> New Pet Category <% } %> <% if
(data.mode === 'edit') { %> Edit Pet Category <% } %>
</h5>
<!-- error -->
<% if (data.error){ %>
<div class="alert alert-danger" role="alert"><%= data.error %></div>
<% } %>
<!-- success -->
<% if (data.success){ %>
<div class="alert alert-success" role="alert"><%= data.success %></div>
<script type="text/javascript">
setTimeout(() => {
window.location.href = "/admin/pet-categories/";
}, 2000);
</script>
<% } %>
<div class="mb-3">
<label for="title" class="form-label">Name</label>
<div class="input-group has-validation">
<input
type="text"
class="form-control <%= data.errors && data.errors['name'] ? 'is-invalid': '' %>"
id="name"
name="name"
value="<%= data.values && data.values['name'] %>"
placeholder="Pet Category name"
/>
<% if (data.errors && data.errors['name']) { %>
<div id="validationServerUsernameFeedback" class="invalid-feedback">
<%= data.errors['name'] %>
</div>
<% } %>
</div>
</div>
</div>
<% if(!data.success) { %>
<div class="mb-3 col-12 text-center">
<button type="submit" class="btn btn-primary">Save</button>
</div>
<% } %>
</div>
</form>
</section>
<%- include('layouts/admin/footer'); %>
برای خواندن دوره های کامل در NestJS درس 04 – مدل و الگوی مخزن، راحت باشید