Egg: [RFC-1775] 重定义 Model 层

Created on 4 Dec 2017  ·  44Comments  ·  Source: eggjs/egg

请移步 https://github.com/XadillaX/egg-rfc1775-fictitious

Egg.js RFC1775 - Model 层规划

目标

目前在 Egg.js 中,Model 层的编写比较混乱,没有一个较为统一的方案,各自 ORM 插件也各自为战,限制较多,多 ORM 共存时也存在较大的兼容性问题。

这里将要重新规划一下 Egg.js 的 Model 层方案,主要解决以下各问题:

  1. 使用限制更少,使得多 ORM、多库等问题能得到比较好的解决,如使 MySQL 与 MongoDB 能在同一个 Egg.js 项目中共存;
  2. 外层 API 风格一致性,使得各 ORM 不各自为战(不包括各 ORM 的模型对象中的 API);
  3. Model 文件递归查找,支持多级子目录形式。

规划构思(egg-model)

本层规划主要由一个新的插件 egg-model 实现。它主要分以下几个功能。

  • 引入 Connection 机制,用于注册和实例化各数据源实例(包括但不局限于各种 ORM 插件);
  • 加入 Model 目录的 Loader 机制,自动加载 model 目录并载入各 Model 实例。

若要使用 Egg.js 的 Model 层特性,则需要先将 egg-model 安装,并在插件配置中开启它:

// package.json
...
"dependencies": {
  "egg-model": "...",
  ...
},
...

// config/plugin.js
...
exports.model = {
  enable: true,
  package: "egg-model"
};
...

Connection 机制

Connection Loader(连接源加载器)

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(...);

Connection(连接源)

在 Egg.js 的 Model 层概念披露后,将引入 Connection 概念,可理解为连接源,某种意义上可以理解为数据源,数据源为连接源的子集。

连接源包括但不局限于各种 ORM 插件,这些插件都有可能成为 Egg.js Model 层的连接源:

  • egg-sequelize
  • egg-mongoose
  • egg-toshihiko
  • egg-rocketmq
  • egg-socketio
  • ...

以上各插件若已存在于生态中,则将会发布一个 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 的类。

Model Loader(模型加载器)

在应用中如果启用了 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 插件中,但它们两个是独立的概念,只是交集的关系。

  • Connection 可以不在 Model 中使用。开发者可以在一个自己想要的地方(如 lib/ 目录下)获取一个 RabbitMQ 的 Connection 并开始监听;
  • Connection 实例中的 .define() 函数并不是强制的,鉴于各 ORM 的 API 本身就不同,而开发者在使用的时候自己当前在用什么 ORM,所以只需要看文档就可以。如 Sequelize 中可以叫 .define(),而另一个不知道什么 ORM 也许也可以叫 .createModel()
  • Model 不一定要由 Connection 创建,正如前文所述,一个放在 app/model 目录下并且能接受 ctx 的类就能是一个 Model;
  • 通常情况下,推荐 Connection 与 Model 结合使用。

MISC

  • app/model 目录可配。如果使用者为 Java 转过来的,也可以供其自定义目录名为 app/dao

小结

本次 RFC 主要为了统一 Egg.js 的 Model 层,有以下几点:

  1. 引入 Connection 与 Connection Loader 概念,从而能引进各种连接源,其不止是提供给 Model 层使用,而且可以在 Egg.js 任何一个角落被引用;
  2. 新增 Model Loader 概念,使得 Model 的定义权不再在某个 ORM 的包上,而在于 egg-model 这个包中。

相关 Issue


以下为老版。

目标

目前在 Egg.js 中,Model 层的编写比较混乱,没有一个较为统一的方案,各自 ORM 插件也各自为战,限制较多,多 ORM 共存时也存在较大的兼容性问题。

这里将要重新规划一下 Egg.js 的 Model 层方案,主要解决以下各问题:

  1. 使用更自由,使得多 ORM、多库等问题能得到比较好的解决,如使 MySQL 与 MongoDB 能在同一个 Egg.js 项目中共存;
  2. API 风格一致性,统一各 ORM 的基础层面 API,使得它们不各自为战;
  3. Model 文件递归查找,支持多级子目录形式。

规划构思

新增 Model Loader

目前一个构思是,加载 Model 不交由各 ORM 自身的函数来加载,而是在 egg-core 中新增一个 Model Loader,自动遍历应用路径中的 model 目录(递归)并加载。

加载遵循以下几点:

  • 各 Model 可导出一个 name 字段,作为模型名,在项目中获取 Model 的时候作为唯一标识;
  • 若无 name 字段,则以路径名做一些修改为模块名,如 "model/foo/bar.js" 将被识别为 "foo.bar"

    • 去除后缀名;

    • 路径的 / 将被替换为 .

在加载完后,可以在应用中通过函数(或者直接对象)获取,如:

const Bar = app.model.get('foo.bar');

// or

const Bar = app.model._.foo.bar; // 待议

新增 Connection 概念与 Connection Loader

脱离 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 定义

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 层,有以下几点:

  1. 使 Model 脱离某个具体的 ORM 能独立存在,理论上甚至可以脱离任何三方的模块;
  2. 引入 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 的数据源到底来自哪里)对于上层开发者来说是不需要关心的,只需要使用被封装的函数即可;

  3. 新增 Model Loader,使得 Model 的定义权不再某个 Model 的基础包上,而在于 Egg.js 的生命周期的加载时中。

相关 Issue

discussion proposals

Most helpful comment

刚看了下上面的讨论,有一点个人的想法,如果 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就可以。

All 44 comments

有两个点:

  1. 所有的 model 对象都必须可以关联到 ctx,也就是每个请求实例化,保证数据库记录可被 trace
  2. 逻辑不入侵 egg-core

有两个点:

  1. 所有的 model 对象都必须可以关联到 ctx,也就是每个请求实例化,保证数据库记录可被 trace
  2. 逻辑不入侵 egg-core

@dead-horse 第一点比较好理解,我可以详细梳理一下;关于第二点,有点不理解,怎么样算入侵 egg-core?

app.model.get('foo.bar');

这个我倾向于直接 ctx.model.foo.bar,也方便 TS 代码提示

如此一来,Model 底层的东西(这一个 Model 的数据源到底来自哪里)对于上层开发者来说是不需要关心的,只需要使用被封装的函数即可

是否需要屏蔽使用者对 mysql 和 Sequelize 的区别,这点我持保留意见,之前有一些讨论是倾向于直接区分开: ctx.sequelize.xxctx.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 个?

两种:

  • egg-sequelize 直接 break change,类似 egg-view-nunjucks 那样
  • 新起一个插件,叫 egg-model-sequelize 好点,弃用 egg-sequelize

这点 @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-类似。

提几个点?

  1. 使 Model 脱离某个具体的 ORM 能独立存在,理论上甚至可以脱离任何三方的模块. model的类型/提供的api实际是不一样的,sequlize/mongoose的model就完全不一样,比如有些model其实是orm创建的(比如mongoose.model), 比如有些model支持链式,有些model有save/sync方法等。。这些不能脱离orm吧

  2. 原有的插件除了废弃掉是不是还能再抢救一下,通过一些monkey层进行patch

  3. loader的话, 重名 Model怎么办? 可以支持子目录? 大小写? 最好能够规范起来。

  4. 可以写写egg-conn基类的设计

@jtyjty99999

  1. 赞同第一点,我上面也提过,这些不同真的能完全屏蔽么?如 CLI 那块。
  2. 「抢救下」,haaaaaaaaaaaaaaaaaaaaa,我觉得还可以试试救一救,是不是直接大版本,但要看旧项目如何升级
  3. 需要补充,子目录,大小写规范需要的
  4. 从 conn 的设想来看,db,socket.io,GraphQL,RPC 都要吃掉?

@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 的区别:

  • egg-model 是一个插件,开发者按需载入。它的用处是遍历 model 目录并且载入模型;
  • egg-connection 希望是底层 Loader,它的用处是将配置文件里面的配置给实例化成一个个相对应的长连接(如 MySQL、MongoDB,甚至是各 ORM 的一个封装体,如 Sequelize 等待)

开发者能根据自己需求,决定当前这个文件对应的 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 个人认为 appctx 应该各挂一份吧?

个人觉得还是分库兼容比较好,虽然有多种数据库都要用到的情况,但是还是只用一个数据库的场景多一点。分包可以使使用单个数据库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 的管理、标准化配置,并解决多数据层同时存在的问题。

  1. 同意自动加载 app/model 下的文件,不建议自定义名字,按 egg-core 的默认加载方式。
  2. 建议只暴露 ctx.model,不暴露 app.model。
  3. 建议 model 配置和 connection 配置分离,不要建在一个 connection.js 配置上。model 配置为 config.model, connection 配置为 config.sequelize/config.mongoose。connection 多实例配置可以参考现有的 singleton。
  4. connection 的管理可以按照 egg-view,egg-schedule 的方式。在 connection 插件自己添加,如
    js app.connection.set('sequelize', () => { 返回类或者创建实例的方法,这个看能否标准化 })
    应用可以 app.connection.get('sequelize') 获取 Sequelize 类,如果要获取多实例可以 app.connection.get('sequelize_custom')
  5. egg-sequelize 和 egg-conn-sequelize 只保留一种,定义 connection 即可。
  6. 我没看到 ctx.model 如何将 ctx 信息传给 connection 的说明?在 model 文件里获取的 connection 是否要 wrap 一层对象,保证这个是 ContextModel,这个也是 connection 这层需要标准化的关键 。

看了上面的讨论

我觉得合理的是

  1. 提供 Model Loader,并且统一 Model 的获取方式
  2. 考虑到多实例多连接的问题,connection 抽象化

model 本身在有 service 的情况下可以认为和 proxy 一样是专门封装对数据源的操作,现在用各家 ORM 无非是为了 ORM 本身定义的 model 的便利性,还想再把对 model 的操作给抽象化就丧失了用各种 ORM 的意义了

但是类似于 rabbitmqsocket.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 提到希望解决的三个问题:

  1. 使用限制更少,使得多 ORM、多库等问题能得到比较好的解决,如使 MySQL 与 MongoDB 能在同一个 Egg.js 项目中共存;
  2. 外层 API 风格一致性,使得各 ORM 不各自为战(不包括各 ORM 的模型对象中的 API);
  3. 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 插件先各自发展吧

Was this page helpful?
0 / 5 - 0 ratings