完美掌握 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 添加回来了。

相关推荐
一 乐26 分钟前
基于vue船运物流管理系统设计与实现(源码+数据库+文档)
前端·javascript·数据库·vue.js·spring boot·后端·船运系统
m0_5287238136 分钟前
在React中使用redux
前端·javascript·react.js
傻小胖1 小时前
vue3中customRef的用法以及使用场景
前端·javascript·vue.js
谦谦橘子1 小时前
手把手教你实现一个富文本
前端·javascript
Future_yzx1 小时前
Java Web的发展史与SpringMVC入门学习(SpringMVC框架入门案例)
java·前端·学习
star010-1 小时前
【视频+图文详解】HTML基础4-html标签的基本使用
前端·windows·经验分享·网络安全·html·html5
engchina1 小时前
CSS Display属性完全指南
前端·css
engchina1 小时前
详解CSS `clear` 属性及其各个选项
前端·css·css3
沈韶珺1 小时前
Elixir语言的安全开发
开发语言·后端·golang
yashunan3 小时前
Web_php_unserialize
android·前端·php