Discover 是一个 Node.js 平台上的 Mysql ORM.
需要 CoffeeScript ≥ v0.12.x 编译源码, 且部分语法依赖 Node.js ≥ v8.x 版本.
- mysqld
- memcached
- 增加
persist()
方法 - 增加
find_and_update()
,find_and_delete()
方法 - 增加
to_model()
方法
- 异步 API 全部返回 Promise, 不再支持 callback (除了 hook functions 之外).b
- 创建模型 schema 的方法变为
define_schema
- name 作为 column 的符号链接
- 驼峰命名 -> 蛇形命名
- schema_pattern 中的
tableName
属性名变为name
find_*
方法中的可选参数options
中的order_by
值从column
变为{ column, order }
.Schema.update
和Schema.delete
返回的 Promise 的 resolve 函数参数为 一个 二元数组, 包含旧模型的 attributes, 以及新的模型, 而不是之前包含 两个 参数.- 引用内部类的方法也有所差异, 参看 类与模块 一章的第一节.
在新版本的 Discover 中, 所有的类和模块已经通过入口暴露出来, 允许基于其上二次开发.
Discover
类是整个库的入口, 暴露了一系列内部类以及一个实例方法.
Discover = require "discover"
discover = new Discover(database_config, [cache_config])
最常规的用法就是像上面这样通过实例化一个 Discover
, 绑定底层资源到这个 discover 实例上.
User = discover.define_schema(schema_pattern)
随后调用define_schema
方法创建一个 新的 Mixed
类, 它就是一个 schema, 我们将用它来创建数据模型. 如上所示, 创建了一个 User
schema.
user_james = new User(attributes)
上面的示例创建了一个 Model
类实例, 也就是数据模型. 我们可以在它上面做 CURD 操作.
其它内部类作为 Discover
的子类, 挂载到 Discover 上:
{ Query, DataBase, Cache, Table, Type, Operator, Schema, Model } = require "discover"
参见 https://github.com/mysqljs/mysql connection options
{
servers: 参见 https://github.com/3rd-Eden/memcached 的 server location
options: 参见 https://github.com/3rd-Eden/memcached 的 options
}
schema_pattern 用于配置一个 Schema
{
name: "mysql 表名字" # String
fields: [ # Array
{
column: "列名"
pk: Boolean # 是否是主键, 可选
auto: Boolean # 是否是, 可选
secret: Boolean # 是否是保密字段, 可选
unique: Boolean # 是否是唯一的, 可选
type: "值的类型"
default: # 默认值, 可选
}
...
]
indices: [ # 可选 Array
# 配置同 fields
]
[custom_method_name]: -> # 可以配置自定义方法
}
attributes 用于生成一个数据模型, 填充 Schema 中对应的 column.
{
column_a: value_a
column_b: value_b
...
}
DataBase
类里直接操作 mysql driver, 通过它实现资源的池化, 连接的创建, 销毁; 以及执行 SQL 语句.
{ DataBase } = require "discover"
cfg =
host: "127.0.0.1"
user: "root"
password: ""
database: "test"
database = new Database cfg
database.query "SELECT * FROM test"
.then ([row]) -> console.log row
.catch (err) -> console.error err
database.next_sequence "name"
.then (id) -> console.log id
.catch (err) -> console.error err
Cache
类里直接操作 memcached driver, 提供了添加/删除/读取的三个接口.
{ Cache } = require "discover"
cfg = servers: "127.0.0.1:11211"
cache = new Cache cfg
cache.set "m", {a: b: 3}, 0
.then (r) ->
console.log r
cache.set "n", {a: 5}, 0
.catch (err) -> console.error "set_err:", err
.then -> cache.get ["m", "n"]
.then (r) -> console.log r
.catch (err) -> console.error "get_err:", err
.then -> cache.del "m"
.then (r) -> console.log r
.catch (err) -> console.error "del_err:", err
Query
类是 Discover 的 SQL 语法的对象表示, 允许以 JavaScript 链式调用构建 SQL 查询语句, 是 Discover 逻辑的核心.
{ Query } = require "discover"
console.log (new Query Schema).select().where({}).to_sql()
console.log (new Query Schema).select().where({ id: 1, col: { op: "like", value: "sss" } }).orderby("id").limit(5, 10).to_sql()
console.log (new Query Schema).select().where({ id: 1, col: { op: "like", value: "sss" } }).orderby({ column: "id", order: 'desc'}).limit(10).to_sql()
console.log (new Query Schema).id().where({ id: 1, col: { op: "like", value: "sss" } }).orderby({ column: "id", order: 'desc'}).limit(10).to_sql()
console.log (new Query Schema).count().where({ id: 1, col: { op: "like", value: "sss" } }).orderby({ column: "id", order: 'desc'}).limit(10).to_sql()
console.log (new Query Schema).max("id").where({ id: 1, col: { op: "like", value: "sss" } }).orderby({ column: "id", order: 'desc'}).limit(10).to_sql()
console.log (new Query Schema).sum(["id", "name"]).where({ id: 1, col: { op: "like", value: "sss" } }).orderby({ column: "id", order: 'desc'}).limit(10).to_sql()
(new Query Schema).update().set({ name: "ooq" }).where({ name: { op: "like", value: "elasticsearch" } }).execute()
Operator
类作为 ooq
模块的 FFI 而存在. qengine 在语义分析时会调用相应 Operator
, 并翻译生成 SQL 的 WHERE
从句.
Operator
类暴露了如下 FFI:
and: (args...) -> new Operator.And args
or: (args...) -> new Operator.Or args
not: (args...) -> new Operator.Not args
xor: (args...) -> new Operator.Xor args
like: (column, value) -> new Operator column, "like", value
eq: (column, value) -> new Operator column, "=", value
neq: (column, value) -> new Operator column, "<>", value
gt: (column, value) -> new Operator column, ">", value
gte: (column, value) -> new Operator column, ">=", value
lt: (column, value) -> new Operator column, "<", value
lte: (column, value) -> new Operator column, "<=", value
isNull: (column) -> new Operator.Null column
isNotNull: (column) -> new Operator.NotNull column
(见后文)
(见后文)
Table
类用于维护映射到 mysql 中对应表的元信息, 包括列名, 列属性, 值类型, 主键, 默认值等等.
Type
类被 Table
类所使用, 表示一个列的类型. 当实例化一个 table 时, 每个列都会被 装箱 成一个 Type
的实例, 提供了值的序列化和提取的方法.
目前支持的类型包括:
"raw"
"int"
"str"
(alias to "string")"string"
"json"
"double"
"float"
(alias to "double")"date"
(alias to "datetime")"datetime"
(见后文)
Discover = require ".."
# 数据库配置
dbcfg =
host: "127.0.0.1"
user: "root"
password: ""
database: "test"
# 缓存配置(可选)
cachecfg = servers: "127.0.0.1:11211"
discover = new Discover dbcfg, cachecfg
# 创建一个 User Schema
User = discover.define_schema
name: "user"
fields: [
{ column: "id", type: "int", pk: yes, auto: yes }
{ column: "name", type: "string", default: "hero of the strom" }
{ name: "age", type: "int", secure: yes }
{ column: "comments", type: "json" }
{ column: "remark", type: "double" }
{ column: "last_login", type: "datetime" }
]
indices: [
{ column: "name", type: "string" }
]
foo: -> null
bar: -> null
# 创建 User Model
user = new User name: "kafka", age: 10, comments: ["good", "bad", "foo", "bar"], remark: 0.98, last_login: new Date()
User.persist()
.then ->
User.all()
.then (models) ->
# console.log models.length
User.count()
.then (count) ->
# console.log count
User.find age: { op: "gte", value: 5 }, { limit: 3, order_by: { column: "id", order: "desc" }, page: 2 }
.then (models) ->
# console.log models
User.find_one age: { op: "gte", value: 5 }
.then (model) ->
# console.log model
User.find_with_count age: { op: "gte", value: 5 }
.then ({ models, total }) ->
# console.log models, total
User.find_by_index "id", 10
.then (models) ->
# console.log models
User.find_by_unique_key "id", 11
.then (model) ->
# console.log model
User.find_by_id id: 20
.then (model) ->
# console.log model
User.find_by_ids [2..4]
.then (models) ->
# console.log models
User.find_and_update { age: { op: "gte", value: 5 } }, { name: "flink", age: 6 }
.then ({ updates }) ->
# console.log "updates:", updates
User.find_and_delete id: 2
.then ({ deletes }) ->
# console.log "deletes:", deletes
User.insert user
.then (model) ->
console.log model is user
model.name = "xxx"
# console.log user.is_changed "name"
# console.log user.changed_attributes()
# console.log user._previous_attributes
# console.log user.previous "name"
User.update model
.then ([oldstates, new_model]) ->
# console.log oldstates, new_model
# console.log user.is_changed "name"
# console.log user.changed_attributes()
# console.log user._previous_attributes
# console.log user.previous "name"
console.log new_model is user
user.age = 1
console.log user.is_changed "age"
console.log user.changed_attributes()
console.log user._previous_attributes
console.log user.previous "age"
user.update()
.then ([oldstates, new_model]) ->
console.log oldstates
.catch (err) -> console.error err
Schema
类仅仅包含了一系列模型操作的类方法.
-
persist()
: {Promise} 在数据库中创建 Schema 对应的表 -
all([options])
: {Promise} 获取所有模型 -
count(condition: Object, [options])
: {Promise} 获取符合条件的模型数量 -
find(condition: Object, [options])
: {Promise} 标准的查找方法 -
find_one(condition: Object, [options])
: {Promise} -
find_with_count(condition: Object, [options])
: {Promise} 同时返回模型及数量 -
find_by_index(index_name, value, [options])
: {Promise} -
find_by_unique_key(key, value, [options])
: {Promise} -
find_by_id(id: String, [options])
: {Promise} -
find_by_id(id: Array(String), [options])
: {Promise} -
find_by_id(id: Object, [options])
: {Promise} -
find_by_ids(ids: Array(String), [options])
: {Promise} -
find_by_ids(ids: Array(Array(String)), [options])
: {Promise} -
find_by_ids(ids: Array(Object), [options])
: {Promise} -
find_and_update(condition: Object, modified: Object, [options])
: {Promise} -
find_and_delete(condition: Object, [options])
: {Promise} -
insert(model: Model)
: {Promise}, resolve 函数的参数为当前模型 -
update(model: Model)
: {Promise}, resolve 函数的参数为一个二元数组, 分别为修改之前的属性集合 oldstates 和当前模型 -
delete(model: Model)
: {Promise}, resolve 函数的参数为一个二元数组, 分别为修改之前的属性集合 oldstates 和当前模型 -
before(method_name: String, exec: Function)
: {Mixed} -
after(method_name: String, exec: Function)
: {Mixed} -
wrap(objects: Object, [options])
: {Array(Model)} -
wrap(objects: Array(Object), [options])
: {Array(Model)} -
load(id: String, key: String, [options])
: {Promise} -
load(id: Array(String), key: String, [options])
: {Promise} -
load(id: Object, key: String, [options])
: {Promise} -
clean_cache(value: Model)
: {Promise} -
clean_cache(value: Array)
: {Promise} -
clean_cache(value: Object)
: {Promise} -
walk(model: Model, prefix: String)
: {Array(Function)} -
is_valid(method_name: String)
: {Boolean} -
cache_key(key: Model)
: {String} -
cache_key(value: Array)
: {String} -
cache_key(value: Object)
: {String} -
to_model(data: Object)
: {Model}
用于配置查询
{
order_by: { column: String, order: "asc" | "desc" } # 依据哪个列按什么顺序排序, 可选
limit: Number # 返回数量限制, 可选
page: Number # 第几页, 可选
disable_check: Boolean # 是否取消查询语法树剪枝, default: false, 默认会检查传入的查询语句并将不属于这个 Schema 的字段去掉
}
id 有三种类型:
String: 当一个 Schema 中只有一个 pk 时, 这个值就作为 pk 传入. Array(String): 当有多于一个 pk 时, 这组值按照 pks 定义的顺序依次映射. Object: key-value 分别为 pk和值
(见后文 查询语法)
Discover 允许在 Schema 上定义它数据模型的钩子函数, 他们将在插入, 更新或者删除前后依次执行.
Schema.before 'insert', ->
# 回调是一个 AsyncFunction
# `this` => 引用了当前操作的模型
# 异常/错误直接抛出
await async_operations()
# insert
Schema.after 'insert', ->
# 回调是一个 AsyncFunction
# `this` => 引用了当前操作的模型
# 异常/错误直接抛出
await async_operations()
# update
Schema.after 'update', (oldstates) ->
# 回调是一个 AsyncFunction
# `this` => 引用了当前操作的模型
# 异常/错误直接抛出
await
# delete
Schema.after 'delete', ->
# 回调是一个 AsyncFunction
# `this` => 引用了当前操作的模型
# 异常/错误直接抛出
await async_operations()
注1: 只允许对 insert
, update
, delete
三个方法设置 hooks
注2: hooks 和被 hook 的方法如果出现错误, 那么后续的任务就会挂起
注3: 所有的任务按照它们定义的顺序依次执行
Discover 支持数据域的修改校验, 你可以通过在 schema_pattern
中自定义方法或者直接在模型上添加**"validate"** 前缀方法来实现.
所有的校验程序会在 insert 和 update 调用前执行 (实际上, 是在所有的钩子函数之后, insert/update 方法之前).
定义一个校验方法:
user.validateFields = ->
# validation 函数是一个 AsyncFunction
# this -> model
# 不通过 validate 的直接抛出异常/错误即可
throw err if err = await async_validation()
};
之后, validateFields
会在调用 insert 或 update 时自动执行.
校验方法会按照它们定义的顺序依次调用.
Model
继承了 EventEmitter2
.
model = new Model(attributes)
attributes
为一个数据模型的 k-v 对象.
$schema
: {Schema} 关联的Schema
类attributes
: {Object} 数据域_oldstates
: {Object} 记录update()
方法调用之前的状态_previous_attributes
: {Object} 记录set()
方法调用之前的状态_changed
: {Boolean}set()
方法调用之前数据域是否发生变更_changing
: {Boolean} 当前是否正在进行数据域修改
insert()
: {Promise}update()
: {Promise}delete()
: {Promise}to_json(include_secure_fields: Boolean)
: {Object}has(column_name: String)
: {Boolean}get(column_name: String)
: {value}set(column_name: String, value, [options])
: {False | Model}set(attrs: Object)
: {False | Model}set(model: Model)
: {False | Model}reset()
: {Model}is_changed([attr: String])
: {Boolean}changed_attributes([current_attrs: Object])
: {Object}previous([attr: String])
: {value}
_update_attrs(new_attrs: Object)
: {null}_perform_validate(attrs: Object, [options])
: {Boolean}
混合模型是由调用 (new Discover db, cache).define_schema(schema_pattern)
方法返回得来的, 它就是我们一般所讲的 Data Model (数据模型).
为何称做混合模型? 因为 Mixed
类本身并不具备什么数据操作能力, 它是通过扩展完善的自身: 继承了 Model
类, 混入了 Schema
类. 因此它具备了 Schema
的静态方法以及 Model
的实例方法.
mixed_model = new Mixed(attributes)
$cache
:Cache
类的实例$database
:DataBase
类的实例$table
:Table
类的实例_before_hooks
: 维护 before hook 的对象_after_hooks
: 维护 after hook 的对象
(new Model).attributes [getter/setter]
: 直接通过Mixed
实例读写模型数据域.
其余同 Model
类.
find_by_${suffix}(column: String, args...)
:suffix
是column name
的蛇形下滑线命名方式. (注意: 这些方法只有在创建 Schema 时配置了indices
时可用)
其余同 Schema
类
同 Model
类
除了上述三类 API, 还有一些不太常用, 但可以用于编写 discover 扩展的内部 API, 它们是通过 Discover
类方法暴露给开发者的.
其实 Schema
类和 Model
类都属于内部类, 这里只是为了阐述 Mixed
模型才将它们单独提出来讲. 它们两个的 API 已经是经过 Mix 的, 因此在 Mixed
类及其实例中均可直接使用.
discover 自从 v0.3 起支持了 ooq
查询语法, 类似 mongodb 的 JSON 查询 DSL. 可以通过 Schema
类的 find_*
方法传入, 例如:
User.find {
name: "nerd"
$or :
age: { op: "gt", value: 19 }
hobbies:
$and: ["gaming", "programming"]
}
这会将查询条件翻译成 SQL 语句:
WHERE name = nerd AND (age > 19 OR (hobbies = "gaming" AND hobbies = "programming"))
$and
/ $or
/ $not
/ $xor
qengine 默认在 ooq 最外层封装一个 $and
, 因为它需要从一个逻辑运算符节点开始分析.
逻辑运算符的值可以是一个 JSON 对象, 表示不同 columns 及子逻辑节点之间的逻辑关系. 值也可以是一个数组, 这时, 这个逻辑运算符的父节点或祖先节点, 其中必须能向上追溯到一个 column, 这时表示对于同一个 column, 它可能的值之间的逻辑关系.
gt
/ gte
/ eq
/ lt
/ lte
/ neq
/ like
/ isNull
/ isNotNull
关系运算符一般用于 ooq 语法树的末端叶节点. 表示一个 column 的值的条件.
应用 eq
运算符的 column 可以简写:
column_a: { op: "eq", value: "dust" }
# 等价于
column_a: "dust"
qengine
库作为 Discover 的子模块实现了ooq-lang 的翻译过程, 规范及更多细节参见: https://github.com/upyun-dev/qengine