因为春节长假的关系,以及部分研发时间投入在 Porter 项目,Leoric 的一月更新日志跳票到了二月。 Leoric 的功能迭代仍然在紧锣密鼓进行中,过去两个月的工作主要集中在字段类型增强、校验功能完善、以及兼容更多常用的 MySQL 特性上面。
二月份 Leoric 迎来两位新的贡献者 👏👏 @LB4027221 和 @luckydrq
v2.2.x 增加更多具体类型,例如 MySQL 中可能用到的 TINYINT、SMALLINT、或者 MEDIUMINT。这些类型在 SQLite 中名义上也支持,在 PostgreSQL 则只有 SMALLINT,我们在相应的数据库驱动中均做了兼容。使用方式和其他常见类型一致:
import { Bone, DataTypes } from 'leoric';
const { TINYINT, SMALLINT, BIGINT } = DataTypes;
export class User extends Bone {
static attributes = {
id: BIGINT,
age: SMALLINT,
sex: TINYINT(1),
}
}
对于 ActiveRecord 风格的使用方式,这次增加的具体类型也可以自动映射。例如,如果 users 表的 DDL 如下:
CREATE TABLE users (
id BIGINT PRIMARY KEY,
age SMALLINT DEFAULT 0 NOT NULL,
sex TINYINT(1) NOT NULL,
);
Leoric 从上述表结构初始化 User 数据模型时,对应的属性定义也会是具体的类型:
class User extends Bone {};
await connect({ database: 'foo', models: [ User ] });
assert.equal(User.attributes.sex.type.toSqlString(), 'TINYINT(1)');
https://github.com/cyjake/leoric/pull/266
| DB/DataType (validate or not in SQL operations) | INTEGER | DATE |
|---|---|---|
| MySQL | 🔲query ✅insert | ✅query ✅insert |
| SQLlite | 🔲query 🔲insert | 🔲query 🔲insert |
| PostgresSQL | ✅query ✅insert | ✅query ✅insert |
我们在 v2.0.2 版本中增强了校验逻辑,将按照所使用的数据库校验逻辑提前校验相关操作,例如在 MySQL 中,如下 SQL 都将触发异常:
mysql> SELECT now() = '2022-13-12';
ERROR 1525 (HY000): Incorrect DATETIME value: '2022-13-12'
mysql> SELECT * FROM articles WHERE gmt_create >= '2022-13-12';
ERROR 1525 (HY000): Incorrect TIMESTAMP value: '2022-13-12'
mysql> INSERT INTO articles (gmt_create) VALUES ('2022-13-12');
ERROR 1292 (22007): Incorrect datetime value: '2022-13-12' for column 'gmt_create' at row 1
v2.0.2 将相关校验逻辑提前,便于尽早发现问题。
在之前的版本中,数据模型没有定义 Model.attributes,由 Leoric 在启动时从数据库初始化字段信息时,Leoric 不会设置字段的缺省值,这么做的好处是生成的插入、更新 SQL 可以在数据库执行时再补充剩余信息,但坏处是如果遇到下面这种表:
CREATE TABLE users (
id BIGINT PRIMARY KEY,
level INTEGER DEFAULT VALUE 0 NOT NULL
);
然后 User 数据模型中没有专门声明字段信息:
class User extends Bone {}
const user = new User();
assert.equal(user.level, 0); // user.level -> null
v2.0.4 修复了这一问题,在应用侧也将读取到正确的缺省值。
MySQL 支持 UPDATE ... ORDER ... LIMIT 或者 DELETE ... ORDER ... LIMIT 等操作,在 Leoric v2.x 之前的版本中并没有完整支持这一特性,例如下面这种代码:
await User
.where({ status: STATUS_BLOCKED })
.update({ nickname: '*blocked*' })
.order('id', 'desc')
.limit(10);
// 更新最近十个被屏蔽用户的昵称
在之前的版本中会漏掉 ORDER 和 LIMIT 部分,v2.0.2 版本修复了这一问题;v2.1.0 则修复了 DELETE 的对应逻辑。给 UPDATE 和 DELETE 排序或者限定条数是 MySQL 特性,PostgreSQL 或者 SQLite 均不支持。
v2.0.2 修复聚合查询排序时用到别名会报错的问题,类似如下用法:
await User.count().group('sex').having('count > 0').order('count', 'desc');
如果使用 Sequelize 写法,大致是:
await User.find({
attributes: sequelize.fn('COUNT', '*'),
group: 'sex',
having: {
count: { $gt: 0 },
},
order: [[ 'count', 'desc' ]],
});
之前版本的 upsert 在执行时不会默认插入 createdAt:
Post.upsert({ title: 'yes' });
更新之前生成的 SQL:
INSERT INTO `posts`
(`title`, `updated_at`)
VALUES
('yes', `CURRENTTIME`)
ON DUPLICATE KEY UPDATE
`title` = VALUES(`title`),
`updated_at` = VALUES(`updated_at`)
这会导致 upsert 插入新数据时会可能不会插入 createdAt,导致字段缺失或者数据库报错
修复后生成 SQL 为:
INSERT INTO `posts`
(`title`, `created_at`, `updated_at`)
VALUES
('yes', `CURRENTTIME`, `CURRENTTIME`)
ON DUPLICATE KEY UPDATE
`title` = VALUES(`title`),
`updated_at` = VALUES(`updated_at`)
v2.1.1 和 v2.2.0 分别完善了 new Realm()、realm.connect()、realm.define()等方法的类型定义,但预计仍然有许多其他问题,对于使用 TypeScript 编写 Egg 应用的用户,我们推荐参考 cnpm/cnpmcore 或者 eggjs/egg-orm!examples/typescript
为了进一步优化数据模型的类型声明,我们引入了 TypeScript 装饰器支持。装饰器现已完全实现,提供了更简洁的模型属性和关联定义方式。
@Column()@Column() 装饰器替代了静态 attributes 定义。它支持从 TypeScript 类型自动推断数据类型,也支持显式覆盖:
import { Bone, Column, DataTypes } from 'leoric';
const { MEDIUMINT, TEXT } = DataTypes;
class Post extends Bone {
@Column({ primaryKey: true, autoIncrement: true })
id: bigint;
@Column({ allowNull: false })
title: string;
@Column(TEXT)
content: string;
@Column({ name: 'gmt_create' })
createdAt: Date;
@Column({ name: 'gmt_modified' })
updatedAt: Date;
@Column({ name: 'gmt_deleted' })
deletedAt: Date;
@Column(MEDIUMINT)
wordCount: number;
}
省略 type 选项时,@Column() 会从 TypeScript 类型推断数据类型:
| TypeScript 类型 | 推断的数据类型 |
|---|---|
bigint |
BIGINT |
number |
INTEGER |
string |
STRING / VARCHAR(255) |
Date |
DATE |
boolean |
BOOLEAN |
@BelongsTo()、@HasMany()、@HasOne()关联装饰器替代了 static initialize() 方法来定义关联关系:
import { Bone, Column, BelongsTo, HasMany, HasOne } from 'leoric';
import User from './user';
import Comment from './comment';
import Profile from './profile';
class Post extends Bone {
@Column({ primaryKey: true })
id: bigint;
@Column()
userId: bigint;
@BelongsTo()
user: User;
@HasMany()
comments: Comment[];
}
class User extends Bone {
@Column({ primaryKey: true })
id: bigint;
@HasMany()
posts: Post[];
@HasOne()
profile: Profile;
}
如果外键不符合命名约定,可以显式指定:
class Post extends Bone {
@BelongsTo({ foreignKey: 'authorId' })
user: User;
}
对于多对多关联,在 @HasMany() 中使用 through 选项:
class Post extends Bone {
@HasMany({ through: 'tagMaps' })
tags: Tag[];
@HasMany()
tagMaps: TagMap[];
}
使用装饰器需要在 tsconfig.json 中设置以下选项:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
完整详情请参阅 TypeScript 支持 文档。