目前在 Egg.js 中,Model 层的编写比较混乱,没有一个较为统一的方案,各自 ORM 插件也各自为战,限制较多,多 ORM 共存时也存在较大的兼容性问题。
这里将要重新规划一下 Egg.js 的 Model 层方案,主要解决以下各问题:
本层规划主要由一个新的插件 egg-model 实现。它主要分以下几个功能。
若要使用 Egg.js 的 Model 层特性,则需要先将 egg-model 安装,并在插件配置中开启它:
// package.json
...
"dependencies": {
"egg-model": "...",
...
},
...
// config/plugin.js
...
exports.model = {
enable: true,
package: "egg-model"
};
...
Connection Loader 为 egg-model 中的一个子功能。主要用于配置和加载各连接源实例。关于连接源的概念请阅读下一节内容。
在应用中,首先开启 config/plugin.js 文件中相应连接源插件:
// package.json
...
"dependencies": {
"egg-sequelize": "...",
...
}
// config/plugin.js
exports.sequelize = {
enable: true,
package: "egg-sequelize"
};
...
然后在连接源配置(connection)中配置好实例,可多实例:
// config/config.default.js
exports.sequelize = {
// 一些 sequelize 的默认配置
host: "127.0.0.1"
};
exports.connections = {
main: {
adapter: "sequelize", // adapter 字段名可再议
// 其它配置
username: "root",
password: "temp"
},
foo: {
adapter: "sequelize",
// 其它配置
host: "10.10.10.10"
},
bar: {
adapter: "mongoose",
// 其它配置
...
}
};
...
在 Egg.js 启动时,egg-model 开始加载的时候将会遍历 connections 配置,并将所有配置好的连接源加载好,挂载到 app.connection 对象下,如:
const mainConn = app.connection.main;
// 定义一个 Model
const TestModel = mainConn.define(...);
在 Egg.js 的 Model 层概念披露后,将引入 Connection 概念,可理解为连接源,某种意义上可以理解为数据源,数据源为连接源的子集。
连接源包括但不局限于各种 ORM 插件,这些插件都有可能成为 Egg.js Model 层的连接源:
以上各插件若已存在于生态中,则将会发布一个 Breaking 的大版本,或者命名一个新的插件。
上面的插件中,都是 Model 的连接源,其中 egg-sequelize、egg-mongoose、egg-toshihiko 又是数据源。
每个连接源插件的连接源实例都要实现自身的一些方法,如 egg-sequelize 需要实现定义 Model 的方法 .define()。还有重要的一点就是,连接源插件要实现一个自身往连接源加载器注册自身成为一个 Adapter 的函数,初步预想如下:
class SequelizeWrapper {
constructor(database, username, password, options) {
this.sequelize = new Sequelize(database, username, password, options);
}
define(name, columns) {
const m = this.sequelize.define(name, columns);
const proto = Object.keys(m);
return class ModelWrapper {
constructor(ctx) {
this.model = m;
this.ctx = ctx;
for(key of proto) {
if(typeof proto[key] !== "function") continue;
this[key] = function(...) {
this.ctx....;
m[key](...);
};
}
}
};
}
};
app.connection.register("sequelize", function(options) {
const m = new SequelizeWrapper(options.database, options.username, options.password, options);
});
首先会有一个 SequelizeWrapper 基类,egg-sequelize 将自身注册进 Connection Loader 的时候工厂函数返回相应的一个 SequelizeWrapper 实例,这个实例将会在 Model 定义的时候被用到;在 Model 定义的时候,执行的是这个 SequelizeWrapper 实例的 .define() 函数,返回一个 ModelWrapper,这个类将在每次请求生命周期被实例化一次,并带有 ctx 信息,最后将被挂载到请求上下文中。
如上一节中看到的 TestModel 实际上就是一个 ModelWrapper 的类。
在应用中如果启用了 egg-model 插件,则插件在加载时期会递归遍历 app/model/ 目录,并将其导出的类缓存在自身插件内部。在请求周期会将模型实例化到 ctx 中去。
所有的模型都将以类的形式存在于 app/model/ 目录下,并且可接受 ctx 且只接受对象。如这将是一个合法的模型:
// app/model/foo.js
class FooModel {
constructor(ctx) {
this.ctx = ctx;
}
getData() {
return "hello world";
}
};
module.exports = FooModel;
如果是一个 Sequelize 插件模型,则 egg-sequelize 可能的一个实现方式如下:
// 插件中
class SequelizeWrapper {
...
define(name, columns) {
const Model = this.sequelize.define(name, columns);
return class EggSequelizeModel {
constructor(ctx) {
this.inner = Model;
this.ctx = ctx;
// 将 this.inner 中所有函数经包装后挂载到该类下
...
}
};
}
};
app.connection.register("sequelize", (options) => { return new SequelizeWrapper(options); });
// app/model/seq.js
const sequelize = app.connection.main; // 一个名为 main 的 SequelizeWrapper 实例
const Seq = sequelize.define("seq", { ... });
// 这里的 Seq 就是一个 EggSequelizeModel
Seq.prototype.findByDate = function* (date, limit) {
// ...
};
module.exports = Seq;
// 或者
const Base = sequelize.define("seq", { ... });
class Seq extends Base {
constructor(ctx) {
super(ctx);
}
findByDate(date, limite) {
// ...
}
};
module.exports = Seq;
在请求生命周期中,所有模型会以目录的形式被挂载到 ctx.model 对象中去。
如这是几个文件名与 ctx.model 模型对象的一个映射举例:
ctx.model.Foo; // app/model/foo.js
ctx.model.SeqFoo; // app/model/seq_foo.js 或者 app/model/seqFoo.js
ctx.model.Seq.Foo; // app/model/seq/foo.js
所以在日常使用中的例子如下:
const Foo = ctx.model.Foo;
const ret = yield Foo.findByDate(date, limit);
Connection 与 Model 都存在于 egg-model 插件中,但它们两个是独立的概念,只是交集的关系。
.define() 函数并不是强制的,鉴于各 ORM 的 API 本身就不同,而开发者在使用的时候自己当前在用什么 ORM,所以只需要看文档就可以。如 Sequelize 中可以叫 .define(),而另一个不知道什么 ORM 也许也可以叫 .createModel();ctx 的类就能是一个 Model;本次 RFC 主要为了统一 Egg.js 的 Model 层,有以下几点:
以下为老版。
目前在 Egg.js 中,Model 层的编写比较混乱,没有一个较为统一的方案,各自 ORM 插件也各自为战,限制较多,多 ORM 共存时也存在较大的兼容性问题。
这里将要重新规划一下 Egg.js 的 Model 层方案,主要解决以下各问题:
目前一个构思是,加载 Model 不交由各 ORM 自身的函数来加载,而是在 egg-core 中新增一个 Model Loader,自动遍历应用路径中的 model 目录(递归)并加载。
加载遵循以下几点:
name 字段,作为模型名,在项目中获取 Model 的时候作为唯一标识;name 字段,则以路径名做一些修改为模块名,如 "model/foo/bar.js" 将被识别为 "foo.bar";/ 将被替换为 .;在加载完后,可以在应用中通过函数(或者直接对象)获取,如:
const Bar = app.model.get('foo.bar');
// or
const Bar = app.model._.foo.bar; // 待议
脱离 ORM 这一层的考虑,我们将思路转变为 Egg.js 的 Model 层。
一些对外的长连接客户端(无论是关系型数据库、K-V 数据库、MongoDB 之类的 NoSQL,甚至是其它一些类似于 RocketMQ 等消息队列),都脱离 Plugin 体系,从语义上区别开来。
如在配置目录下新建一个 connection.js,然后配置相应的连接,下面是一个草稿样例:
module.exports = {
main: {
adapter: 'sequelize',
// 其它一些 sequelize connection 配置
database: '库 1'
},
auxiliary: {
adapter: 'sequelize',
// 其它一些 sequelize connection 配置
host: '10.10.10.10',
database: '库 2'
},
foo: {
adapter: 'mongoose',
// 其它一些 mongoose connection 配置
},
bar: {
adapter: 'toshihiko',
// ...
}
};
然后在应用的任意位置(前提是 connection 加载完后的生命周期中)都能通过 app 进行获取:
const Main = app.connection.get('main');
这个时候,原有的 egg-sequelize 等包就不需要变更,旧的项目也不需要变更还能继续使用。但若要使用新的 Model 层特性,则需要用新的 ORM 包,可以重新规范化一个 Connection 相关的命名规则,如 egg-conn-sequelize 等。
Model 的定义新增一层规范,使得其其实可以脱离 Connection 机制独立存在。
app.model.get() 实际上只是返回一个对象而已,至于这个对象内部具体是什么(如只是一个普通的对象,或者是一个 Sequelize Model,还是一个别的连接实例化出来的对象)都无关紧要。所以可以考虑成 Model 层只是一个工厂模式,对于底层具体是什么并不关心。
如一个最简单的 Model 文件可以这么写:
// model/foo.js
module.exports = {
getData: function() {
return { txt: 'hello world' };
}
};
此时我们在应用中就可以这么使用该 Model:
const Foo = app.model.get('foo');
console.log(Foo.getData());
再假想我们有一个 egg-conn-sequelize 的配置,其名为 "main",则可以有这么一个假设:
// model/bar.js
const sequelize = app.connection.get('main');
const Bar = sequelize.define('bar', {
name: STRING(30)
});
module.exports = Bar;
同样地,我们就能这么使用它了:
const Bar = app.model.get('bar'); // 这就是一个 Sequelize Model 对象
本次 RFC 主要为了统一 Egg.js 的 Model 层,有以下几点:
引入 Connection Loader 概念,从而能引进各长连接库,其不止是提供给 Model 层使用,而且可以在 Egg.js 任何一个角落被引用。并且该概念的引入可以给其它意想不到的 Model 提供了支持,例如如果有需求是要将一些统计信息与用户 Id 绑定起来,并存入 Redis 中,那么 Model 层中的某个模型就可以基于 Redis 来写:
// st.js
const redis = app.connection.get('redis');
const St = {
getById: function* (userId) {
return yield redis.获取信息(userId);
}
};
module.exports = St;
// 应用中
const St = app.model.get('st');
yield St.getById(USER_ID);
如此一来,Model 底层的东西(这一个 Model 的数据源到底来自哪里)对于上层开发者来说是不需要关心的,只需要使用被封装的函数即可;
有两个点:
有两个点:
- 所有的 model 对象都必须可以关联到 ctx,也就是每个请求实例化,保证数据库记录可被 trace
- 逻辑不入侵 egg-core
@dead-horse 第一点比较好理解,我可以详细梳理一下;关于第二点,有点不理解,怎么样算入侵 egg-core?
app.model.get('foo.bar');
这个我倾向于直接 ctx.model.foo.bar,也方便 TS 代码提示
如此一来,Model 底层的东西(这一个 Model 的数据源到底来自哪里)对于上层开发者来说是不需要关心的,只需要使用被封装的函数即可
是否需要屏蔽使用者对 mysql 和 Sequelize 的区别,这点我持保留意见,之前有一些讨论是倾向于直接区分开: ctx.sequelize.xx 和 ctx.mongoose.xx,它们的模型差别还是有不少的,如果屏蔽掉区别,工具分析方面会不会有问题?
在 egg-core 中新增一个 Model Loader
不建议侵入 egg-core,可以考虑类似 egg-view 那样,加一个 egg-model 插件来规范。
egg-conn-sequelize
从用户使用的角度来看,会需要安装几个插件?
关于第二点,有点不理解,怎么样算入侵 egg-core?
如 @atian25 所说,egg-core 只提供基础的 loader 能力,至于 load 什么内容,由一个额外的插件来实现,这个插件集成到 egg 中。
@atian25
关于
app.model.get()和app.model.NAME。
个人认为两种都可以保留,可深入继续探讨。
是否需要屏蔽使用者对 mysql 和 Sequelize 的区别。
工具分析可以直接在 Model 目录下得知它们是何种适配器。不然开发者在开发的时候需要过一遍脑子想想这个 Model 是由哪个 Adapter 提供的。当然也可以继续讨论。
不建议侵入 egg-core,可以考虑类似 egg-view 那样,加一个 egg-model 插件来规范。
这个可以有,我指的 egg-core 只是一个说法,其实本意只是为了在生命周期启动时能统一载入 Model。
从用户使用的角度来看,会需要安装几个插件?
我之前自己的框架和实践来看,一个项目会有三四个吧,包括数据库、Redis、Memcached、RocketMQ 等。而这些用途个人认为不止是用作 Model 里面定义,比如 RocketMQ 就可以在另一个体系中使用。
从另一个角度回答,用户只需要安装 egg-conn-sequelize,并且 egg-sequelize 这个包会被弃用。两者是互斥的。
从用户使用的角度来看,会需要安装几个插件?
这个的意思是,用户需要 egg-conn-sequelize + egg-sequelize 还是只要 1 个?
两种:
这点 @eggjs/core 也要看看。
@atian25 只需要 egg-conn-sequelize,而 egg-sequelize 将被弃用。而正如我先前所说的,表示长连接,而并不是全是 model,所以我在 RFC 一开始认为应该使用 egg-conn-* 而不是 egg-model-*。因为它脱离了 Model,用户不需要安装 egg-model 也能使用这些 connections。
@XadillaX 目标2的说法应该改一下,意思应该是统一model定义,挂载等方面,然后由框架统一进行。
现在这说法有种让我感觉是要抹平各个ORM的API差别的感觉。
在框架(插件)统一获取定义,并进行挂载后,也避免类似egg-sequlize和egg-mongoose这种挂载到同一个对象导致冲突的问题。
关于插件名称 egg-conn-这种。我觉得是可以的,有前缀能明确插件的范围。和egg-view-类似。
提几个点?
使 Model 脱离某个具体的 ORM 能独立存在,理论上甚至可以脱离任何三方的模块. model的类型/提供的api实际是不一样的,sequlize/mongoose的model就完全不一样,比如有些model其实是orm创建的(比如mongoose.model), 比如有些model支持链式,有些model有save/sync方法等。。这些不能脱离orm吧
原有的插件除了废弃掉是不是还能再抢救一下,通过一些monkey层进行patch
loader的话, 重名 Model怎么办? 可以支持子目录? 大小写? 最好能够规范起来。
可以写写egg-conn基类的设计
@jtyjty99999
@jtyjty99999
使 Model 脱离某个具体的 ORM 能独立存在...
这里指的脱离 ORM 独立存在,指的是这种样例:
// model/foo.js
module.exports = {
getData: function() {
return { txt: 'hello world' };
}
};
开发者能根据自己需求,决定当前这个文件对应的 Model 是基于哪个 ORM 插件实现的,甚至可以完全不基于这些插件而是自己手写。
原有的插件除了废弃掉是不是还能再抢救一下,通过一些monkey层进行patch
关于这一点,我是想在这些 ORM 上直接做 breaking 的修改。但是如果开发者在新版中不想使用我们新的写在这个 RFC 里面的方式来定义 Model,他仍然可以使用旧的这些插件继续好好玩耍,只不过两者是互斥的。
loader的话, 重名 Model怎么办? 可以支持子目录? 大小写? 最好能够规范起来。
重名 Model 用子目录,以及其实可以用不同名的文件等等,这一点在 RFC 里面有提到。
可以写写egg-conn基类的设计
正有此意。不过 egg-connection 我想在底层做(egg-core)做。
这里提一下 egg-model 和 egg-connection 的区别:
开发者能根据自己需求,决定当前这个文件对应的 Model 是基于哪个 ORM 插件实现的,甚至可以完全不基于这些插件而是自己手写。
基于哪个 ORM 插件 这个还要额外引入吧,比如我编写mongoose的model,是不是还要引入一个mongoose。。 那这样model层是很薄了,但是是不是意义就不太大了
@atian25
这些不同真的能完全屏蔽么?
这里的屏蔽不是为了抹平各自 ORM 的 API 差异,而是说在开发者获取 Model 的时候都是通过统一的接口来获取,对于上层开发者来说也不需要去刻意考虑这个 Model 是由哪个 ORM 提供的,直接调用方法就可以了。
在模型定义的时候自然还是要按照各自 ORM 的方式来搞,CLI 该怎么样还是怎么样。可能是我的表述有点问题。
@atian25 抢救下主要是考虑迁移成本实在是太高了,使用老orm插件的人太多了
@jtyjty99999 对的,就是额外引入插件。
Model 层主要就是为了统一导出一堆 Model 对象,由 Loader 统一载入。至于各 Model 是什么样的,要看各 ORM 是这么提供实例化的。所以几个点就是:
提供 Model Loader,并且统一 ORM 规范。
@jtyjty99999 所以完全可以互斥,不想升的人继续用老方案,要升的,我们再推敲下怎么给出 migration 方案。
官方方案最好就一种,所以新方案设计的时候,尽量考虑到不要太痛的升级兼容方式,有必要的时候提供 codemod。
@atian25,官方方案当然就一种,但是有些用户就是不想升级的话,你也不能破坏他原有的应用使其不能跑啊。
而且就算我们这套官方方案出来的话,你也没办法去限制其他三方用户就是一定不会自己再写个类似 egg-sequelize 之类的东西。
那是必然的,遵循 semver 这点是我们一直的坚守。
@atian25 所以这套 RFC 是一个 Minor 的 Feature 升级,并且与原有的这些 ORM 插件互斥,但是并不影响他们不想升级的用户。我是照着这么一个思路来写的。
我和 @jtyjty99999 的意思是,要考虑那些「想升级但升级成本很高」的用户,npm 层面肯定是大版本,不会影响到他们,但由于我们后面只维护新的那个,那就需要考虑如何让旧的项目能平衡的升级,而不是手动去改。
@XadillaX 之前你说的我现在基本+1了,我理解就是做一个 model 的加载器(getter和setter) + conn基类和 n个不同的conn
另外还有一种情况,就是“orm”升级,我们的处理方式,比如很多人会问 egg-mongoose里依赖的mongoose是否要升级啦?? 这个 egg-conn是否要跟不同的orm有个版本同步维护机制。。
@jtyjty99999 你理解的没错,并且 conn 实际上也是一个 Loader,只不过不是遍历目录的 Loader 而是遍历配置文件相应字段。
升级处理方式是抛弃 egg-mongoose 而去使用 egg-conn-mongoose,然后给出升级的代码示例。是否有更好的方法?
@XadillaX 不是,我的意思是 假设已经升级到了 egg-conn-mongoose ,conn-mongoose依赖的mongoose大版本变化咋办,比如3.x变成4.x了
比如 egg-conn-mongoose 1.x 对应mongoose 3.x, 2.x 对应4.x。。
@jtyjty99999 感觉这个需要好好讨论一下。
不过个人认为,在某种程度上,底层 ORM 的大版本升级可以通过 conn 抹平的。实在抹不平的话那 conn 只能跟着升大版本了。
@XadillaX 我觉得这个要看对应orm的BK change是什么。
从egg-conn不处理底下orm具体api的上面来说。如果是这种api废弃移除,类似的变动那只能升级。
和现在的egg-mongoose插件之类的也没什么区别。
如果只是类似配置项这种变动。那是可以在插件层面抹平。
各 Model 可导出一个 name 字段,作为模型名,在项目中获取 Model 的时候作为唯一标识;
这个觉得没必要,约定大于配置,不然有时候看到一个名字,都不知道在哪里定义的。
如果应用开发者实在需要这个别名,自己在 app/extend/context.js 里面加个 getter 即可。
PS:我还是更倾向于 ctx.model.xx.xx 而不是 get
PS2:很多应该是挂载在 ctx 而不是 app 上的。
app.connection.get
这个跟 egg-mysql 用的 addSingleton 有点类似,也就是多数据库模式,这个可以一起考虑。
@atian25 个人认为 app 和 ctx 应该各挂一份吧?
个人觉得还是分库兼容比较好,虽然有多种数据库都要用到的情况,但是还是只用一个数据库的场景多一点。分包可以使使用单个数据库orm更加简单。不赞成把非关系型数据库,关系型数据库和内存数据库都放到一起。分库迁移成本应该也要小。
个人觉得 model 层不一定要真实存在,提供一个所有 orm 的统一约束就好啦,实现交给各 orm 去做。这样插件也只要依赖社区的某一个插件,插件层的逻辑会比较简单,插件单独有的功能部分也能方便暴露。如果统一到 egg-model 里面,逻辑会复杂化,而且像 migration 之类的功能会实现难度也增大。社区如果产出了新的优秀的 orm 插件都需要往 egg-model 里面加,感觉会太臃肿。
@iyuq,没有 egg-model 则 ORM 统一不了。插件不需要往 egg-model 里面加,它只是个 loader,是所有 model 插件的依赖而已。
感觉讨论到这一步,可以写几个简单示例库来讨论了,包括插件怎么写,用户怎么用。如果发现太复杂了,就需要推倒简化
发自我的 iPhone
在 2017年12月5日,10:25,Khaidi Chu notifications@github.com 写道:
@iyuq,没有 egg-model 则 ORM 统一不了。插件不需要往 egg-model 里面加,它只是个 loader,是所有 model 插件的依赖而已。
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.
@iyuq model层实际上就是存在的,现在是各个插件各自为战。你说的统一约束其实应该正是这个草案想要做的,希望把挂载定义的方式统一,这方面的控制权从各个插件,收归一个统一的地方。
插件各自的功能还是自己做。migration之类实现也不会受到太大影响。
我理解中更主要的还是IOC这个概念。
可以移步至 https://github.com/XadillaX/egg-rfc1775-fictitious 进行讨论了,我稍后会往上更新 RFC 内容,以及一个假设的开发者可能会写的新版代码。
我先理解一下,这里讨论的是 model 层的写法,而非数据层的解决方案,所以 orm 写法、API、工具(比如 migration)等等都无法做统一。
可以抽取 egg-model 或 egg-connection 作为这层的标准,实现 model 文件加载(非定义)、connection 的管理、标准化配置,并解决多数据层同时存在的问题。
js
app.connection.set('sequelize', () => { 返回类或者创建实例的方法,这个看能否标准化 })
app.connection.get('sequelize') 获取 Sequelize 类,如果要获取多实例可以 app.connection.get('sequelize_custom')看了上面的讨论
我觉得合理的是
connection 抽象化model 本身在有 service 的情况下可以认为和 proxy 一样是专门封装对数据源的操作,现在用各家 ORM 无非是为了 ORM 本身定义的 model 的便利性,还想再把对 model 的操作给抽象化就丧失了用各种 ORM 的意义了
但是类似于 rabbitmq、socket.io 也包括进来就有点奇怪了,消息的监听消费其实还是 请求-响应 模型,更适合模拟成 http 请求的处理流程
没来得及看完整讨论,先 marked 关注和收邮件。非常赞的 RFC 讨论。
@denghongcai 本来 Connection 与 Model 是分开来两个插件。Model 只是一个 Loader,而 Connection 除了提供各种 RabbitMQ、Socket.io 之类的连接之外,Model 的各种 ORM 连接基类也由它提供。
但后来在与 @popomore 讨论的时候给合并起来了。
至于是要合并还是拆分,可以继续讨论。
刚看了下上面的讨论,有一点个人的想法,如果 Connection 与 Model 完全分开来,使用上是不是有些麻烦,我觉的并不一定要把connection暴露给用户,统一Model的load方式,由Model统一管理connection,这样增加一个model-loader就可以解决大部分问题,如定义model都需要有一下属性和方法
// model.js
const EggModel = Symbol('EggModel')
class Model {
static get [EggModel]() { return true; } // 标识一下
static get options() { //或者也用symbol,返回配置项
return {};
}
static load(app) { //loader加载完成之后,由loader调用
}
static useConnection() { // 返回一个model,如app.model.User.useConnection('reader'),由各个插件实现,用户也可以override
return this;
}
}
// loader.js
const modelDir = path.join(app.baseDir, 'app/model');
app.loader.loadToApp(modelDir, MODELS, {
inject: app,
caseStyle: 'upper',
ignore: 'index.js',
});
for (const name of Object.keys(app[MODELS])) {
const ModelClass = app[MODELS][name];
if(ModelClass[EggModel]) {
// check 是否有load之类的规范方法
ModelClass.load(app)
ModelClass.app = app;
}
}
然后sequelize可以这么用
// egg-sequelize.js
const { Model } = require('sequelize');
class SequelizeModel extends Model {
static get [EggModel]() { return true; }
static get options() {
return {
modelName: {},
attributes: {},
options: {},
};
}
static load(app) {
const sequelize = someFunction(app.config); // connection
const { modelName, attributes, attributes } = this.options;
options.modelName = modelName;
options.sequelize = sequelize;
this.init(attributes, options); // 调用Sequelize.Model#init
}
static useConnection() { // 用户可以override这个方法,实现分库分表之类的,或者通过配置文件
const AnotherModel = class extends this {}; // 这地不一定对,还得看看源码
const sequelize = anotherConnection;
const { modelName, attributes, attributes } = this.options;
options.sequelize = sequelize;
AnotherModel.init(attributes, options);
return AnotherModel;
}
}
module.exports = SequelizeModel;
// app/model/user.js
const { Model, STRING } = require('egg-sequelize');
class User extends Model { // Model就是前面的SequelizeModel
static get options() {
return {
attributes: {
userName: STRING,
},
};
}
findByLogin(login) {
return this.findOne({ login });
}
static async findXXX() {
// find
}
}
module.exports = User;
目前来看,大多数orm的Model跟connection都不耦合,并且都把Model暴露了出来,如sequelize.Model,mongoose.Model,前者通过Model.init()方法,后者通过Model.compile()方法,都可以完成初始化,我们直接扩展原有的Model就可以。
@XadillaX 提到希望解决的三个问题:
- 使用限制更少,使得多 ORM、多库等问题能得到比较好的解决,如使 MySQL 与 MongoDB 能在同一个 Egg.js 项目中共存;
- 外层 API 风格一致性,使得各 ORM 不各自为战(不包括各 ORM 的模型对象中的 API);
- Model 文件递归查找,支持多级子目录形式。
我认为比较必要甚至急迫的就是第一个问题,不同的插件都往ctx.model上注入是不行的,这个必须解决。另外两个问题的需求都不太迫切。事实上,一个项目不会引入太多这种类型的插件,api的不一致不是大问题,挨个看文档就好了。
API风格强行一致,往往是出于一些特殊的强需求,比如feathersjs的service统一接口的设计(https://docs.feathersjs.com/guides/basics/services.html ),是为了跨数据库、跨前后端通信的便利。eggjs目前并没有要做到这个程度,开发实践上大部分项目也不必要做这种设计。
因此大方向上我觉得可能保守一点,比如针对这个挂载点碰撞的问题做一点设计或约定,说不定就够了。
你"请移步"的链接错了,多了个句号。所以我就在这接着讨论吧。
读了一遍你们的讨论,个人 Connection 连接源就不应该出现在 egg-model 的插件里,这个插件的目的不是为了做个官方的 ORM,而是用来规范和统一 ORM 插件的 model API 风格。
egg-model 定位应该是服务于 egg-sequelize, egg-mongoose 以及未来一些包装过的 ORM 插件的 model,在包装或者实现 ORM 插件时 来做统一的加载、初始化等工作。分库、分表、读写分类应该是由 ORM 的封装插件去做配置和支持,这些需求不应该由 egg-model 解决。
如果这样定位,它的功能应该是类似为 model 添加统一的 hook 方式,方便做测试和 model 层的统一处理操作。
在实现时,定义直接用 ORM 的 model 方式定义,在加载的时候根据判断基类类型和配置信息来做初始化,并添加到 ctx.model 上。这样多种数据源的 model 可以并存。
model 定义的代码完全不用变,只要根据model所在数据源进行对应的定义就好,这样就不用去做属性类型转换了。
// app/model/user.js
module.exports = app => {
const { STRING, INTEGER, DATE, Model } = app.Sequelize;
const User = app.sequelize.define('user', { // 只有这里名字和原来不一样
// 但父类还是app.Sequelize.Model
login: STRING,
name: STRING(30),
password: STRING(32),
age: INTEGER,
last_sign_in_at: DATE,
created_at: DATE,
updated_at: DATE,
});
User.findByLogin = function* (login) {
return yield this.findOne({ login: login });
}
User.prototype.logSignin = function* () {
yield this.update({ last_sign_in_at: new Date() });
}
return User;
};
例如 egg-sequelize 的大概配置如下:
// in config of egg-sequelize
exports.model = {
orm: {
sequelize: {
baseModel: 'app.sequelize.model',
init: model=>{ // 这里的意思就是个 hook,在加载完或者加载前等几个关键点为orm处理
model.associate();
}
},
},
};
exports.sequelize = {
dialect: 'mysql',
database: '',
host: 'localhost',
port: 3306,
username: 'root',
password: '',
};
对应用户而言除非需要去实现一个 ORM 插件,否则对于 egg-model 的感受应该就是一套API规范。
ps: 个人的一点思考,供你参考。
https://cnodejs.org/topic/5a7be8cc99ef9fac6b2e6844
app.Sequelize 这个,是不是可以考虑类似 Controller 那样,改为直接 require('egg').Sequelize 啥的
方案太复杂,没有进展,各个 ORM 插件先各自发展吧
Most helpful comment
刚看了下上面的讨论,有一点个人的想法,如果 Connection 与 Model 完全分开来,使用上是不是有些麻烦,我觉的并不一定要把connection暴露给用户,统一Model的load方式,由Model统一管理connection,这样增加一个model-loader就可以解决大部分问题,如定义model都需要有一下属性和方法
然后sequelize可以这么用
目前来看,大多数orm的Model跟connection都不耦合,并且都把Model暴露了出来,如sequelize.Model,mongoose.Model,前者通过Model.init()方法,后者通过Model.compile()方法,都可以完成初始化,我们直接扩展原有的Model就可以。