Midway是面向未来的一款支持Serverless的Node框架,其内置的web框架也是非常的好用,还有一些其他的特性,前后端一体等等。

如果使用过同样是阿里开源的EggJS框架的话,上手是非常简单的,使用装饰器的新特性来开发还是一件很爽的事情的。

TypeORM是一款非常使用的ORM框架,可以通过实体的操作来对带数据库的数据进行读写,只要将实体定义出来之后就可以非常简便的操作数据库了

准备工作

Midway中对TypeORM做了定制化,首先安装依赖,这里需要安装数据库驱动和typeorm

$ npm i @midwayjs/orm typeorm --save
$ npm install mysql2 --save

然后将orm引入configuration.ts中

import { Configuration } from '@midwayjs/decorator';
import * as orm from '@midwayjs/orm';

@Configuration({
  imports: [
    orm                             // 加载 orm 组件
  ],
  // ……
})
export class ContainerConfiguratin {

}

在config.local.ts中添加数据库配置

export const orm = {
  type: 'mysql',
  host: '127.0.0.1',
  port: 3306,
  username: 'remote',
  password: '123456',
  database: 'your_db_name',
  synchronize: false,   // 如果第一次使用,不存在表,有同步的需求可以写 true
  logging: false,
};

创建实体

所谓的实体就是一个对象,这里我们使用User实体来做示例,我们第一步只需要将实体的属性罗列出来

class User {
  id: number;
  username: string;
  phone: string;
  sex: number;
  email: string;
  createTime: Date;
}

现在我们需要使用装饰器来修饰一下这个对象,让它变成框架可以是别的实体

  1. 在原生的TypeORM中,声明实体使用的是@Entity,在Midway中使用的是@EntityModel装饰器,可以更好地和Midway结合
  2. 使用@PrimaryColumn来声明主键(PrimaryGeneratedColumn可以声明自增主键);使用@Column声明普通列,可以声明列的类型(默认是varchar(255))、名称……;使用@CreateDateColumn@UpdateDateColumn可以声明创建时间个更新时间,这两个时间会自动生成,不需要我们额外去维护
@EntityModel('user') // 可以指定表名,不指定会根据类名自己生成
export class User {
  @PrimaryGeneratedColumn()
  id: number;
  
  @Column({
    length: 20, // 长度
    name: 'user_name' // 指定列名
  })
  username: string;
  
  @Column({
    nullable: true, // 是否可以为空
    length: 11
  })
  phone: string;
  
  @Column({
    default: 0, // 默认值
    length: 1
  })
  sex: number;
  
  @Column({
    length: 30,
    nullable: true,
  })
  email: string;
  
  @CreateDateColumn()
  createTime: Date;
}

这样一个可以被TypeORM识别的简单实体类就完成了

数据库操作

在进行数据库的操作之前我们需要先注入Entity的Model,通过@InjectEntityModel注解来进行操作数据库

import { Provide, Inject } from '@midwayjs/decorator';
import { InjectEntityModel } from '@midwayjs/orm';
import { User } from './entity/user';
import { Repository } from 'typeorm';

@Provide()
export class UserService {

  @InjectEntityModel(User)
  userModel: Repository<User>;
}

现在已经注入了userModel,下面的所有的数据库操作都会基于这个userModel进行

新增数据库记录只需要创建一个实体类的实例,将属性值复制之后调用userModel的insert方法即可添加数据

@Provide()
export class UserService {

  @InjectEntityModel(User)
  userModel: Repository<User>;
  
  async saveUser(): Promise<User> {
    const user = new User();
    user.username = 'King';
    
    cosnt insertRes = await this.userModel.insert(user); // save方法也可以
    return insertRes;
  }
}

如果实体中没有配置某一属性是否可以为空或者设置默认值,则必须添加一个值,否则会报错

查数据算是最复杂的一项,可以使用find和findOne等多种方法,find返回的只有一条记录,find会返回一个数组,即使只有一条记录被筛选出来。

@Provide()
export class UserService {

  @InjectEntityModel(User)
  userModel: Repository<User>;
  
  async selectUser(id: number): Promise<User> {
    cosnt user = await this.userModel.findOne({
      where: { id }
    });
    return user;
  }
  
  async selectUsers(id: number): Promise<User[]> {
    cosnt users = await this.userModel.find({
      where: { sex: 1 }
    });
    return users;
  }
}

find还有很多操作项,为了不影响整体的阅读体验,会整理一个table放在后面

删除数据很简单,只需要将要删除的数据查出来再调用一下remove方法即可

@Provide()
export class UserService {

  @InjectEntityModel(User)
  userModel: Repository<User>;
  
  async deleteUser(id: number): Promise<User> {
    cosnt user = await this.userModel.findOne({
      where: { id }
    });
    const res = await this.userModel.remove(user);
    return res;
  }
}

在开发中一般不建议将数据进行remove,更推荐的是使用标记位📌来区分数据的有效性,删除数据时将数据标记置为0等

改的思路同删除,首先要将实体查出来,然后修改实体的属性值,然后再保存

@Provide()
export class UserService {

  @InjectEntityModel(User)
  userModel: Repository<User>;
  
  async updateUser(id: number, username: string): Promise<User> {
    cosnt user = await this.userModel.findOne({
      where: { id }
    });
    user.username = username;
    const res = await this.userModel.update(user);
    return res;
  }
}

find操作项

find选项 参数类型 功能
select string[] 类似于SQL语句中的select查询,返回结果的列只有标注的列
relations string[] 关系需要加载的主体
join {alias: string, leftjoinAndSelect: Record<string, string>} 为实体执行连接操作
where Record<string, any> | Record<string, any>[] SQL语句中的where条件,类型为数组时会使用OR连接不同的部分
order Record<string, ‘ASC’ | ‘DESC’> 会根据指定的属性及排序方式进行排序,ASC是升序
skip number 偏移量(分页)
take number 得到的最大实体数(分页)
cache boolean 启用或禁用查询结果缓存
lock { mode: “optimistic”, version: number | Date } | { mode: “pessimistic_read” | “pessimistic_write” | “dirty_read” } 启用锁查询。 只能在findOne方法中使用

TypeORM 提供了许多内置运算符,可用于创建更复杂的查询:

Not、LessThan、LessThanOrEqual、MoreThan、MoreThanOrEqual、Equal、Like、ILike、Between、In、Any、IsNull、Raw,以上的操作符都可以与Not搭配使用

示例

cosnt users = await this.userModel.find({
  where: { sex: Not(Equal(1)) }
});

实体进阶

前面介绍了实体类的简单定义,真正的开发过程中实体之间会存在多种联系:一对一、一对多……,通过在实体中定义这些关系可以让我们更简单地进行连表查询

还是前面的User实体,我们这里再新加一个签到表的实体Attendance,我们通过不同的记录方式来分别介绍几种关系

一对一

一对一是所有的关系中最简单的一种关系,说白了就是传统数据库的外键的概念,在需要添加外键的实体生声明一个标识就可以了

这种实体对应的模式是一个用户对应一条签到记录,每次签到会更新记录,根据updateTime和attendanceDays来判断连续签到

export class Attendance {
  @PrimaryGeneratedColumn()
  id: number;
  
  @Column({
    default: 0,
  })
  attendanceDays: number;
  
  @UpdateDateColumn()
  updateTime: Date;
  
  @OnoToOne(type => User)
  @JoinColumn()
  user: User;
  /**
   * 可以声明级联保存,级联保存之后不再需要单独保存,
   * 在声明级联的一侧保存实体会自动保存另一方
   * @OneToOne(type => User, user => user.attendance, {
   *		cascade: true,
   * })
   * @JoinColumn()
   * user: User;
   */
}

通过OnoToOne来声明一对一的关系,使用JoinColumn来添加外键,保存时需要将user实例赋值给attendance实例的user属性,然后再保存

async saveAttendance() {
  const user = new User();
  user.username = 'King';
  await this.userModel.save(user);
  
  const attendance = new Attendance();
  attendance.user = user;
  await this.attendanceModel.save(attendance);
}

在查询时需要声明relations可以将两个表联合起来查出来

async findUserA(userId: number) {
  const userA = await this.attendanceModel.findOne({
    where: { id: userId },
    relations: ['user']
  })
}

一对多/多对一

一对多和多对一是一个对应的关系,在一个实体中声明一对多的关系需要在对应的实体中声明多对一关系

这次我们要将签到表的模式换为每次签到生成一条签到记录,一个用户对应多条签到记录,一条签到记录只属于一个用户

export class Attendance {
  @PrimaryGeneratedColumn()
  id: number;
  
  @UpdateDateColumn()
  updateTime: Date;
  
  @ManyToOne(type => User, user => user.attendances)
  user: User;
}

然后要在User中添加一对多关系

export class User {
  // ……
  @ManyToOne(type => Attendance, attendance => attendance.user)
  attendances: Attendance[];
}

这种情况下会在attendance表中将user中的主键作为外键

多对多

多对多关系算是比较复杂的一种情况,再声明多对多关系之后会生成一张使用两个实体主键作为联合主键的表

这里我们使用用户表和权限表做示例,一个用户可以有多个角色,一个角色下对应多个用户,这两者就是多对多的关系

通过ManyToMany来声明两个实体之间的关系,通过JoinTable来表明关系的拥有者

export class Role {
  @PrimaryGeneratedColumn()
  id: number;
  
  @Column({
    default: 0
  })
  roleType: number;
  
  @ManyToMany(type => User, user => user.roles)
  users: User[]
  // 如果只需要在用户表中关联查role这里可以不声明,即为单向关系,申明双向关系之后可以在两个实体中查询
}

在User实体中新增关系声明

export class User {
  // ……
  @ManyToMany(type => Role, role => role.users)
  @JoinTable()
  roles: Role[];
}

QueryBuilder

QueryBuilder是 TypeORM 最强大的功能之一 ,它允许你使用优雅便捷的语法构建 SQL 查询,执行并获得自动转换的实体。

示例

let photos = await this.photoModel
    .createQueryBuilder("photo") // alias
    .innerJoinAndSelect("photo.metadata", "metadata") //relations & alias
    .leftJoinAndSelect("photo.albums", "album")
    .where("photo.isPublished = true")
    .andWhere("(photo.name = :photoName OR photo.name = :bearName)")
    .orderBy("photo.id", "DESC")
    .skip(5)
    .take(10)
    .setParameters({ photoName: "My", bearName: "Mishka" })
    .getMany();

更多的QueryBuilder这里无法展开细说,看一下官网文档


前端小白