ORM框架以及数据库关系和操作
提到后端肯定离不开数据库和数据的相关操作,最原始的办法我们就是直接写SQL语句进行查询,但是这样写起来比较麻烦,而且容易出错,所以我们需要一个框架来帮助我们操作数据库,这个框架就是ORM框架。
ORM(Object-Relational Mapping,对象关系映射)框架是一种编程技术,用于在面向对象的编程语言和关系型数据库之间建立映射关系,从而简化数据库操作。比如 java 中的 MyBatis
,Python 中的 SQLAlchemy
,Node.js中的 TypeORM
等。
Node.js有很多可用的ORM框架,有 TypeORM
、Sequelize
、Prisma
等,其中 TypeORM
的用户也是很多的,后面我们将以这个ORM框架来开展。
数据库
在具体讲解ORM框架之前,我们先来说一下数据库,当前流行的数据库很多,可分为关系型数据库和非关系型数据库。
维度 | 关系型数据库 | 非关系型数据库 |
---|---|---|
数据模型 | 基于 "表 - 行 - 列" 的二维结构,数据之间通过外键建立关联(如 "用户表" 与 "订单表" 通过用户 ID 关联)。 | 数据模型多样,常见类型: 1.文档型(如 MongoDB,存储 JSON-like 文档) 2.键值型(如 Redis,key-value 键值对 3. 图型(如 Neo4j,节点 - 边关系) 4. 列族型(如 HBase,按列族存储)。 |
Schema(结构) | 严格固定,表的字段、类型、长度需预先定义,修改结构(如加字段)需执行 DDL 语句,可能影响现有数据。 | 灵活可变,无需预先定义结构,同一份数据可以有不同字段(如 MongoDB 的文档可动态增删字段),适合数据结构频繁变化的场景。 |
事务与一致性 | 强支持 ACID 特性(原子性、一致性、隔离性、持久性),适合需要严格数据一致性的场景(如金融交易)。 | 多数不支持完整 ACID,或仅支持最终一致性(如 MongoDB 4.0 后支持多文档事务,但功能有限),更注重可用性和分区容错性。 |
查询方式 | 使用标准化 SQL 语言,支持复杂查询(如多表关联、分组、聚合等),查询能力强大 | 无统一查询语言,依赖各自 API(如 MongoDB 用查询文档,Redis 用命令行),复杂关联查询能力较弱(需业务层实现) |
扩展性 | 垂直扩展为主(升级服务器硬件),水平扩展(分布式)较复杂(需分库分表中间件支持) | 天生支持水平扩展(通过分片、复制),可通过增加节点轻松扩容,适合海量数据场景 |
适用数据类型 | 结构化数据(如用户信息、订单、财务数据等,格式固定且关系明确) | 非结构化 / 半结构化数据(如日志、图片、JSON 数据)、高频读写的简单数据(如缓存、计数器) |
在考虑关系与非关系数据库时,需要根据具体业务需求来选择,比如从数据结构是否固定、数据量和并发量、查询复杂度、一致性要求等方面来考虑。
我们后面关系型数据库用我们最熟悉的MySQL,非关系型数据库用最常用的Redis,MongoDB等。
我们先来安装一下MySQL
安装的过程就在这里不细说了,不会安装或者有疑惑的人,可以看看别人的MySQL安装教程。
安装完成后,我们还需要有一个 MySQL 客户端,比如我们常用的 Navicat,这个之前是收费的,现在也有免费版了,但是只能使用一种数据库,如果需要使用多种数据库,就需要购买专业版了,当然我们可有别的办法免费使用专业版(doge),或者你也可以使用 HeidiSQL,这个是免费软件,且开源,且支持 MySQL
、 MariaDB
、 SQL Server
和 PostgreSQL
等数据库管理。
打开之后我们连接我们的MySQL数据库,然后创建一个数据库,我们这里创建一个 nest_demo
的数据库。



TypeORM
TypeORM是一个功能强大的ORM框架,它支持多种数据库,包括 MySQL、PostgreSQL、SQLite、Microsoft SQL Server 等,并且支持事务、关系、查询构建器等高级功能。
我们先来安装一下
bash
pnpm i @nestjs/typeorm typeorm mysql2
然后在我们的app.module.ts中引入TypeORM模块,并配置数据库连接信息
ts
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql', // 数据库类型
username: 'root',
password: '123456',
host: '127.0.0.1',
port: 3306,
database: 'nest_demo', // 数据库名称
autoLoadEntities: true, // 是否自动将实体类自动同步到数据库 (实体文件)
synchronize: true, // 定义数据库表结构与实体类字段同步
}),
UserModule
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
然后打开我们上节生成的user模块中的 user/entities/user.entity.ts 文件,在这个文件里我们可以定义我们的实体类,也就是数据库中的表结构,我们这里定义一个用户表:
ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('d_user')
export class User {
@PrimaryGeneratedColumn({
type: 'int',
comment: '用户id',
})
id: number;
@Column({
type: 'varchar',
comment: '用户昵称',
})
username: string;
@Column({
type: 'varchar',
comment: '密码',
select: false,
})
password: string;
@Column({
type: 'datetime',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
})
createTime: Date;
}
Entity
装饰器用于定义实体类,PrimaryGeneratedColumn
装饰器用于定义主键列,Column
装饰器用于定义普通列。 Entity
可以接收一个字符串参数,表示实体类对应的数据库表名,如果不传,默认会使用类名作为表名。
然后我们进入到我们user模块中的 user.module.ts 文件,引入我们刚刚定义的实体类,并配置到 TypeORM 模块中
ts
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
这时候我们打开我们的数据库,可以看到已经自动生成了我们定义的表结构

关系
在数据库中,关系是指两个或多个表之间的关联,比如一个商品上市需要注册,每一个商品都有对应的注册信息,反之一个注册信息对应一件商品,这就是一对一的关系;我们在网上买东西,一个用户账号可以有很多个订单,这些订单都是属于这个用户的,这就是一对多的关系;我们一个订单可以有很多个商品,一个商品也可以出现在多个订单中,这就是多对多的关系。
那这些关系在 typeorm 中是如何表示的呢?
我们这边来单独演示表的关系,所以我们单独在src下创建个文件夹entity,里面放所有我们演示的数据库实体类。
一对一
我们创建一个product.entity.ts和register.entity.ts,分别表示商品和注册信息,然后我们创建一个一对一的关系,一个商品只能有一个注册信息,一个注册信息也只能对应一个商品。
register.ts
ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm';
@Entity('t_register')
export class Register {
@PrimaryGeneratedColumn('uuid', {
comment: '注册信息id',
})
id: string;
@Column({
type: 'varchar',
length: 50,
comment: '注册信息名称',
})
register_name: string;
@Column({
type: 'varchar',
length: 255,
comment: '注册信息描述',
})
register_desc: string;
@Column({
type: 'varchar',
length: 255,
comment: '注册信息类别',
})
register_category: string;
}
product.entity.ts
ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn, ManyToMany } from 'typeorm';
import { Register } from './register.entity'
@Entity('t_product')
export class Product {
@PrimaryGeneratedColumn('uuid', {
comment: '商品id',
})
id: string;
@Column({
type: 'varchar',
length: 50,
comment: '商品名称',
})
product_name: string;
@Column({
type: 'varchar',
length: 255,
comment: '商品描述',
})
product_desc: string;
@Column({
type: 'varchar',
length: 255,
comment: '商品类别',
})
product_category: string;
@Column({
type: 'decimal',
precision: 10,
scale: 2,
comment: '商品价格',
})
product_price: number;
// 一对一关系,表示一个产品对应一个注册信息
@OneToOne(() => Register)
@JoinColumn({
name: 'register_id',
}) // 表示这个实体将存储关联的外键
register: Register;
}
我们通过 @OneToOne
装饰器来定义一对一关系,第一个参数是要关联的实体类,第二个参数是关联的外键字段。 @JoinColumn
装饰器表示这个实体将存储关联的外键。
我们在 app.module.ts 中引入我们刚刚定义的实体类,并配置到 TypeORM 模块中
在实际的nest项目中,我们一般会在模块中引入实体类,然后在模块中配置 TypeORM 模块,这样就可以在模块中使用实体类了。我们这里为了演示方便,直接在 app.module.ts 中引入实体类。
在nest项目中,我们推荐使用 @nestjs/typeorm
来配置 TypeORM 模块,这个模块提供了 TypeOrmModule.forRoot
和 TypeOrmModule.forFeature
两个方法,分别用于配置全局数据库连接和模块数据库连接。使用 @nestjs/typeorm
因为它与 NestJS 的依赖注入系统深度集成,更符合框架设计理念。
ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Product } from './entity/product.entity';
import { Register } from './entity/register.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql', // 数据库类型
username: 'root',
password: '123456',
host: '127.0.0.1',
port: 3306,
database: 'nest_demo', // 数据库名称
autoLoadEntities: true, // 是否自动将实体类自动同步到数据库
synchronize: true, // 定义数据库表结构与实体类字段同步
}),
TypeOrmModule.forFeature([Product, Register]),
UserModule
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
然后我们打开我们的数据库,可以看到已经自动生成了我们定义的表结构

并且在 t_product
表中多了一个 register_id
字段,这个字段就是关联的外键字段。
我们给 t_product
和 t_register
表插入一条数据,然后我们通过 register_id
字段来查询关联的 t_register
表中的数据。
在实际的nest项目中,我们一般会在 service
中使用实体类,在实体中我们通过依赖注入获取表的 Repository
,通过Repository
来操作数据库,然后在 controller
中调用 service
中的方法。这边我们直接在 app.service
中演示
ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Product } from './entity/product.entity'
import { Register } from './entity/register.entity'
import { Repository } from 'typeorm';
@Injectable()
export class AppService {
constructor(
@InjectRepository(Product)
private readonly productRepository: Repository<Product>,
@InjectRepository(Register)
private readonly registerRepository: Repository<Register>,
) {}
async inertData() {
const register = this.registerRepository.create({
register_name: 'XXX薯片',
register_desc: 'XXXX公司生产的薯片',
register_category: '食品',
})
await this.registerRepository.save(register)
const product = this.productRepository.create({
product_name: '薯片',
product_desc: 'XXX薯片,香脆可口',
product_category: '食品',
product_price: 12.5,
register: register
})
await this.productRepository.save(product)
return 'success'
}
}
我们在controller 中调用这两个方法,为了方便起见,我直接使用Get请求了,这样直接在浏览器访问就可以调用了
ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
inertData() {
return this.appService.inertData();
}
}
我们直接在浏览器访问 http://localhost:3000
之后触发 inertData
方法,我们打开数据库就可以看到我们插入的数据了。

那怎么查询呢?我们直接在 app.service
中添加一个查询方法
ts
getProducts() {
const products = this.productRepository.createQueryBuilder('product')
.leftJoinAndSelect('product.register', 'register')
.getMany()
return products;
}
我们在 app.controller
中添加一个查询方法
ts
@Get('get')
getData() {
return this.appService.getProducts();
}
我们直接在浏览器访问 http://localhost:3000/get
就可以看到我们查询的数据了。

一对多/多对一
我们创建一个 account.entity.ts 和 order.entity.ts,分别表示账户和订单,然后我们创建一个一对多的关系,一个订单可以有多个订单详情,一个订单详情只能对应一个订单。
ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, OneToMany } from 'typeorm';
import { Order } from './order.entity';
@Entity('t_account')
export class Account {
@PrimaryGeneratedColumn('uuid', {
comment: '账户id',
})
id: number;
@Column({
type: 'varchar',
length: 50,
comment: '账户名称',
})
account_name: string;
@Column({
type: 'varchar',
length: 255,
comment: '账户描述',
})
account_desc: string;
@CreateDateColumn({
type: 'datetime',
name: 'created_at',
comment: '开户时间',
})
createdAt: Date;
@OneToMany(() => Order, (order) => order.account)
orders: Order[];
}
ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn } from 'typeorm';
import { Account } from './account.entity';
// 订单状态
export enum OrderStatus {
PENDING = 'PENDING', // 待支付
PAID = 'PAID', // 已支付
CANCELED = 'CANCELED', // 已取消
}
@Entity('t_order')
export class Order {
@PrimaryGeneratedColumn('uuid', {
comment: '订单id',
})
id: number;
@Column({
type: 'varchar',
length: 50,
comment: '订单名称',
})
order_name: string;
@Column({
type: 'varchar',
length: 255,
comment: '订单描述',
})
order_desc: string;
@Column({
type: 'decimal',
precision: 12, // 总位数
scale: 2, // 小数位数
comment: '订单总金额(元)',
})
total_amount: number;
@Column({
type: 'enum',
default: OrderStatus.PENDING,
enum: OrderStatus,
comment: '订单状态',
})
status: OrderStatus;
/*
订单创建时间(自动生成,无需手动设置)
使用 CreateDateColumn 会自动在插入时设置为当前时间
*/
@CreateDateColumn({
type: 'datetime',
name: 'created_at',
comment: '订单创建时间',
})
createdAt: Date;
/*
自动在数据更新时刷新
*/
@Column({
type: 'datetime',
name: 'updated_at',
nullable: true,
comment: '订单更新时间',
})
updatedAt?: Date;
@ManyToOne(() => Account, (account) => account.orders)
@JoinColumn({
name: 'account_id',
})
account: Account;
}
我们通过 @OneToMany
和 @ManyToOne
装饰器来定义一对多/多对一关系,第一个参数是要关联的实体类,第二个参数是关联的外键字段。@ManyToOne
装饰器表示这个实体将存储关联的外键。
你可以在 @ManyToOne
/ @OneToMany
关系中省略 @JoinColumn
,除非你需要自定义关联列在数据库中的名称。@ManyToOne
可以单独使用,但 @OneToMany
必须搭配 @ManyToOne
使用。
我们在app.module.ts中将刚才这两个实体引入
ts
// ....
import { Product } from './entity/product.entity';
import { Register } from './entity/register.entity';
import { Order } from './entity/order.entity'
import { Account } from './entity/account.entity'
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
// .....
}),
TypeOrmModule.forFeature([Product, Register, Order, Account]),
UserModule
],
controllers: [AppController],
providers: [AppService],
})
我们来看一下数据库


并且在 t_order
表中多了一个 account_id
字段,这个字段就是关联的外键字段。
我们给 t_order
和 t_account
表插入一些数据
ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Product } from './entity/product.entity'
import { Register } from './entity/register.entity'
import { Account } from './entity/account.entity';
import { Order } from './entity/order.entity';
import { Repository } from 'typeorm';
import { OrderStatus } from './entity/order.entity';
@Injectable()
export class AppService {
constructor(
@InjectRepository(Product)
private readonly productRepository: Repository<Product>,
@InjectRepository(Register)
private readonly registerRepository: Repository<Register>,
@InjectRepository(Account)
private readonly accountRepository: Repository<Account>,
@InjectRepository(Order)
private readonly orderRepository: Repository<Order>,
) {}
/* async inertData() {
const register = this.registerRepository.create({
register_name: 'XXX薯片',
register_desc: 'XXXX公司生产的薯片',
register_category: '食品',
})
await this.registerRepository.save(register)
const product = this.productRepository.create({
product_name: '薯片',
product_desc: 'XXX薯片,香脆可口',
product_category: '食品',
product_price: 12.5,
register: register
})
await this.productRepository.save(product)
return 'success'
} */
async inertData() {
const account = this.accountRepository.create({
account_name: '张三',
account_desc: '张三的账户',
});
await this.accountRepository.save(account);
const order = this.orderRepository.create({
order_name: '订单1',
order_desc: '订单1的描述',
total_amount: 100,
status: OrderStatus.PENDING,
account,
});
const order2 = this.orderRepository.create({
order_name: '订单2',
order_desc: '订单2的描述',
total_amount: 200,
status: OrderStatus.PAID,
account,
});
await this.orderRepository.save([order, order2]);
return 'success'
}
}
我们在浏览器访问之后来看一下数据库中的数据

那么我们如何查询呢?
app.service.ts
ts
getALlAccountOrders() {
const orders = this.accountRepository.createQueryBuilder('account')
.leftJoinAndSelect('account.orders', 'orders')
.getMany();
return orders;
}
app.controller.ts
ts
@Get('order')
getDataById() {
return this.appService.getALlAccountOrders();
}
我们查询一下看看 http://localhost:3000/order

如果我想查询某个人账户下的所有订单该怎么写?
ts
getTargetAccountOrders(name: string) {
const orders = this.accountRepository.createQueryBuilder('account')
.leftJoinAndSelect('account.orders', 'orders')
.where('account.account_name = :name', { name })
.getMany();
return orders;
}
ts
@Get('order/:name')
getDataByName(@Param('name') name: string) {
return this.appService.getTargetAccountOrders(name);
}
我们就可以通过http://localhost:3000/order/XXX
查询了
多对多
一个订单可以有多个商品,一个商品也可以属于多个订单,这就是多对多的关系。
ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, ManyToMany, JoinColumn, CreateDateColumn, JoinTable } from 'typeorm';
import { Account } from './account.entity';
import { Product } from './product.entity';
// 订单状态
export enum OrderStatus {
PENDING = 'PENDING', // 待支付
PAID = 'PAID', // 已支付
CANCELED = 'CANCELED', // 已取消
}
@Entity('t_order')
export class Order {
@PrimaryGeneratedColumn('uuid', {
comment: '订单id',
})
id: number;
// ...
@ManyToOne(() => Account, (account) => account.orders)
@JoinColumn({
name: 'account_id',
})
account: Account;
@ManyToMany(() => Product)
@JoinTable({
name: 't_order_product', // 中间表名称
joinColumn: {
name: 'order_id',
referencedColumnName: 'id',
},
inverseJoinColumn: {
name: 'product_id',
referencedColumnName: 'id',
},
})
products: Product[];
}
我们通过 @ManyToMany
装饰器来定义多对多关系,@JoinTable
装饰器表示这个实体将存储关联的外键。
我们保存之后,在数据库中查看一下

可以看到,多对多关系在数据库中会生成一个中间表,这个中间表会存储两个实体之间的关联关系。
那我们怎么去保存商品和订单这种多对多关系的数据呢?
ts
async inertData() {
const product1 = this.productRepository.create({
product_name: '锅巴',
product_desc: 'XXX锅巴,香的窜天',
product_category: '食品',
product_price: 10.5,
})
const product2 = this.productRepository.create({
product_name: '辣条',
product_desc: 'XXX辣条,辣你两头',
product_category: '食品',
product_price: 6.5,
})
await this.productRepository.save([product1, product2])
// 先找出张三的账户,创建新的订单,并关联上
const account = await this.accountRepository.findOne({
where: { account_name: '张三' }
})
if(account) {
const order = this.orderRepository.create({
order_name: '多对多的订单',
order_desc: '多对多的订单的描述',
total_amount: 100,
status: OrderStatus.PENDING,
products: [product1, product2],
account
})
await this.orderRepository.save(order)
return 'success'
} else {
return 'error'
}
}
我们通过 products: [product]
来保存多对多关系的数据。
同样我们通过浏览器来调用我们的接口然后插入数据,这时候我们看一下我们的数据库

可以看到,我们的中间表已经保存了订单和商品之间的关系,一个订单有两个商品。
这时候我们想查询某个订单下的所有商品该怎么写呢?
ts
// 获取指定订单下的所有产品
getProductsByOrder(id: string) {
const products = this.orderRepository.createQueryBuilder('order')
.leftJoinAndSelect('order.products', 'products')
.where('order.id = :id', { id })
.getMany()
return products;
}
ts
@Get('getProductsByOrder/:id')
getOrderById(@Param('id') id: string) {
return this.appService.getProductsByOrder(id);
}
我们查询一下看看 http://localhost:3000/getProductsByOrder/f8e287f2-03df-4f08-b089-166443c5d1ec

那我们可以反过来,想查询某个商品存在于哪些订单中,该怎么写呢?
在这之前,我们发现在做多对多关系的时候,只给product的实体中定义了@ManyToMany
,但是没有给Order实体定义@ManyToMany
,这是因为,在多对多关系中,只需要在一边定义即可,另一边会自动生成,但是这时候,我们想查询某个商品存在于哪些订单中,就需要在Order实体中定义@ManyToMany
了,但是这边就不能写@JoinTable
了,我们已经生成了中间表,所以这边就不需要再生成一次了。
product.entity.ts
ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn, ManyToMany } from 'typeorm';
import { Register } from './register.entity'
import { Order } from './order.entity'
@Entity('t_product')
export class Product {
@PrimaryGeneratedColumn('uuid', {
comment: '商品id',
})
id: string;
// ...
// 一对一关系,表示一个产品对应一个注册信息
@OneToOne(() => Register)
@JoinColumn({
name: 'register_id',
}) // 表示这个实体将存储关联的外键
register: Register;
@ManyToMany(() => Order, (order) => order.products)
orders: Order[];
}
我们还得把 order.entity.ts 中 @ManyToMany
这块修改一下
ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, ManyToMany, JoinColumn, CreateDateColumn, JoinTable } from 'typeorm';
import { Account } from './account.entity';
import { Product } from './product.entity';
// 订单状态
export enum OrderStatus {
PENDING = 'PENDING', // 待支付
PAID = 'PAID', // 已支付
CANCELED = 'CANCELED', // 已取消
}
@Entity('t_order')
export class Order {
@PrimaryGeneratedColumn('uuid', {
comment: '订单id',
})
id: number;
// ...
@ManyToMany(() => Product, (product) => product.orders)
@JoinTable({
name: 't_order_product', // 中间表名称
joinColumn: {
name: 'order_id',
referencedColumnName: 'id',
},
inverseJoinColumn: {
name: 'product_id',
referencedColumnName: 'id',
},
})
products: Product[];
}
这下我们可以写查询方法了
ts
// 获取包含该产品的所有订单
getOrdersByProduct(id: string) {
const orders = this.productRepository.createQueryBuilder('product')
.leftJoinAndSelect('product.orders', 'orders')
.where('product.id = :id', { id })
.getMany()
return orders;
}
ts
@Get('getOrdersByProduct/:id')
getOrdersByProduct(@Param('id') id: string) {
return this.appService.getOrdersByProduct(id);
}
我们查询一下看看 http://localhost:3000/getOrdersByProduct/0ba4c21f-c553-4c35-a077-2aa256993e9d

可以看到,我们获取到了该商品存在于哪些订单中。
ps. 最上面的一对一反着查询的时候跟这块是一样的,就不回头写了。
✨本专栏源码地址