跳到主要内容
版本:4.0.0 🚧

Sequelize

本文档介绍如何在 Midway 中使用 Sequelize。

提示

当前模块从 v3.4.0 开始已经重构,历史写法兼容,如果查询历史文档,请参考 这里

相关信息:

描述
可用于标准项目
可用于 Serverless
可用于一体化
包含独立主框架
包含独立日志

和老写法的区别

如果想使用新版本的用法,请参考下面的流程,将老代码进行修改,新老代码不能混用。

升级方法:

  • 1、请在业务依赖中显式添加 sequelizesequelize-typescript
  • 2、不再使用 BaseTable 装饰器,而直接使用 sequelize-typescript 包导出的 Table 装饰器
  • 3、在 src/config.defaultsequelize 部分配置调整,参考下面的数据源配置部分
    • 3.1 修改为数据源的形式 sequelize.dataSource
    • 3.2 将实体模型在数据源的 entities 字段中声明

安装依赖

$ npm i @midwayjs/sequelize@3 sequelize sequelize-typescript --save

或者在 package.json 中增加如下依赖后,重新安装。

{
"dependencies": {
"@midwayjs/sequelize": "^3.0.0",
"sequelize": "^6.21.3",
"sequelize-typescript": "^2.1.0"
// ...
},
"devDependencies": {
// ...
}
}

安装数据库 Driver

常用数据库驱动如下,选择你对应连接的数据库类型安装:

# for MySQL or MariaDB,也可以使用 mysql2 替代
npm install mysql --save
npm install mysql2 --save

# for PostgreSQL or CockroachDB
npm install pg --save

# for SQLite
npm install sqlite3 --save

# for Microsoft SQL Server
npm install mssql --save

# for sql.js
npm install sql.js --save

# for Oracle
npm install oracledb --save

# for MongoDB(experimental)
npm install mongodb --save

下面的文档,我们将以 mysql2 作为示例。

Directory structure

一个基础的参考目录结构如下。

MyProject
├── src
│ ├── config
│ │ └── config.default.ts
│ ├── entity
│ │ └── person.entity.ts
│ ├── configuration.ts
│ └── service
├── .gitignore
├── package.json
├── README.md
└── tsconfig.json

启用组件

src/configuration.ts 文件中启用组件。

import { Configuration, ILifeCycle } from '@midwayjs/core';
import { join } from 'path';
import * as sequelize from '@midwayjs/sequelize';

@Configuration({
imports: [
// ...
sequelize,
],
importConfigs: [join(__dirname, './config')],
})
export class MainConfiguration implements ILifeCycle {
// ...
}

模型定义

1、创建 Model(Entity)

我们通过模型和数据库关联,在应用中的模型就是数据库表,在 Sequelize 中,模型是和实体绑定的,每一个实体(Entity) 文件,即是 Model,也是实体(Entity)。

在示例中,需要一个实体,我们这里拿 person 举例。新建 entity 目录,在其中添加实体文件 person.entity.ts ,一个简单的实体如下。

// src/entity/person.entity.ts
import { Table, Model, Column, HasMany } from 'sequelize-typescript';

@Table
export class Hobby extends Model {
@Column
name: string;
}

@Table
export class Person extends Model {
@Column
name: string;

@Column
birthday: Date;

@HasMany(() => Hobby)
hobbies: Hobby[];
}

要注意,这里的实体文件的每一个属性,其实是和数据库表一一对应的,基于现有的数据库表,我们往上添加内容。

@Table 装饰器可以在不传递任何参数的情况下使用,更多参数请查看 定义选项

@Table({
timestamps: true,
...
})
export class Person extends Model {}

2、主键

主键 (id) 将从基类 Model 继承。 一般来说主键是 Integer 类型并且是自增的。

主键设置有两种方法,设置 @Column({primaryKey: true}) 或者 @PrimaryKey

比如:

import { Table, Model, PrimaryKey } from 'sequelize-typescript';

@Table
export class Person extends Model {
@PrimaryKey
name: string;
}

3、时间列

主要指代的是 @CreatedAt, @UpdatedAt, @DeletedAt 单个装饰器标注的列。

比如:

import { Table, Model, CreatedAt, UpdatedAt, DeletedAt } from 'sequelize-typescript';

@Table
export class Person extends Model {
@CreatedAt
creationDate: Date;

@UpdatedAt
updatedOn: Date;

@DeletedAt
deletionDate: Date;
}
装饰器描述
@CreatedAt会设置 timestamps=truecreatedAt='creationDate'
@UpdatedAt会设置 timestamps=trueupdatedAt='updatedOn'
@DeletedAt会设置 timestamps=true, paranoid=truedeletedAt='deletionDate'

4、普通列

@Column 装饰器用于标注普通列,可以在不传递任何参数的情况下使用。 但是因此需要能够自动推断 js 类型(详见类型推断)。

import { Table, Model, Column } from 'sequelize-typescript';

@Table
export class Person extends Model {
@Column
name: string;
}

或者指定列类型。

import { Table, Column, DataType } from 'sequelize-typescript';

@Table
export class Person extends Model {
@Column(DataType.TEXT)
name: string;
}

更多类型描述,请参考 这里

比如:

import { Table, Model, Column, DataType } from 'sequelize-typescript'

@Table
export class Person extends Model {
@Column({
type: DataType.FLOAT,
comment: 'Some value',
...
})
value: number;
}
装饰器描述
@Column使用推导的 dataType 作为类型
@Column(dataType: DataType)显式设置 dataType
@Column(options: AttributeOptions)设置 attribute options

数据源配置

新版本我们启用了 数据源机制,在 src/config.default.ts 中配置:

// src/config/config.default.ts

import { Person } from '../entity/person.entity';

export default {
// ...
sequelize: {
dataSource: {
// 第一个数据源,数据源的名字可以完全自定义
default: {
database: 'test4',
username: 'root',
password: '123456',
host: '127.0.0.1',
port: 3306,
encrypt: false,
dialect: 'mysql',
define: { charset: 'utf8' },
timezone: '+08:00',
// 本地的时候,可以通过 sync: true 直接 createTable
sync: false,

// 实体形式
entities: [Person],

// 支持如下的扫描形式,为了兼容我们可以同时进行.js和.ts匹配️
entities: [
'entity', // 指定目录
'**/entity/*.entity.{j,t}s', // 通配加后缀匹配
],
},

// 第二个数据源
default2: {
// ...
},
},
},
};

模型关联

可以通过 HasMany@HasOne@BelongsTo@BelongsToMany@ForeignKey 装饰器在模型中直接描述关系。

提示

你不需要在数据库中创建外键也可以使用这个功能。

一对多

import { Table, Model, Column, ForeignKey, BelongsTo, HasMany } from 'sequelize-typescript';

@Table
export class Player extends Model {
@Column
name: string;

@Column
num: number;

@ForeignKey(() => Team)
@Column
teamId: number;

@BelongsTo(() => Team)
team: Team;
}

@Table
export class Team extends Model {
@Column
name: string;

@HasMany(() => Player)
players: Player[];
}

sequelize-typescript 会在内部进行关联,会自动查询出相关的依赖。

比如通过 find 查询。

const team = await Team.findOne({ include: [Player] });

team.players.forEach((player) => {
console.log(`Player ${player.name}`);
});

多对多

import { Table, Model, Column, ForeignKey, BelongsToMany } from 'sequelize-typescript';

@Table
export class Book extends Model {
@BelongsToMany(() => Author, () => BookAuthor)
authors: Author[];
}

@Table
export class Author extends Model {
@BelongsToMany(() => Book, () => BookAuthor)
books: Book[];
}

@Table
export class BookAuthor extends Model {
@ForeignKey(() => Book)
@Column
bookId: number;

@ForeignKey(() => Author)
@Column
authorId: number;
}

上面的类型,在某些场景下是不安全的,比如上面的 BookAuthorAuthorbooks 的类型,可能会丢失某些属性,需要手动设置。

@BelongsToMany(() => Book, () => BookAuthor)
books: Array<Book & {BookAuthor: BookAuthor}>;

一对一

对于一对一,使用 @HasOne(...)(关系的外键存在于另一个模型上)和 @BelongsTo(...)(关系的外键存在于此模型上)。

比如:

import { Table, Column, Model, BelongsTo, ForeignKey } from 'sequelize-typescript';
import { User } from './user.entity';

@Table
export class Photo extends Model {
@ForeignKey(() => User)
@Column({
comment: '用户Id',
})
userId: number;

@BelongsTo(() => User)
user: User;

@Column({
comment: '名字',
})
name: string;
}

@Table
export class User extends Model {
@Column
name: string;
}

模型循环依赖

如果你使用了 @BelongsTo 装饰器,很容易触发一个模型循环依赖的错误,比如:

ReferenceError: Cannot access 'Photo' before initialization

你可以将类型使用 ReturnType 包裹起来。

import { Table, Column, Model, BelongsTo, ForeignKey } from 'sequelize-typescript';
import { User } from './user.entity';

@Table
export class Photo extends Model {
// ...
@BelongsTo(() => User)
user: ReturnType<() => User>;
}

静态操作方法

如果是单个数据源,可以使用下面的静态方法。

保存

在需要调用的地方,使用实体模型来操作。

import { Provide } from '@midwayjs/core';
import { Person } from '../entity/person.entity';

@Provide()
export class PersonService {
async createPerson() {
const person = new Person({ name: 'bob', age: 99 });
await person.save();
}
}

查找和更新

import { Provide } from '@midwayjs/core';
import { Person } from '../entity/person.entity';

@Provide()
export class PersonService {
async updatePerson() {
const person = await Person.findOne();
// 更新
person.age = 100;
await person.save();

await Person.update(
{
name: 'bobby',
},
{
where: { id: 1 },
}
);
}
}

Repository 模式

Repository 模式可以将查找、创建等静态操作从模型定义中分离出来。它还支持与多个 sequelize 实例(多数据源)一起使用。

启动 Repository 模式

和数据源配置相同,只是多了一个属性。

// src/config/config.default.ts

import { Person } from '../entity/person.entity';

export default {
// ...
sequelize: {
dataSource: {
default: {
// ...
entities: [Person],

// 多了这一个
repositoryMode: true,
},
},
sync: false,
},
};

如果是多个数据源,务必在每个数据源都开启该属性,开启后,原有的静态操作方法不再可用。

你需要使用 Repository 的操作方式。

使用 Repository 模式

基本 API 和静态操作相同,Midway 对其进行了一些简单包裹,使用 InjectRepository 装饰器可以在服务中注入 Repository

import { Controller, Get } from '@midwayjs/core';
import { InjectRepository } from '@midwayjs/sequelize';
import { Photo } from '../entity/photo.entity';
import { User } from '../entity/user.entity';
import { Op } from 'sequelize';
import { Repository } from 'sequelize-typescript';

@Controller('/')
export class HomeController {
@InjectRepository(User)
userRepository: Repository<User>;

@InjectRepository(Photo)
photoRepository: Repository<Photo>;

@Get('/')
async home() {
// 查询
let result = await this.photoRepository.findAll();
console.log(result);

// 新增
await this.photoRepository.create({
name: '123',
});

// 删除
await this.photoRepository.destroy({
where: {
name: '123',
},
});

// 联合查询
// SELECT * FROM photo WHERE name = "23" OR name = "34";
let result = await this.photoRepository.findAll({
where: {
[Op.or]: [{ name: '23' }, { name: '34' }],
},
});
// => result

// 连表查询
let result = await this.userRepository.findAll({ include: [Photo] });
// => result
}
}

关于 OP 的更多用法:https://sequelize.org/v5/manual/querying.html

多库的支持

在 Repository 模式下,我们可以在 InjectRepository 参数中指定特定的数据源。

import { Controller } from '@midwayjs/core';
import { InjectRepository } from '@midwayjs/sequelize';
import { Photo } from '../entity/photo.entity';
import { User } from '../entity/user.entity';
import { Repository } from 'sequelize-typescript';

@Controller('/')
export class HomeController {
// 指定某个数据源
@InjectRepository(User, 'default')
userRepository: Repository<User>;
// ...
}

高级功能

数据源同步配置

sequelize 在同步数据源时可以添加 sync 的参数。

export default {
// ...
sequelize: {
dataSource: {
default: {
sync: true,
syncOptions: {
force: false,
alter: true,
},
},
},
// 多个数据源时可以用这个指定默认的数据源
defaultDataSourceName: 'default',
},
};

指定默认数据源

在包含多个数据源时,可以指定默认的数据源。

export default {
// ...
sequelize: {
dataSource: {
default1: {
// ...
},
default2: {
// ...
},
},
// 多个数据源时可以用这个指定默认的数据源
defaultDataSourceName: 'default1',
},
};

获取数据源

数据源即创建出的 sequelize 对象,我们可以通过注入内置的数据源管理器来获取。

import { Configuration } from '@midwayjs/core';
import { SequelizeDataSourceManager } from '@midwayjs/sequelize';

@Configuration({
// ...
})
export class MainConfiguration {

async onReady(container: IMidwayContainer) {
const dataSourceManager = await container.getAsync(SequelizeDataSourceManager);
const conn = dataSourceManager.getDataSource('default');
await conn.authenticate();
}
}

从 v3.8.0 开始,也可以通过装饰器注入。

import { Configuration } from '@midwayjs/core';
import { InjectDataSource } from '@midwayjs/sequelize';
import { Sequelize } from 'sequelize-typescript';

@Configuration({
// ...
})
export class MainConfiguration {

// 注入默认数据源
@InjectDataSource()
defaultDataSource: Sequelize;

// 注入自定义数据源
@InjectDataSource('default1')
customDataSource: Sequelize;

async onReady(container: IMidwayContainer) {
// ...
}
}

常见问题

1、Dialect needs to be explicitly supplied as of v4.0.0

原因为配置中数据源没有指定 dialect 字段,确认数据源的结构,格式以及配置合并的结果。

2、生成实体列

请参考社区提供的模块,如 sequelize-typescript-generator

3、Raw Query

如果遇到比较复杂的,可以使用 raw query 方法

4、TS2612 错误

如果你的模型列报了 TS2612 错误,比如:

src/entity/AesTenantConfigInfo.ts:29:6 - error TS2612: Property 'id' will overwrite the base property in 'Model<AesTenantConfigInfoAttributes, AesTenantConfigInfoAttributes>'. If this is intentional, add an initializer. Otherwise, add a 'declare' modifier or remove the redundant declaration.

29 id?: number;
~~

可以将其赋一个空值。

import { Table, Column } from 'sequelize-typescript';

@Table
export class User extends Model {
@Column({
primaryKey: true,
autoIncrement: true,
type: DataType.BIGINT,
})
id?: number = undefined;
}

其他

  • 上面的文档,翻译自 sequelize-typescript,更多 API ,请参考 英文文档
  • 一些 案例