2022.02 v2.2

因为春节长假的关系,以及部分研发时间投入在 Porter 项目,Leoric 的一月更新日志跳票到了二月。 Leoric 的功能迭代仍然在紧锣密鼓进行中,过去两个月的工作主要集中在字段类型增强、校验功能完善、以及兼容更多常用的 MySQL 特性上面。

二月份 Leoric 迎来两位新的贡献者 👏👏 @LB4027221@luckydrq

字段类型增强

TINYINT、SMALLINT、etc.

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)');

校验功能 & 缺省值

Validations

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 将相关校验逻辑提前,便于尽早发现问题。

DEFAULT VALUE

在之前的版本中,数据模型没有定义 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 修复了这一问题,在应用侧也将读取到正确的缺省值。

问题修复

UPDATE … ORDER … LIMIT

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 均不支持。

ORDER BY alias

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 默认值

之前版本的 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`)

类型补充

Realm 类型补充

v2.1.1 和 v2.2.0 分别完善了 new Realm()realm.connect()realm.define()等方法的类型定义,但预计仍然有许多其他问题,对于使用 TypeScript 编写 Egg 应用的用户,我们推荐参考 cnpm/cnpmcore 或者 eggjs/egg-orm!examples/typescript

[wip] 装饰器

Leoric 的类型定义还远未完善,为了进一步优化数据模型的类型声明,我们正在尝试使用装饰器来简化相关配置,目前大致思路是通过 class property 配合 @Column 装饰器完成 Model.attributes 配置:

import { DataTypes } from 'leoric';

const { MEDIUMINT } = DataTypes;

class Post extends Bone {
  @Column()
  id: bigint;

  @Column({ name: 'gmt_create' })
  createdAt: Date;

  @Column({ name: 'gmt_modified'})
  updatedAt: Date;

  @Column({ name: 'gmt_deleted' })
  deletedAt: Date;

  @Column(MEDIUMINT)
  wordCount: number;
}