本文的主角是 sequelize-automate
背景
Sequelize 是 Node.js 中常用的 ORM 库,其作用就是对数据库表和代码中的对象做一个映射,让我们能够通过面向对象的方式去查询和操作数据库。
举个例子,数据库可能有一张 user
表,使用 Sequelize 将其映射为一个 UserModel
,之后我们就可以通过 UserModel.findAll()
去查询数据库,Sequelize 会将该方法转换为 SQL:select * from user
。
当我们使用 Sequelize 时,首先要手动定义一个 Model,如:
class UserModel extends Model {}
User.init({
id: DataTypes.INTEGER,
name: DataTypes.STRING,
birthday: DataTypes.DATE
}, { sequelize, modelName: 'userModel' });
然后可以通过 sequelize.sync()
将 UserModel
同步到数据库中。简而言之就是,先在代码中定义 Models,再通过 Models 创建/更新表结构。
但通常我们开发时,是先创建表,然后再写业务代码。而且我们的表结构不能轻易变更,变更表结构可能有单独的流程。所以大部分情况下,我们都是根据表结构手动写 Models,而不能直接使用 sequelize.sync()
去更新表结构。
然而当表非常多的时候,手动写 Models 是一件非常繁琐的事情,并且都是低级的重复性的事情。显然这种事情应该交由工具来做,这个工具就是 sequelize-automate 。
Sequelize-Automate 简介
sequelize-automate 是一个根据表结构自动创建 models 的工具。主要功能特性如下:
- 支持 MySQL / PostgreSQL / Sqlite / MariaDB / Microsoft SQL Server 等 Sequelize 支持的所有数据库
- 支持生成 JavaScript / TypeScript / Egg.js / Midway.js 等不同风格的 Models,并且可扩展
- 支持主键、外键、自增、字段注释等属性
- 支持自定义变量命名、文件名风格
以 MySQL 为例,假设表结构如下:
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'primary ket',
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'user name',
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'user email',
`created_at` datetime NOT NULL COMMENT 'created datetime',
`updated_at` datetime NOT NULL COMMENT 'updated datetime',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='User table'
则使用 sequelize-automate 可以自动生成的 Model 文件为 models/user.js
:
const {
DataTypes
} = require('sequelize');
module.exports = sequelize => {
const attributes = {
id: {
type: DataTypes.INTEGER(11).UNSIGNED,
allowNull: false,
defaultValue: null,
primaryKey: true,
autoIncrement: true,
comment: "primary key",
field: "id"
},
name: {
type: DataTypes.STRING(100),
allowNull: false,
defaultValue: null,
primaryKey: false,
autoIncrement: false,
comment: "user name",
field: "name",
unique: "uk_name"
},
email: {
type: DataTypes.STRING(255),
allowNull: false,
defaultValue: null,
primaryKey: false,
autoIncrement: false,
comment: "user email",
field: "email"
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: null,
primaryKey: false,
autoIncrement: false,
comment: "created datetime",
field: "created_at"
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: null,
primaryKey: false,
autoIncrement: false,
comment: "updated datetime",
field: "updated_at"
}
};
const options = {
tableName: "user",
comment: "",
indexes: []
};
const UserModel = sequelize.define("user_model", attributes, options);
return UserModel;
};
这样我们就可以在项目中直接使用了:
const Sequelize = require('sequelize');
const UserModel = require('./models/user');
// Option 1: Passing parameters separately
const sequelize = new Sequelize('database', 'username', 'password', {
host: 'localhost',
dialect: /* one of 'mysql' | 'mariadb' | 'postgres' | 'mssql' | 'sqlite' */
});
const userModel = UserModel(sequelize);
const users = await userModel.findAll();
Sequelize-Automate 使用
sequelize-automate 提供了 sequelize-automate
这个命令,可以全局安装也可以只在项目中安装。
全局安装
首先需要安装 sequelize-automate
$ npm install -g sequelize-automate
然后还需要安装使用的数据库对应的依赖包,这点与 sequelize 一致:
# 根据你使用的数据库,从下面的命令中选一个安装即可
$ npm install -g pg pg-hstore # Postgres
$ npm install -g mysql2
$ npm install -g mariadb
$ npm install -g sqlite3
$ npm install -g tedious # Microsoft SQL Server
之所以这样设计,是因为如果我使用的是 MySQL,则我只需要安装 mysql2
(sequelize 使用 mysql2 操作 MySQL 数据库),不需要也没必要安装其他包。
仅在项目中安装
可能你不喜欢全局安装 sequelize-automate
,因为还需要全局安装 mysql2
或其他依赖,或者你可能只想在某个项目中使用它,则可以仅在项目中安装使用:
$ cd your_project_dir
$ npm install sequelize-automate --save
然后同样需要安装对应的数据库依赖包:
# 根据你使用的数据库,从下面的命令中选一个安装即可
$ npm install --save pg pg-hstore # Postgres
$ npm install --save mysql2
$ npm install --save mariadb
$ npm install --save sqlite3
$ npm install --save tedious # Microsoft SQL Server
当然,如果你已经在项目中使用了 sequelize,则一定会安装一个对应的数据库依赖包。
安装成功后,你就可以在项目目录中通过 ./node_modules/.bin/sequelize-automate
使用 sequelize-automate
了 。然而我们经常不会这样做。
推荐的做法是,在 package.json
中添加一个 script
:
"scripts": {
"sequelize-automate": "sequelize-automate"
},
这样就可以通过 npm run sequelize-automate
来间接执行项目中的 sequelize-automate
这个命令。
sequelize-automate
命令详解
sequelize-automate
命令支持的参数主要有:
-
--type, -t
指定 models 代码风格,当前可选值:js
ts
egg
midway
-
--dialect, -e
数据库类型,可选值:mysql
sqlite
postgres
mssql
mariadb
-
--host, -h
数据库 host -
--database, -d
数据库名 -
--user, -u
数据库用户名 -
--password, -p
数据库密码 -
--port, -P
数据库端口,默认:MySQL/MariaDB 3306,Postgres 5432,SSQL: 1433 -
--output, -o
指定输出 models 文件的目录,默认会生成在当前目录下models
文件夹中 -
--camel, -C
models 文件中代码是否使用驼峰发命名,默认false
-
--emptyDir, -r
是否清空 models 目录(即-o
指定的目录),如果为true
,则生成 models 之前会清空对应目录,默认false
-
--config, -c
指定配置文件,可以在一个配置文件中指定命令的参数
更详细的参数介绍可参考文档:sequelize-automate 。
使用示例
全局命令:
sequelize-automate -t js -h localhost -d test -u root -p root -P 3306 -e mysql -o models
如果在项目中使用的话,则可以将改命令添加到 package.json 中:
"scripts": {
"sequelize-automate": "sequelize-automate -t js -h localhost -d test -u root -p root -P 3306 -e mysql -o models"
},
然后通过 tnpm run sequelize-automate
来自动生成 models。
指定配置文件
因为命令的参数较多,所以支持了在 JSON 配置文件中指定参数。
首先需要创建一个配置文件,比如在当前目录下新建名为 sequelize-automate.config.json
的配置文件:
{
"dbOptions": {
"database": "test",
"username": "root",
"password": "root",
"dialect": "mysql",
"host": "localhost",
"port": 3306,
"logging": false
},
"options": {
"type": "js",
"dir": "models"
}
}
当然也可以使用 JS 文件:
module.exports = {
dbOptions: {
database: "test",
username: "root",
password: "root",
dialect: "mysql",
host: "localhost",
port: 3306,
logging: false
},
options: {
type: "js",
dir: "models"
}
}
然后就可以通过 sequelize-automate -c sequelize-automate.config.json
来使用。
配置文件中主要有 dbOptions
和 options
两个对象。
dbOptions
dbOptions
与 sequelize 构造函数 的参数完全一致,是数据库相关信息。sequelize-automate
将会以 dbOptions
为参数去创建一个 Sequelize 实例,详见:src/index.js#L43。
这里简单列举 dbOptions
的部分属性:
dbOptions: {
database: 'test',
username: 'root',
password: 'root',
dialect: 'mysql',
host: '127.0.0.1',
port: 3306,
define: {
underscored: false,
freezeTableName: false,
charset: 'utf8mb4',
timezone: '+00:00',
dialectOptions: {
collate: 'utf8_general_ci',
},
timestamps: false,
},
};
通常我们会用到的就是 database
username
password
dialect
host
port
。
options
options
是 sequelize-automate
本身的一些配置。主要有如下属性:
options: {
type: 'js', // 指定 models 代码风格
camelCase: false, // Models 文件中代码是否使用驼峰发命名
fileNameCamelCase: true, // Model 文件名是否使用驼峰法命名,默认文件名会使用表名,如 `user_post.js`;如果为 true,则文件名为 `userPost.js`
dir: 'models', // 指定输出 models 文件的目录
typesDir: 'models', // 指定输出 TypeScript 类型定义的文件目录,只有 TypeScript / Midway 等会有类型定义
emptyDir: false, // 生成 models 之前是否清空 `dir` 以及 `typesDir`
tables: null, // 指定生成哪些表的 models,如 ['user', 'user_post'];如果为 null,则忽略改属性
skipTables: null, // 指定跳过哪些表的 models,如 ['user'];如果为 null,则忽略改属性
tsNoCheck: false, // 是否添加 `@ts-nocheck` 注释到 models 文件中
}
所有参数可以参考源码:src/index.js#L13。
这里补充一点,之所以有 tsNoCheck
属性,是因为 Sequelize 的类型定义中,不支持 type: DataTypes.INTEGER(255)
这种写法,只支持 `type: DataTypes.INTEGER
。这样在 TypeScript 中就会报错。所以添加了 tsNoCheck
属性,如果为 true
,则会自动在 model 文件头部添加 @ts-nocheck
,如:
// @ts-nocheck
import { IApplicationContext, providerWrapper } from 'midway';
import { DataTypes } from 'sequelize';
import { IDB } from './db';
export default async function setupModel(context: IApplicationContext) {
const db: IDB = await context.getAsync('DB');
const attributes = {
id: {
type: DataTypes.BIGINT.UNSIGNED,
allowNull: false,
defaultValue: null,
primaryKey: true,
autoIncrement: true,
comment: '主键',
field: 'id',
},
name: {
type: DataTypes.STRING(100),
allowNull: false,
defaultValue: null,
primaryKey: false,
autoIncrement: false,
comment: null,
field: 'name',
},
};
const options = {
tableName: 'flow',
comment: '',
indexs: [],
};
return db.sequelize.define('userModel', attributes, options);
}
providerWrapper([{
id: 'UserModel',
provider: setupModel,
}]);
API
上面主要讲了 sequelize-automate 的命令行使用方式,sequelize-automate 本身也提供了接口,让使用者自定义开发。主要有两个:
-
automate.getDefinitions
将数据库表转换为 JSON -
automate.run
生成 models 代码
使用方法如下:
const Automate = require('sequelize-automate');
// dbOptions 和 options 前面已经提到,这里不再赘述
const dbOptions = {
// ...
};
const options = {}
// ...
}
// 创建一个 automate 实例
const automate = new Automate(dbOptions, options);
(async function main() {
// // 获取 Models JSON 定义
// const definitions = await automate.getDefinitions();
// console.log(definitions);
// 或生成代码
const code = await automate.run();
console.log(code);
})()
Sequelize-Automate 的实现
sequelize-automate 的实现思路很简单,就是首先从数据库中查询到所有表信息,包括表结构、索引、外键等,然后将表信息转换为一个 JSON 定义,最后使用 AST 根据 JSON 定义去生成代码。
获取表信息
查询表信息依赖了 sequelize 的一些方法,这也是为什么 sequelize-automate 依赖了 sequelize,并且有个参数是 dbOptions
。用到的相关 API 主要是:
- QueryInterface.showAllTables
- QueryInterface.describeTable
- QueryInterface.showIndex
- QueryInterface.getForeignKeyReferencesForTable
很多 API 在 Sequelize 的文档中并没有写出来,我也是看了它的源码才找到。
Sequelize 做的比较好的一点,就是对开发者屏蔽了不同数据库之间的差异,比如所有使用 this.queryInterface.showIndex
的返回值都是一样的格式。当然,有些 API 它并未完全做到,比如 showAllTables
,有的数据库返回是 [ 'tableName' ]
,而有的数据库返回 { tableName, schema }
,详见 sequelize#11451。
查询并聚合后的表信息如下:
{
"user":{
"structures":{
"id":{
"type":"INT(11) UNSIGNED",
"allowNull":false,
"defaultValue":null,
"primaryKey":true,
"autoIncrement":true,
"comment":"primary ket"
},
"name":{
"type":"VARCHAR(100)",
"allowNull":false,
"defaultValue":null,
"primaryKey":false,
"autoIncrement":false,
"comment":"user name"
},
"email":{
"type":"VARCHAR(255)",
"allowNull":false,
"defaultValue":null,
"primaryKey":false,
"autoIncrement":false,
"comment":"user email"
},
"created_at":{
"type":"DATETIME",
"allowNull":false,
"defaultValue":null,
"primaryKey":false,
"autoIncrement":false,
"comment":"created datetime"
},
"updated_at":{
"type":"DATETIME",
"allowNull":false,
"defaultValue":null,
"primaryKey":false,
"autoIncrement":false,
"comment":"updated datetime"
}
},
"indexes":[
{
"primary":true,
"fields":[
{
"attribute":"id",
"order":"ASC"
}
],
"name":"PRIMARY",
"tableName":"user",
"unique":true,
"type":"BTREE"
},
{
"primary":false,
"fields":[
{
"attribute":"name",
"order":"ASC"
}
],
"name":"uk_name",
"tableName":"user",
"unique":true,
"type":"BTREE"
}
],
"foreignKeys":[
]
},
"user_post":{
"structures":{
"id":{
"type":"INT(11) UNSIGNED",
"allowNull":false,
"defaultValue":null,
"primaryKey":true,
"autoIncrement":true,
"comment":"primary key"
},
"user_id":{
"type":"INT(11) UNSIGNED",
"allowNull":false,
"defaultValue":null,
"primaryKey":false,
"autoIncrement":false,
"comment":"user id"
},
"title":{
"type":"VARCHAR(255)",
"allowNull":false,
"defaultValue":null,
"primaryKey":false,
"autoIncrement":false,
"comment":"post title"
},
"content":{
"type":"TEXT",
"allowNull":true,
"defaultValue":null,
"primaryKey":false,
"autoIncrement":false,
"comment":"post content"
},
"created_at":{
"type":"DATETIME",
"allowNull":false,
"defaultValue":null,
"primaryKey":false,
"autoIncrement":false,
"comment":"created datetime"
},
"updated_at":{
"type":"DATETIME",
"allowNull":false,
"defaultValue":null,
"primaryKey":false,
"autoIncrement":false,
"comment":"updated datetime"
}
},
"indexes":[
{
"primary":true,
"fields":[
{
"attribute":"id",
"order":"ASC"
}
],
"name":"PRIMARY",
"tableName":"user_post",
"unique":true,
"type":"BTREE"
},
{
"primary":false,
"fields":[
{
"attribute":"user_id",
"order":"ASC"
}
],
"name":"fk_user_id",
"tableName":"user_post",
"unique":false,
"type":"BTREE"
}
],
"foreignKeys":[
{
"constraint_name":"fk_user_id",
"constraintName":"fk_user_id",
"constraintSchema":"test",
"constraintCatalog":"test",
"tableName":"user_post",
"tableSchema":"test",
"tableCatalog":"test",
"columnName":"user_id",
"referencedTableSchema":"test",
"referencedTableCatalog":"test",
"referencedTableName":"user",
"referencedColumnName":"id"
}
]
}
}
处理 Models JSON 定义
得到表信息后,就需要将表信息转换为 sequelize models 的定义。比如:将 "type":"INT(11) UNSIGNED"
转换为 "type":"DataTypes.INTEGER(11).UNSIGNED"
、处理索引、处理外键等。并且还涉及到不同数据库之间的差异,比如 MySQL 的自增,只需要设置 AUTO_INCREMENT
即可,而 PostgreSQL 则是通过 serial
实现,将 defaultValue 设置为 extval(my_data_id_seq::regclass)
,详见:sequelize-automate#9。
最终得到的 models 定义也就是 getDefinitions
返回的 JSON 如下所示:
[
{
"modelName":"user_model",
"modelFileName":"user",
"tableName":"user",
"attributes":{
"id":{
"type":"DataTypes.INTEGER(11).UNSIGNED",
"allowNull":false,
"defaultValue":null,
"primaryKey":true,
"autoIncrement":true,
"comment":"primary ket",
"field":"id"
},
"name":{
"type":"DataTypes.STRING(100)",
"allowNull":false,
"defaultValue":null,
"primaryKey":false,
"autoIncrement":false,
"comment":"user name",
"field":"name",
"unique":"uk_name"
},
"email":{
"type":"DataTypes.STRING(255)",
"allowNull":false,
"defaultValue":null,
"primaryKey":false,
"autoIncrement":false,
"comment":"user email",
"field":"email"
},
"created_at":{
"type":"DataTypes.DATE",
"allowNull":false,
"defaultValue":null,
"primaryKey":false,
"autoIncrement":false,
"comment":"created datetime",
"field":"created_at"
},
"updated_at":{
"type":"DataTypes.DATE",
"allowNull":false,
"defaultValue":null,
"primaryKey":false,
"autoIncrement":false,
"comment":"updated datetime",
"field":"updated_at"
}
},
"indexes":[
]
},
{
"modelName":"user_post_model",
"modelFileName":"user_post",
"tableName":"user_post",
"attributes":{
"id":{
"type":"DataTypes.INTEGER(11).UNSIGNED",
"allowNull":false,
"defaultValue":null,
"primaryKey":true,
"autoIncrement":true,
"comment":"primary key",
"field":"id"
},
"user_id":{
"type":"DataTypes.INTEGER(11).UNSIGNED",
"allowNull":false,
"defaultValue":null,
"primaryKey":false,
"autoIncrement":false,
"comment":"user id",
"field":"user_id",
"references":{
"key":"id",
"model":"user_model"
}
},
"title":{
"type":"DataTypes.STRING(255)",
"allowNull":false,
"defaultValue":null,
"primaryKey":false,
"autoIncrement":false,
"comment":"post title",
"field":"title"
},
"content":{
"type":"DataTypes.TEXT",
"allowNull":true,
"defaultValue":null,
"primaryKey":false,
"autoIncrement":false,
"comment":"post content",
"field":"content"
},
"created_at":{
"type":"DataTypes.DATE",
"allowNull":false,
"defaultValue":null,
"primaryKey":false,
"autoIncrement":false,
"comment":"created datetime",
"field":"created_at"
},
"updated_at":{
"type":"DataTypes.DATE",
"allowNull":false,
"defaultValue":null,
"primaryKey":false,
"autoIncrement":false,
"comment":"updated datetime",
"field":"updated_at"
}
},
"indexes":[
{
"name":"fk_user_id",
"unique":false,
"type":"BTREE",
"fields":[
"user_id"
]
}
]
}
]
而后续的 run
方法,就是根据该 JSON 去生成不同风格的 models 代码,如 JS、TS 或 Egg.js。当然,开发者也可以根据这份 JSON 定义,去生成别的风格的代码。
使用 AST 生成 models
得到 models 的 JSON 定义后,就可以根据定义生成 models。这个过程我选择的是用 AST,先生成 models 的 AST,然后根据 AST 生成代码。主要用到的工具有 @babel/parser
@babel/generator
@babel/types
@babel/traverse
。
比如生成字符串 "primary key"
的 AST:
const t = require('@babel/types');
const str = t.stringLiteral('Primary key');
// { type: 'StringLiteral', value: 'Primary key' }
生成对象 { comment: "primary key" }
的 AST:
const obj = t.objectProperty(t.identifier('comment'), t.stringLiteral("Primary key"));
/**
{
type: 'ObjectProperty',
key: { type: 'Identifier', name: 'comment' },
value: { type: 'StringLiteral', value: 'Primary' },
computed: false,
shorthand: false,
decorators: null
}
*/
然后就可以根据 AST 生成代码:
const generator = require('@babel/generator').default;
const code = generate(obj);
// { code: 'comment: "Primary"', map: null, rawMappings: null }
需要注意的是,如果要支持中文,则需要设置 jsescOption.minimal
为 true
,否则输出的是 unicode 字符:
const obj = t.objectProperty(t.identifier('comment'), t.stringLiteral("主键"));
const code1 = generate(obj);
{ code: 'comment: "\\u4E3B\\u952E"', map: null, rawMappings: null }
const code2 = generate(obj, {
jsescOption: {
minimal: true,
},
});
// { code: 'comment: "主键"', map: null, rawMappings: null }
总结
最开始写 sequelize-automate 是因为每次表结构修改了,都需要手动在代码里面修改 models ,修改起来非常繁琐而且容易写错,当表非常多的时候,写起来就更麻烦了。所以开发了这个小工具,能够让工具做的事情,就尽量让工具去做。
在写 sequelize-automate 之前,其实我也发现了 sequelize/sequelize-auto
也可以用来自动生成 models,但这个包已经几年没有更新了,使用的 sequelize 还是 3.30 版本,现在 sequelize 已经更新到 6.0 了;并且它还有很多 BUG 没有修复,很难使用起来。我也去看了它的代码,感觉很混乱,全是回调嵌套,难以维护。其生成代码也是用的字符串拼接的方式,没有 AST 先进、高端、准确且可预测。所以,毫不犹豫的选择并使用 sequelize-automate 吧!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。