前言
在使用 Node 操作数据库时,有直接让你写 SQL 语句的框架,比如:mysql2
const mysql = require('mysql2')
const connection = mysql.createConnection({
// ...
})
connection.execute('INSERT INTO customers (name) VALUES (?)', ['光'], (err, results, fields) => {
console.log(err)
})
但是一般不会直接去执行 SQL 语句,而是会使用 ORM 框架。
ORM 是 Object Relational Mapping,对象关系映射。也就是说把关系型数据库的表映射成面向对象的 class,表的字段映射成对象的属性映射,表与表的关联映射成属性的关联。
简单来说,就是我们只需要去操作列表,然后 ORM 框架会根据列表去同步数据库。
// 就是这种感觉,不需要用户去写SQL操作数据库,只需要跟写JS代码一样即可
const list = [{ id: 1, name: 'pnm' }]
list.push({ id: 2, name: 'wnm' })
当然,ORM 只是一种框架,其内部还是需要 mysql2 这样的包做支持的。
TypeORM
TypeORM 就是一个 Javascript ORM 框架,采用 Typescript 编写。当然除了这个框架,还有一个名为 Prisma 也很火(学海无涯,别卷尼玛)
# 通过npx创建一个项目,--database 指定使用的数据库的种类
npx typeorm@latest init --name projectName --database mysql
# 当然你也可以通过全局命令运行
npm install typeorm -g
typeorm init --name projectName --database mysql
安装完之后,我们再在里面安装 mysql2 并在 src/data-source.ts 中去写一些配置。
// ./src/data-source.ts 文件
import 'reflect-metadata' // 熟悉的 reflect 元数据
import { DataSource } from 'typeorm'
import { User } from './entity/User' // 实例
export const AppDataSource = new DataSource({
type: 'mysql',
host: '地址',
port: 3306,
username: '账号',
password: '密码',
database: '数据库名',
synchronize: true, // 是否同步建表
logging: false,
entities: [User],
migrations: [],
subscribers: [],
connectorPackage: 'mysql2', // 声明我们需要连接的数据库的包名,需要安装这个
extra: {
authPlugin: 'sha256_password', // 验证用的加密方式
},
})
配置完之后我们可以运行项目,yarn start 之后会发现,我们数据库中会新增了一个 User 表。
这个 User 表是由我们定义的 entities 实体创建的,进入我们的 User 实体会看到如下的代码,和我们 Nest 类似,都是这种调调的。。
@Entity()
export class User {
// 主键
@PrimaryGeneratedColumn()
id: number
@Column()
firstName: string
// 可以在这里配置一些列设置,比如 varchar(512)
@Column({
type: 'varchar',
length: '512',
})
lastName: string
@Column()
age: number
}
那实体又是如何给表添加数据的?这则是由 index.ts 里的代码执行的。
// 启动之后会执行这里的代码,去连接数据库
// 连接数据库之后,配置中的 synchronize: true, entities: [User] 会让它去创建 User 表。
// 然后 new User 了一个实例,通过这个实例去将数据插入表中
AppDataSource.initialize()
.then(async () => {
// 创建 user 实例
const user = new User()
// 添加数据,非常 js 代码的感觉
user.firstName = 'Timber'
user.lastName = 'Saw'
user.age = 25
// 插入进数据库中
await AppDataSource.manager.save(user)
// 查询数据库中的 User表
const users = await AppDataSource.manager.find(User)
console.log('Loaded users: ', users)
})
.catch((error) => console.log(error))
这种 ORM 框架的方式在添加,修改数据时,都会在内部的 SQL 中开启事务,便于我们操作。
方法汇总:
save:新增或者修改 Entity,如果传入了 id 会先 select 再决定修改还新增
update:直接修改 Entity,不会先 select
insert:直接插入 Entity
delete:删除 Entity,通过 id
remove:删除 Entity,通过对象
find:查找多条记录,可以指定 where、order by 等条件
findBy:查找多条记录,第二个参数直接指定 where 条件,更简便一点
findAndCount:查找多条记录,并返回总数量
findByAndCount:根据条件查找多条记录,并返回总数量
findOne:查找单条记录,可以指定 where、order by 等条件
findOneBy:查找单条记录,第二个参数直接指定 where 条件,更简便一点
findOneOrFail:查找失败会抛 EntityNotFoundError 的异常
query:直接执行 sql 语句
createQueryBuilder:创建复杂 sql 语句,比如 join 多个 Entity 的查询
transaction:包裹一层事务的 sql
getRepository:拿到对单个 Entity 操作的类,方法同 EntityManager
当有一些复杂的查询时,比如关联多个表,还算需要用到 createQueryBuilder 创建 queryBuilder 来执行 sql 语句。
外键
在 TypeORM 中 Entity 实体里要如何定义外键关联?
export class User {
// 主键
@PrimaryGeneratedColumn()
id: number
@Column()
firstName: string
@Column()
lastName: string
// 定义外键
@OneToOne(() => Level)
@JoinColumn({
name: 'level_id',
})
level_id: number
@OneToOne(() => Level, {
onDelete: 'CASCADE',
})
@JoinColumn({
name: 'level_id_2',
})
level_id_2: Level
}
上面就是定义外键的方式,会发现我定了 2 种,一种为 level_id: number 一种为 level_id_2: Level,这 2 种都可以定义外键,但在操作的时候会有些问题。
比如:
const user = await AppDataSource.manager.findOne(User, {
where: {
id: 1,
},
})
// 上述这样只能查出 id, firstName, lastName
// 如果需要查出 level_id 和 level_id_2 则需要将其关联
const user = await AppDataSource.manager.findOne(User, {
where: {
id: 1,
},
// 关联数据
relations: {
level_id_2: true,
// 由于 level_id 类型为 number 会导致类型推到成 never ,所以这里加了 ts-ignore
// @ts-ignore
level_id: true,
},
})
当然实际上,最后 user 也会打印出 level_id 关联的信息,但是保险起见,还是 level_id_2: Level 格式更好。
主表里查询添加关联
上面是 从表 可以透过外键查到关联的信息,但如果是主表呢?主表没有维护外键,那该如何???
export class Level {
@PrimaryGeneratedColumn({
type: 'tinyint',
comment: '等级的主键id,等级总共不多,一个字节足够',
})
id: number
@Column({
type: 'varchar',
length: 30,
comment: '等级名称,30字内',
})
name: string
// 【注】我们会发现这里添加了第二个参数,该参数就是告诉他 user 的关联的外键是哪一个
// 【注】由于没有 @JoinColumn ,所以这里不会给主表添加外键的,这样做就符合我们的需求
@OneToOne(() => User, (user) => user.level_id_2)
user: User
}
// 之后我们就可以通过这个,查找到 user 的信息
const level = await AppDataSource.manager.findOne(Level, {
where: {
id: 1,
},
relations: {
user: true,
},
})
一对多
一对多的情况下, 比如 部门表 和 员工表,员工表属于多的那一方,我们需要给其添加上 ManyToOne 这个装饰器
// 员工表
@Entity()
export class Employee {
@PrimaryGeneratedColumn({
comment: '员工ID ',
})
id: number
@Column()
name: string
@ManyToOne(() => Department, (Department) => Department.id, {
onDelete: 'SET NULL', // 与部门关联,且当部门被删除时,员工的 department_id 字段也将设为 null
})
// 可以不需要 JoinColumn ,因为一对多的场景,只能在多的那一方设立外键
// 所以 typeORM 会自动给多的这一方创建外键,而一对一则需要用户自己设置。
// 但毕竟 typeORM 设置的外键,名字是的就很默认,所以这里我还是加了这个去改外键的名字
@JoinColumn({
name: 'department_id',
})
department: Department
}
而在部门表处,需要添加 OneToMany 装饰器
@Entity()
export class Department {
@PrimaryGeneratedColumn({
type: 'tinyint',
comment: '部门ID,不会有辣么多部门的啦',
})
id: number
@Column()
name: string
@OneToMany(() => Employee, (employee) => employee.department, {
cascade: ['insert', 'update'],
})
employee_list: Employee[]
}
多对多
在之前 MySQL 学习中,我们知道了多对多,需要添加一个中间表作为关联。而在 TypeORM 中,我们只需要给其中一个表添加上 JoinTable 即可添加上中间表。
@Entity()
export class Article {
@PrimaryGeneratedColumn()
id: number
@Column({
length: 100,
comment: '文章标题',
})
name: string
// 多对多的处理
@ManyToMany(() => Tag, (tag) => tag.articles)
@JoinTable({
name: 'article_tags', // 中间表名字
joinColumn: {
name: 'article_id', // 这里设置中间表中与 Article 关联的列名
referencedColumnName: 'id',
},
inverseJoinColumn: {
name: 'tag_id', // 这里设置中间表中与 Tag 关联的列名
referencedColumnName: 'id',
},
})
tags: Tag[]
}
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm'
import { Article } from './Article'
@Entity()
export class Tag {
@PrimaryGeneratedColumn()
id: number
@Column({
length: 20,
comment: '标签名',
})
name: string
// 多对多处理
@ManyToMany(() => Article, (article) => article.tags)
articles: Article[]
}