一对一
数据库表关系
在数据库中,表与表之间存在不同类型的关系:
- 一对一关系:例如用户(User)与身份证(IdCard)之间的关系。
- 一对多关系:例如部门(Department)与员工(Employee)之间的关系。
- 多对多关系:例如文章(Article)与标签(Tag)之间的关系。
这些关系通常通过外键(Foreign Key)来维护,而多对多关系还需要建立一个中间表(Intermediate Table)。
一对一映射关系创建
TypeORM 是一个 ORM 框架,它将数据库的表、字段以及表之间的关系映射为实体类(Entity Class)、属性(Property)和实体之间的关系。
下面是如何在 TypeORM 中映射这些关系的操作步骤:
创建数据库
sql
create database typeorm_test;
初始化项目
初始化 TypeORM 项目:
bash
npx typeorm@latest init --name typeorm-relation-mapping --database mysql
安装驱动包 mysql2:
bash
npm install mysql2
修改 DataSource 文件的配置
typescript
import 'reflect-metadata'
import { DataSource } from 'typeorm'
import { User } from './entity/User'
export const AppDataSource = new DataSource({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'xxx',
database: 'typeorm_test',
synchronize: true,
logging: true,
entities: [User],
migrations: [],
subscribers: [],
poolSize: 10,
connectorPackage: 'mysql2',
extra: {
authPlugin: 'sha256_password',
},
})
启动项目
bash
npm run start
创建身份证表(IdCard)
bash
npx typeorm entity:create src/entity/IdCard
在 IdCard
实体中添加属性和映射信息:
typescript
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity({ name: 'id_card' })
export class IdCard {
@PrimaryGeneratedColumn()
id: number;
@Column({
length: 50,
comment: '身份证号'
})
cardNumber: string;
}
在 DataSource 的 entities 里引入下:
重新 npm run start:
现在 user 和 id_card 表都有了,怎么让它们建立一对一的关联呢?
建立一对一关联
切换 typeorm_test 数据库,把这两个表删除:
sql
drop table id_card,user;
在 IdCard
实体中添加 user
属性,并使用 @OneToOne
和 @JoinColumn
装饰器来指定与 User
实体的一对一关系:
如果用 @JoinColumn({ name: 'user_id' })
会告诉 TypeORM 使用 user_id
作为外键列的名称,而不是默认的 userId
。
一对一关系的外键列可以放在任何一方,通常,外键列放置在访问最频繁的那一方。
外键列放在哪一方,那一方就是拥有关系的一方(也称为拥有者方)。拥有者方负责维护关系,包括外键的更新和删除。
重新 npm run start 后,在 workbench 里看下:
多出了 userId
外键列与 user 表相连。
级联操作
级联关系还是默认的:
如果我们想设置 CASCADE ,可以在第二个参数指定:
我们可以将其设置为 CASCADE。
一对一映射关系增删改查
增加
创建 User
和 IdCard
对象,建立关联后保存。先保存 user,再保存 idCard。
npm run start 后,数据都插入成功了:
上面保存还要分别保存 user 和 idCard,能不能自动按照关联关系来保存呢?
可以的,在 @OneToOne 那里指定 cascade 为 true:
这样我们就不用自己保存 user 了:
查询
使用 find
方法或 QueryBuilder
来查询数据,可以通过指定 relations
参数来实现关联查询:
QueryBuilder
进行更复杂查询:
先 getRepository 拿到操作 IdCard 的 Repository 对象。
再创建 queryBuilder 来连接查询,给 idCard 起个别名 ic,然后连接的是 ic.user,起个别名为 u:
或者也可以直接用 EntityManager 创建 queryBuilder 来连接查询:
查询的结果是一样的。
修改
我们来修改下数据,数据长这样:
我们给它加上 id 再 save:
可以看到在一个事务内,执行了两条 update 的 sql。
删除
如果设置了外键的 onDelete
为 cascade
,删除 User
实体时,关联的 IdCard
实体也会被自动删除:
如果没有设置级联删除,需要手动删除关联的实体:
反向关系
如果现在想在 user 里访问 idCard 呢?
同样需要加一个 @OneToOne 的装饰器:
需要通过第二个参数告诉 typeorm,外键是另一个 Entity 的哪个属性。
我们查一下试试:
可以看到,同样关联查询成功了。
这就是一对一关系的映射和增删改查。
一对多
环境搭建
创建 typeorm 项目:
bash
npx typeorm@latest init --name typeorm-relation-mapping2 --database mysql
进入项目目录,安装驱动包 mysql2:
bash
npm install mysql2
修改 data-source.ts 文件,配置数据库连接信息:
typescript
import "reflect-metadata"
import { DataSource } from "typeorm"
import { User } from "./entity/User"
export const AppDataSource = new DataSource({
type: "mysql",
host: "localhost",
port: 3306,
username: "root",
password: "xxx",
database: "typeorm_test",
synchronize: true,
logging: true,
entities: [User],
migrations: [],
subscribers: [],
poolSize: 10,
connectorPackage: 'mysql2',
extra: {
authPlugin: 'sha256_password',
}
})
实体创建与映射
创建 Department 和 Employee 两个实体:
bash
npx typeorm entity:create src/entity/Department
npx typeorm entity:create src/entity/Employee
在 Department 和 Employee 实体中添加映射信息:
typescript
// Department 实体
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Department {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 50 })
name: string;
}
// Employee 实体
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Employee {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 50 })
name: string;
}
把这俩 Entity 添加到 DataSource 里:
typescript
import 'reflect-metadata'
import { DataSource } from 'typeorm'
import { User } from './entity/User'
import { Department } from './entity/Department'
import { Employee } from './entity/Employee'
export const AppDataSource = new DataSource({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'xxx',
database: 'typeorm_test',
synchronize: true,
logging: true,
entities: [Department, Employee],
migrations: [],
subscribers: [],
poolSize: 10,
connectorPackage: 'mysql2',
extra: {
authPlugin: 'sha256_password',
},
})
index.ts 代码清空:
然后 npm run start:
这两个表都创建成功了。
添加一对多关系映射
在多的一方使用 @ManyToOne 装饰器:
把这两个表删掉:
重新 npm run start:
mysql workbench 可以看到这个外键:
一对多的 CRUD 操作
初始化和保存数据
引入相关实体和数据源,然后初始化数据源:
typescript
import { Department } from './entity/Department';
import { Employee } from './entity/Employee';
import { AppDataSource } from "./data-source";
AppDataSource.initialize().then(async () => {
// 数据初始化和保存的逻辑
}).catch(error => console.log(error));
添加并保存实体
创建一个部门和几个员工实例,并将员工分配给该部门:
typescript
const d1 = new Department();
d1.name = '技术部';
const e1 = new Employee();
e1.name = '张三';
e1.department = d1;
const e2 = new Employee();
e2.name = '李四';
e2.department = d1;
const e3 = new Employee();
e3.name = '王五';
e3.department = d1;
await AppDataSource.manager.save(Department, d1);
await AppDataSource.manager.save(Employee, [e1, e2, e3]);
可以看到被 transaction 包裹的 4 条 insert 语句,分别插入 1 了 Department 和 3 个 Employee。
建立多对多的外键关联并设置级联保存
我们在 Department 实体中使用 @OneToMany 装饰器声明关系这是"一的一方",不维护外键关系。
通过第二个参数指定外键列在哪里。并设置 cascade 选项,简化保存逻辑:
多的那一方,即 Employee 会自动添加外键,可以通过 @JoinColumn 来修改外键列的名字:
typescript
@JoinColumn({
name: 'd_id',
})
@ManyToOne(() => Department)
department: Department;
这样当保存 department 的时候,关联的 employee 也会保存:
typescript
const e1 = new Employee();
e1.name = '张三';
const e2 = new Employee();
e2.name = '李四';
const e3 = new Employee();
e3.name = '王五';
const d1 = new Department();
d1.name = '技术部';
d1.employees = [e1, e2, e3];
await AppDataSource.manager.save(Department, d1);
关联查询
查询所有部门:
typescript
const deps = await AppDataSource.manager.find(Department)
console.log(deps)
想要关联查询员工需要声明下 relations:
typescript
const deps = await AppDataSource.manager.find(Department, {
relations: {
employees: true,
},
})
console.log(deps)
console.log(deps.map(item => item.employees))
这个 relations 其实就是 left join on。
或者使用 QueryBuilder 进行更灵活的关联查询:
typescript
const es = await AppDataSource.manager
.getRepository(Department)
.createQueryBuilder('d')
.leftJoinAndSelect('d.employees', 'e')
.getMany()
console.log(es)
console.log(es.map(item => item.employees))
也可以直接用 EntityManager 创建 QueryBuilder:
typescript
const es = await AppDataSource.manager
.createQueryBuilder(Department, 'd')
.leftJoinAndSelect('d.employees', 'e')
.getMany()
console.log(es)
console.log(es.map(item => item.employees))
删除数据
在删除数据时,如果设置了 onDelete 为 SET NULL 或 CASCADE,我们就只要删除主表(一的那一方)对应的 Entity 就好了,msyql 会做后续的关联删除或者 id 置空:
否则就要先删除所有的从表(多的那一方)对应的 Entity 再删除主表对应的 Entity:
typescript
const deps = await AppDataSource.manager.find(Department, {
relations: {
employees: true
}
});
await AppDataSource.manager.delete(Employee, deps[0].employees);
await AppDataSource.manager.delete(Department, deps[0].id);
多对多
实体关系映射
一对一关系
- 使用 @OneToOne 和 @JoinColumn 注解来映射实体到数据库表,转换为表之间的外键关联。
一对多关系
- 通过 @OneToMany 和 @ManyToOne 注解实现,不需要使用 @JoinColumn 指定外键列,因为外键自然存在于"多"的一方。
多对多关系
- 多对多关系通过中间表实现,通过 @ManyToMany 注解,可以将多对多关系拆解为两个一对多关系:
多对多实体操作
初始化项目和配置数据库
bash
npx typeorm@latest init --name typeorm-relation-mapping --database mysql
cd typeorm-relation-mapping
npm install mysql2
配置文件(data-source.ts)修改
typescript
import 'reflect-metadata'
import { DataSource } from 'typeorm'
import { User } from './entity/User'
export const AppDataSource = new DataSource({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'xxx',
database: 'typeorm_test',
synchronize: true,
logging: true,
entities: [User],
migrations: [],
subscribers: [],
poolSize: 10,
connectorPackage: 'mysql2',
extra: {
authPlugin: 'sha256_password',
},
})
创建实体生成表
这次我们创建 Article 和 Tag 两个实体:
bash
npx typeorm entity:create src/entity/Article
npx typeorm entity:create src/entity/Tag
添加一些属性:
typescript
// Article 实体
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Article {
@PrimaryGeneratedColumn()
id: number;
@Column({
length: 100,
comment: '文章标题'
})
title: string;
@Column({
type: 'text',
comment: '文章内容'
})
content: string;
}
// Tag 实体
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Tag {
@PrimaryGeneratedColumn()
id: number;
@Column({
length: 100
})
name: string;
}
data-source.ts 引入这两个 Entity:
把 index.ts 的代码去掉:
然后 npm run start:
可以看到它生成了两个表:
我们将其删除,然后来添加多对多的关联关系。
配置多对多关系
通过 @ManyToMany 关联,比如一篇文章可以有多个标签:
然后 npm run start:
会建三张表:
中间表 article_my_tags_tag 还有 2 个外键分别引用着两个表:
级联删除和级联更新都是 CASCADE,也就是说这两个表的记录删了,那它在中间表中的记录也会跟着被删:
也可以自己指定中间表的名字:
插入
typescript
import { AppDataSource } from './data-source';
import { Article } from './entity/Article';
import { Tag } from './entity/Tag';
AppDataSource.initialize()
.then(async () => {
const article1 = new Article();
article1.title = '标题一';
article1.content = '内容一';
const article2 = new Article();
article2.title = '标题二';
article2.content = '内容二';
const tag1 = new Tag();
tag1.name = '标签1';
const tag2 = new Tag();
tag2.name = '标签2';
const tag3 = new Tag();
tag3.name = '标签3';
// 文章1 有两个 tag
article1.myTags = [tag1, tag2];
// 文章2 有三个 tag
article2.myTags = [tag1, tag2, tag3];
const entityManager = AppDataSource.manager;
await entityManager.save([tag1, tag2, tag3]);
await entityManager.save([article1, article2]);
})
.catch(error => console.log(error));
创建了两篇文章,3 个标签,建立它们的关系之后,会先保存所有的 tag,再保存 article。
npm run start 可以看到,3 个标签、2 篇文章,还有两者的关系,都插入成功了:
查询
typescript
const entityManager = AppDataSource.manager;
const article = await entityManager.find(Article, {
relations: {
myTags: true,
},
});
console.log(article);
console.log(article.map(item => item.myTags));
也可以手动用查询构建器(query builder)来获取数据,结果是一样的:
typescript
const entityManager = AppDataSource.manager;
const article = await entityManager
.createQueryBuilder(Article, 'a')
.leftJoinAndSelect('a.myTags', 't')
.getMany();
console.log(article);
console.log(article.map(item => item.myTags));
或者先拿到 Article 的 Repository 再创建 query builder 来查询也行:
typescript
const entityManager = AppDataSource.manager;
const article = await entityManager
.getRepository(Article)
.createQueryBuilder('a')
.leftJoinAndSelect('a.myTags', 't')
.getMany();
console.log(article);
console.log(article.map(item => item.myTags));
更新
如果需要更新文章的标题和标签:
typescript
const entityManager = AppDataSource.manager;
// 查询ID为2的文章,并包含其标签关系
const articleToUpdate = await entityManager.findOne(Article, {
where: { id: 2 },
relations: { myTags: true },
});
// 更新文章标题
articleToUpdate.title = '新标题';
// 筛选包含"标签1"的标签
articleToUpdate.myTags = articleToUpdate.myTags.filter(tag =>
tag.name.includes('标签1')
);
// 保存更新后的文章
await entityManager.save(articleToUpdate);
运行后:
articleId 为 2 的新标题对应的 tagId 就只有 1 个了。
删除
对于删除操作,由于设置了 CASCADE 级联删除,删除文章或标签时相关的关联记录也会被自动删除:
typescript
// 删除ID为1的文章记录
await entityManager.delete(Article, 1);
// 删除ID为1的标签记录
await entityManager.delete(Tag, 1);
第一行代码执行后:
可以看到 article 表和中间表对应的数据都被删除。
第二行代码执行后:
此时中间表的数据已经清空,代表两张表没有关联的内容了。
反向引用
如果 tag 里也想有文章的引用呢?那就再加一个 @ManyToMany 的映射属性:
需要第二个参数指定外键列在哪里。
article 里也要加:
因为多对多的时候,双方都不维护外键,所以都需要第二个参数来指定外键列在哪里,怎么找到当前 Entity。
然后我们通过 tag 来关联查询下:
typescript
const entityManager = AppDataSource.manager;
const tags = await entityManager.find(Tag, {
relations: {
myArticles: true,
},
});
console.log(tags);
成功关联查出来。
话说回来,之前一对一的时候, user 那方不维护外键,所以需要第二个参数来指定通过哪个外键找到 user:
一对多,一对应的 department 那方,不维护外键,所以需要第二个参数来指定通过哪个外键找到 department:
迁移
TypeORM 简介与环境配置
TypeORM 是一个基于 TypeScript 的 ORM 框架,支持使用 TypeScript 或 JavaScript 进行数据库操作。
它提供了一个高级的接口来操作数据库,使得数据库操作更加简洁和高效。
环境搭建步骤
创建 TypeORM 项目
bash
npx typeorm@latest init --name typeorm-migration --database mysql
cd typeorm-migration
安装 MySQL2
bash
npm install mysql2
配置数据源(data-source.ts)
typescript
import 'reflect-metadata';
import { DataSource } from 'typeorm';
import { User } from './entity/User';
export const AppDataSource = new DataSource({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'xxx',
database: 'migration-test',
synchronize: true,
logging: true,
entities: [User],
migrations: [],
subscribers: [],
poolSize: 10,
connectorPackage: 'mysql2',
extra: {
authPlugin: 'sha256_password',
},
});
创建数据库
可以使用 MySQL Workbenc:
也可以执行 sql 命令:
bash
CREATE SCHEMA `migration-test` DEFAULT CHARACTER SET utf8mb4 ;
运行项目
bash
npm run start
生成对应的表并插入了数据:
因为代码默认 save 了一条数据:
开发环境与生产环境的差异
在开发环境中,使用 synchronize 功能可以在修改代码后,自动创建和修改表结构,极大地方便了开发。
然而,在生产环境中,这种做法可能会导致数据丢失,因此不推荐使用。
迁移(Migration)的使用
迁移允许你以编程方式管理数据库的变更,每次变更都会被记录,可以随时撤销。
创建和执行迁移的步骤:
创建迁移文件
使用命令 npx typeorm-ts-node-esm migration:create ./src/migration/first
创建一个新的迁移文件。
生成了 时间戳-first 的 ts 文件:
编写迁移逻辑
在上面生成的迁移文件中,可以使用 SQL 语句编写创建或修改表的逻辑。
执行迁移
使用命令 npx typeorm-ts-node-esm migration:run -d ./src/data-source.ts
执行迁移,应用数据库变更。
生成迁移文件
我们修改下实体:
这时实体已经发生变化,可以使用 npx typeorm-ts-node-esm migration:generate ./src/migration/first -d ./src/data-source.ts
命令自动生成迁移文件,简化迁移流程。
生成的文件如下:
把 synchronize 关掉,用 migration 来手动建表:
npx typeorm-ts-node-esm migration:run -d ./src/data-source.ts
执行下迁移后:
确实没有 age 字段了:
撤销迁移
使用命令 npx typeorm-ts-node-esm migration:revert -d ./src/data-source.ts
撤销上一次的迁移操作。
看看数据库:
age 添加回来了。