Programming Leoric I

2016 年初由于工作所需,我开发了一个比较粗浅的模块,用来映射 MySQL 表到 JavaScript 类,取名 xx-orm。两年后,Node.js 社区的 ORM 方案仍然是五花八门,在 npmjs.com 搜 orm 能让人挑花眼。我把 xx-orm 从应用代码中剥离出来,取名 Leoric Leoric,为这场混战添一把柴火。

Leoric 最朴素的需求其实很简单,是为了做字段名 column name 到属性名 attribute name 的映射。因为 DBA 通常使用 snake_case 来表达数据库名、表名、字段名,但是 JavaScript 默认的代码风格又是 camelCase 的。所以 Leoric 做的第一件事情就是映射字段名和属性名:

column name attribute name
foo foo
foo_bar fooBar

另一件事情,则是 Leoric 目前的一个 feature 或者 bug,无需 Model 属性定义以及 Migration。在开发 xx-orm 时,我们的表结构设计都是通过数据库服务所提供的设计工具进行。待设计完成后,只需继承 Bone 然后 connect 数据模型和数据库:

const { Bone, connect } = require('Leoric')
class User extends Bone {}
connect({ client: 'mysql', models: [User] })

这样 User 就可以用了,所有 users 表中的字段信息都会被自动导入:

User.columns
// => ['id', 'name', 'age', 'created_at', ... ]
User.attribtes
// => ['id', 'name', 'age', 'createdAt', ... ]

User 的使用者只需要关心映射后的属性名 attribute name。可以阅读《Leoric 基础》一文了解更多相关内容。

在这个朴素需求之上,Leoric 的绝大多数特性都是借鉴 Active Record 的,包括但不限于查询、关联关系的 API 设计。但对于熟悉 Node.js 但不了解 Ruby on Rails 的程序员来说,前面这句不会给人直观印象,所以下文将 Leoric 与 Node.js 流行的 ORM 库做个比较。

目前社区中最成熟的方案大致是 Sequelize、Bookshelf(大多数会直接用它底层的 Knex) 、还有 sails 框架所包含的 Waterline。Waterline 是一个志在兼收并蓄的模块,不仅能够映射关系型数据库,还可以把底层存储换成文件系统、Redis 等等。功能太过强大,惹不起惹不起,这里就不深入讨论了。

Leoric 的主要比较对象是 Sequelize、Knex。从查询说起,假设我们需要查询 WHERE (foo IS NULL OR foo = 1) AND deleted_at IS NULL,在 Sequelize 里:

Table.findAll({
  where: {
    [Op.and]: [
      { [Op.or]: [
        { foo: null },
        { foo: 1 } ] },
      { deletedAt: null }
    ]
  }
})

在 Knex 里:

Table.where(function() {
  this.where({ foo: null }).orWhere({ foo: values })
}).andWhere({ deletedAt: null })

用 Leoric,则是:

Table.find('(foo = null or foo = ?) and deletedAt is null', 1)

其实 Sequelize 这种查询方式 Leoric 也支持,但作为一个曾经的 Ruby on Rails 小粉丝,我还是认为 SQL-like 的表达方式是最合适的。注意这里传入的字符串并不会直接被放到 WHERE,而是会被解析、过滤,最后再拼到 SQL 中去。

如果认为 placeholder 形式不够直观,也可以用 tagged template literal

Table.find`(foo = null or foo = ${foo}) and deletedAt is null`

可惜这个我所认为的优势并不被 cnodejs.org 所认同。如果你仍然对 Leoric 的查询 API 感兴趣,不妨阅读《Leoric 查询接口》一文。

另一个比较方便的是关联关系的处理。使用 Leoric,我们可以在 Model 中声明多种映射关系

const { Bone } = requier('Leoric')
class Comment extends Bone {}
class User extends Bone {}
class Post extends Bone {
  static describe() {
    this.hasMany('comments')
    this.belongsTo('author', { className: 'User', foreignKey: 'authorId' })
    this.hasMany('tagMaps')
    this.hasMany('tags', { through: 'tagMaps' })
  }
}

查询的时候就可以一次取出:

Post.include('comments', 'author', 'tags').where('posts.id = ?', [8, 24])

上面这种关联关系,使用 Sequelize 表示,可能是这样的:

const Post = sequelize.define('post', { ... })
const Comment = sequelize.define('comment', { ... })
const User = sequelize.define('user', { ... })
Post.belongsTo(User, { as: 'author', foreignKey: 'authorId' })
Post.hasMany(Comment)
Post.hasMany(TagMap)
Post.hasMany(Tag, { through: 'TagMap' })

使用 Sequelize 查询的时候:

Post.findAll({
  include: [
    { model: Comment },
    { model: User },
    { model: Tag,
      through: { /* ? */ } }
  ],
  where: {
    id: { $in: [8, 24] }
  }
})

一些基于 Knex 的 ORM 也支持关联关系,但使用的语法比 Sequelize 还要繁琐,这里就不深入讨论了。在关联关系定义上,Leoric 和 Sequelize 相差不多,Leoric 的 API 更现代化一些。而在查询的时候,Leoric 则要简洁许多,毕竟 Leoric 懂得你的查询表达式