TypeORM - sql框架

486 阅读6分钟

前言

在使用 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:查找多条记录,可以指定 whereorder by 等条件
findBy:查找多条记录,第二个参数直接指定 where 条件,更简便一点
findAndCount:查找多条记录,并返回总数量
findByAndCount:根据条件查找多条记录,并返回总数量
findOne:查找单条记录,可以指定 whereorder 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[]
}