完美掌握 TypeORM 关系映射和迁移

一对一

数据库表关系

在数据库中,表与表之间存在不同类型的关系:

  • 一对一关系:例如用户(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。

一对一映射关系增删改查

增加

创建 UserIdCard 对象,建立关联后保存。先保存 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。

删除

如果设置了外键的 onDeletecascade,删除 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 添加回来了。

相关推荐
To_OC7 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic8 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy8 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志8 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble9 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯10 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
戮漠summer10 小时前
Missing Semester 计算机教育中缺失的一课 Lecture 01 Shell
开发语言·后端·scala
名字还没想好☜11 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司11 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
kaiking_g11 小时前
vue项目中,静态导入 vs 动态导入 详解
前端·javascript·vue.js