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

LoopBack3.0最佳实践(三)——面向Model编程_第1张图片

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继承关系图


LoopBack3.0最佳实践(三)——面向Model编程_第2张图片
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服务都可以成为数据源


LoopBack3.0最佳实践(三)——面向Model编程_第3张图片
丰富的CRUD方法

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

LoopBack3.0最佳实践(三)——面向Model编程_第4张图片

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

// 这些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
LoopBack3.0最佳实践(三)——面向Model编程_第5张图片
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”:


LoopBack3.0最佳实践(三)——面向Model编程_第6张图片

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

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

你可能感兴趣的:(LoopBack3.0最佳实践(三)——面向Model编程)