LoopBack3.0最佳实践(三)——面向Model编程

1. Model的继承关系

虽然我们在定义一个Model时,只需要配置一些属性,但LoopBack会将这些Model转换为一个Class。在LoopBack中有三种类型的Model Class,一个用户定义的Model被转换成哪种Class取决于它继承了哪一种父类:

  • 基类(Base Model Class):这是所有Model的父类,地位类似于Java里面的Object。这个类里面封装了REST API的全部相关功能。所以这意味着任何一个LoopBack的Model天生就可以是RESTful的。Model配置文件中的base属性设定为Model时,会继承该类,开发者需要手动编写所有的API方法。
  • 数据持久类(PersistedModel Class):连接数据源进行数据持久化的类。在基类的基础上自带了数据的增删改查方法,这些方法直接可以暴露为REST API。Model配置文件中的base属性设定为PersistedModel时继承该类(或者不设定,默认情况下继承该类),这是最常用的Model类型。
  • 内置类(Built-in Model):包括User、Role和ACL等。用户可以直接在model-config.json中直接引用这些LoopBack提供的内置类,来实现用户认证和权限控制等相关功能。当然这些内置类也可以被继承。

下图是官方给出的Model继承关系图


Model inheritance

在上一篇文章中我们提到

在Loopback的世界里,一个Model不仅仅是Property的集合,还可以提供REST API Endpoint方法,并且集成ORM功能。开发者仅需要定义Property和配置参数,Loopback会自动集成API和数据持久化方法。

这种可以直接打通API层到数据持久层的逻辑的杀手锏,就是数据持久类PersistedModel。不用写一行业务逻辑代码,它就把Java程序员熟悉的Controller和DAO的基本功能全部完成了。

这确实会提高开发效率,但也容易引发开发者关于代码架构的困惑。传统的Web开发的分层架构也许不再那么适用于LoopBack,业务逻辑代码可能要更多地围绕着Model去实现,可以说需要“面向Model编程”。在讨论这个话题之前,我们不妨先将Model的API功能与ORM功能剥离开,看一下LoopBack是怎么支持复杂业务逻辑开发的。

2. ORM功能

支持多种数据源

PersistedModel通过Datasource可以连接多种数据源,除了各种数据库之外,甚至连Email服务都可以成为数据源


丰富的CRUD方法

LoopBack为PersistedModel集成了下面这些CRUD方法,既有类方法(Static Method)也有实例方法(Instance Method),常用功能全覆盖。

通过这些方法我们可以轻松实现对数据库的访问:

// 这些CURD方法有callback和promise两种调用方式:
// 1. callback方式
CoffeeShop.findById(shopId, function (err, instance) {
  if (err)
    console.error(err);
  else 
    console.log(instance);
});
// 2. promise方式
CoffeeShop.findById(shopId).then(function (instance) {
  console.log(instance);
}).catch(function (err) {
  console.error(err);
});
支持建立Model间的关系

LoopBack支持以下几种关系:

  • BelongsTo
  • HasOne
  • HasMany
  • HasManyThrough
  • HasAndBelongsToMany
  • Polymorphic
  • Embedded (EmbedsOne/EmbedsMany/EmbedsMany with belongsTo)
  • ReferenceMany

定义一个Model的Relation可以使用交互命令lb relation,或者直接修改Model配置文件,以belongsTo为例:

{
  "name": "Review",
  "base": "PersistedModel",
  ... // 此处略
  "relations": {
    "coffeeShop": {
      "type": "belongsTo", // 与CoffeeShop建立BelongsTo关系
      "model": "CoffeeShop", 
      "foreignKey": "" // 这里没有指定外键,默认为coffeeShopId
    }
  }
}

Model间的关系通过外键关联,可实现关联查询

// 查找所有的Review记录,并返回其关联的coffeeShop的信息
Review.find({"include":["coffeeShop"]}).then(function(instances) {
  console.log(instances);
});

更多关于Model关系的用法,敬请期待本系列的后续文章。

数据校验

LoopBack针对Model实例数据的校验提供了validation方法

validatesAbsenceOf: 检查Model实例是否不包含某些属性
validatesExclusionOf: 检查Model实例的某一个属性是否不等于某些值
validatesFormatOf: 检查Model实例的某一个属性是否符合一个正则表达式的格式
validatesInclusionOf: 检查Model实例的某一个属性是否等于某些值
validatesLengthOf: 校验Model实例的某属性的长度
validatesNumericalityOf: 校验Model实例的某属性是否为数值格式
validatesPresenceOf: 检查Model实例是否包含某些属性
validatesUniquenessOf: 校验Model实例某属性的唯一性
validatesDateOf: 校验Model实例的某属性是否为日期格式

Model定义文件中调用这些校验方法后方可生效:

module.exports = function(CoffeeShop) {
  // validation方法
  CoffeeShop.validatesLengthOf('name', {min: 2, message: {min: 'name is too short'}});
  CoffeeShop.validatesInclusionOf('city', {in: ['Beijing', 'Shanghai']});
  // 自定义的validation方法
  CoffeeShop.validate('city', function(err) {
    if (this.city && this.city.length > 15) {
      return err();
    }
  }, {
    message: 'city value is too long'
  });
  ... // 此处略
}

默认情况下,这些校验方法会在Model实例创建或更新之前被自动调用,保证了合法数据才能被持久化。下面看在新增一个CoffeShop实例时,非法数据的例子:

var CoffeeShop = app.models.CoffeeShop;
var instanceData = {
  'name': 'hi coffee',
  'city': 'Shijiazhuang'
};
CoffeeShop.create(instanceData)
  .then(result => console.log(result))
  .catch(err => console.error(err));

请求数据中,city这个属性的值Shijiazhuang不符合validatesInclusionOf的规则,抛出异常:

Error: 
{ ValidationError: The `CoffeeShop` instance is not valid. Details: `city` is not included in the list (value: "Shijiazhuang").
... // 此处略

3. REST API

Remote Method

上文我们提到LoopBack会把PersistedModel的CRUD方法自动暴露为REST API,但如果我们要自定义一个API,则需要用到Remote Method。分为注册和定义两步:

module.exports = function(CoffeeShop) {
  // 1. 注册一个remoteMethod
  CoffeeShop.remoteMethod('status', {
    description: 'get the status of a CoffeeShop',
    accepts: [
      {arg: 'id', type: 'string', required: true, description: 'CoffeeShop Id', http: {source: 'path'}}
    ], // 定义请求参数格式,支持在path/body/query中携带参数
    returns: {arg: 'status', type: 'object', description: '', root: true}, // 定义返回结果的格式
    http: {path: '/:id/status', verb: 'get', status: 200, errorStatus: 500} // 定义HTTP相关属性
  });

  // 2. 定义相应的remoteMethod
  CoffeeShop.status = function(id, cb) { // 用callback的方式返回结果
    CoffeeShop.findById(id).then(shop => {
      if (!shop) {
        var error = new Error('Coffee Shop ' + id + ' can not be found');
        error.statusCode = 404;
        return cb(error); // 返回错误信息
      }
      var status = 'Coffee Shop ' + id + ' is open now';
      cb(null, status); // 返回结果
    });
  };
}

除了callback的方式外,Remote Method也支持以promise的方式返回结果

CoffeeShop.status = function(id) { // 直接return一个promise
  return CoffeeShop.findById(id).then(shop => {
    if (!shop) {
      var error = new Error('Coffee Shop ' + id + ' can not be found');
      error.statusCode = 404;
      throw error; // 处理异常
    }
    var status = 'Coffee Shop ' + id + ' is open now';
    return status;
  });
};

正确请求API时的返回结果

curl -X GET http://localhost:3000/api/CoffeeShop/1/status

错误请求的结果

curl -X GET http://localhost:3000/api/CoffeeShop/4/status
API参数校验

上文中我们用validation方法实现了对Model实例数据的检验。但如果要利用这个功能实现对API请求参数的校验,则可以定义一个专用的Request Model:

{
  "name": "APIRequestModel",
  "base": "Model",  // 基类设置为Model
  "idInjection": false, // 取消id的自动注入
  "strict": true, // 必需严格符合属性的定义
  "properties": {
    "id": false, // 取消id字段
    "param1": {
      "type": "string",
      "required": true
    },
    "param2": {
      "type": "string"
    }
  },
  "validations": [],
  "relations": {},
  "acls": [],
  "methods": {}
}

api-request-model.js里面加入一些validation方法:

module.exports = function(APIRequestModel) {
  APIRequestModel.validatesLengthOf('param1', {max: 6, message: {max: 'length is too long'}});
  APIRequestModel.validatesExclusionOf('param2', {in: ['string'], message: {in: 'can not be `string`'}});
}

那么如何利用APIRequestModel对参数进行校验?第一步,在注册Remote Method时将API的请求参数的类型设置为APIRequestModel,然后API在被请求时,LoopBack会自动把请求数据转换为APIRequestModel的实例。第二步,在Remote Method中调用该实例的isValid方法,触发数据校验:

APIModel.remoteMethod('testRequestValidation', {
  description: 'test the validation of the request data',
  accepts: [
    {arg: 'data', type: 'APIRequestModel', required: true, description: 'Request Data', http: {source: 'body'}}
  ], // 请求参数的type一定要设置成相应的Model
  returns: {arg: 'result', type: 'boolean', description: '', root: true},
  http: {path: '/validation', verb: 'post', status: 200, errorStatus: 500}
});

APIModel.testRequestValidation = function(data) {
  if (!data.isValid()) { // 调用isValid方法来校验输入数据
    var err = new Error('Invalid Request Data');
    err.statusCode = 400;
    err.stack = data.errors; // 获取错误信息
    throw err;
  }
  return Promise.resolve(true);
};

4. 面向Model编程

通过上面的介绍我们可以看到,LoopBack里的一切功能皆围绕着Model展开,Model承担着传统Web应用分层架构中Controller和DAO两种角色。在实际项目中使用LoopBack框架时,如果API的请求/返回数据的格式和数据库的Schema比较接近,可以允许Model同时实现API逻辑和ORM逻辑。但对于数据模型比较复杂的Web应用,如果对不加以区分,可能会导致代码的耦合。所以我们要考虑如何组织应用程序中的Model,使得代码架构更加合理。

一种思路是,将Model在逻辑上区分为“API Model”和“Data Model”,前者并不绑定数据源,只负责暴露API方法,后者连接数据源,负责CRUD。“API Model”在实现时,可以同时辅以“API Request Model”和”API Response Model“,规范和校验API的请求和返回数据。“Data Model”也可以在逻辑上进行进一步区分,将那些连接第三方服务的Model称为“Service Data Model”,以区别于用于持久化数据到数据库的“DB Data Model”:


在大部分应用场景下,一切皆可为Model,因为Model在本质上讲就是Class。当业务逻辑和代码架构都围绕着Model展开时,就是在“面向Model编程”。

当然这也是一家之言,欢迎留言讨论。另外,本文涉及的代码可以到Github项目loopback-hello-world下载。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,863评论 6 13
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,573评论 18 399
  • 在《南都娱乐》上看了几篇薛之谦的访谈,突然想写一篇关于他的文章。 我一直认为,我们是看不到一个公众人物最真实的那面...
    小诺z阅读 692评论 2 5
  • 170814【读书 day155】《刻意学习》 Scalers 1h 今天想要谈谈关于Scalers表达的关于“强...
    水若_小水呓梦阅读 153评论 0 0