1. Model的繼承關(guān)系
雖然我們?cè)诙x一個(gè)Model時(shí)卫旱,只需要配置一些屬性惭每,但LoopBack會(huì)將這些Model轉(zhuǎn)換為一個(gè)Class骨饿。在LoopBack中有三種類型的Model Class,一個(gè)用戶定義的Model被轉(zhuǎn)換成哪種Class取決于它繼承了哪一種父類:
-
基類(Base Model Class):這是所有Model的父類台腥,地位類似于Java里面的Object宏赘。這個(gè)類里面封裝了REST API的全部相關(guān)功能。所以這意味著任何一個(gè)LoopBack的Model天生就可以是RESTful的黎侈。
Model配置文件
中的base
屬性設(shè)定為Model
時(shí)察署,會(huì)繼承該類,開(kāi)發(fā)者需要手動(dòng)編寫(xiě)所有的API方法蜓竹。 -
數(shù)據(jù)持久類(PersistedModel Class):連接數(shù)據(jù)源進(jìn)行數(shù)據(jù)持久化的類箕母。在基類的基礎(chǔ)上自帶了數(shù)據(jù)的增刪改查方法储藐,這些方法直接可以暴露為REST API俱济。
Model配置文件
中的base
屬性設(shè)定為PersistedModel
時(shí)繼承該類(或者不設(shè)定,默認(rèn)情況下繼承該類)钙勃,這是最常用的Model類型蛛碌。 -
內(nèi)置類(Built-in Model):包括User、Role和ACL等辖源。用戶可以直接在
model-config.json
中直接引用這些LoopBack提供的內(nèi)置類蔚携,來(lái)實(shí)現(xiàn)用戶認(rèn)證和權(quán)限控制等相關(guān)功能希太。當(dāng)然這些內(nèi)置類也可以被繼承。
下圖是官方給出的Model繼承關(guān)系圖
在上一篇文章中我們提到
在Loopback的世界里酝蜒,一個(gè)Model不僅僅是Property的集合誊辉,還可以提供REST API Endpoint方法,并且集成ORM功能亡脑。開(kāi)發(fā)者僅需要定義Property和配置參數(shù)堕澄,Loopback會(huì)自動(dòng)集成API和數(shù)據(jù)持久化方法。
這種可以直接打通API層到數(shù)據(jù)持久層的邏輯的殺手锏霉咨,就是數(shù)據(jù)持久類PersistedModel
蛙紫。不用寫(xiě)一行業(yè)務(wù)邏輯代碼,它就把Java程序員熟悉的Controller和DAO的基本功能全部完成了途戒。
這確實(shí)會(huì)提高開(kāi)發(fā)效率坑傅,但也容易引發(fā)開(kāi)發(fā)者關(guān)于代碼架構(gòu)的困惑。傳統(tǒng)的Web開(kāi)發(fā)的分層架構(gòu)也許不再那么適用于LoopBack喷斋,業(yè)務(wù)邏輯代碼可能要更多地圍繞著Model去實(shí)現(xiàn)唁毒,可以說(shuō)需要“面向Model編程”。在討論這個(gè)話題之前星爪,我們不妨先將Model的API功能與ORM功能剝離開(kāi)枉证,看一下LoopBack是怎么支持復(fù)雜業(yè)務(wù)邏輯開(kāi)發(fā)的。
2. ORM功能
支持多種數(shù)據(jù)源
PersistedModel通過(guò)Datasource可以連接多種數(shù)據(jù)源移必,除了各種數(shù)據(jù)庫(kù)之外室谚,甚至連Email服務(wù)都可以成為數(shù)據(jù)源
豐富的CRUD方法
LoopBack為PersistedModel集成了下面這些CRUD方法,既有類方法(Static Method)也有實(shí)例方法(Instance Method)崔泵,常用功能全覆蓋秒赤。
通過(guò)這些方法我們可以輕松實(shí)現(xiàn)對(duì)數(shù)據(jù)庫(kù)的訪問(wèn):
// 這些CURD方法有callback和promise兩種調(diào)用方式:
// 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間的關(guān)系
LoopBack支持以下幾種關(guān)系:
- BelongsTo
- HasOne
- HasMany
- HasManyThrough
- HasAndBelongsToMany
- Polymorphic
- Embedded (EmbedsOne/EmbedsMany/EmbedsMany with belongsTo)
- ReferenceMany
定義一個(gè)Model的Relation可以使用交互命令lb relation
,或者直接修改Model配置文件
憎瘸,以belongsTo為例:
{
"name": "Review",
"base": "PersistedModel",
... // 此處略
"relations": {
"coffeeShop": {
"type": "belongsTo", // 與CoffeeShop建立BelongsTo關(guān)系
"model": "CoffeeShop",
"foreignKey": "" // 這里沒(méi)有指定外鍵入篮,默認(rèn)為coffeeShopId
}
}
}
Model間的關(guān)系通過(guò)外鍵關(guān)聯(lián),可實(shí)現(xiàn)關(guān)聯(lián)查詢
// 查找所有的Review記錄幌甘,并返回其關(guān)聯(lián)的coffeeShop的信息
Review.find({"include":["coffeeShop"]}).then(function(instances) {
console.log(instances);
});
更多關(guān)于Model關(guān)系的用法潮售,敬請(qǐng)期待本系列的后續(xù)文章。
數(shù)據(jù)校驗(yàn)
LoopBack針對(duì)Model實(shí)例數(shù)據(jù)的校驗(yàn)提供了validation方法:
validatesAbsenceOf: 檢查Model實(shí)例是否不包含某些屬性
validatesExclusionOf: 檢查Model實(shí)例的某一個(gè)屬性是否不等于某些值
validatesFormatOf: 檢查Model實(shí)例的某一個(gè)屬性是否符合一個(gè)正則表達(dá)式的格式
validatesInclusionOf: 檢查Model實(shí)例的某一個(gè)屬性是否等于某些值
validatesLengthOf: 校驗(yàn)Model實(shí)例的某屬性的長(zhǎng)度
validatesNumericalityOf: 校驗(yàn)Model實(shí)例的某屬性是否為數(shù)值格式
validatesPresenceOf: 檢查Model實(shí)例是否包含某些屬性
validatesUniquenessOf: 校驗(yàn)Model實(shí)例某屬性的唯一性
validatesDateOf: 校驗(yàn)Model實(shí)例的某屬性是否為日期格式
在Model定義文件
中調(diào)用這些校驗(yàn)方法后方可生效:
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'
});
... // 此處略
}
默認(rèn)情況下锅风,這些校驗(yàn)方法會(huì)在Model實(shí)例創(chuàng)建或更新之前被自動(dòng)調(diào)用酥诽,保證了合法數(shù)據(jù)才能被持久化。下面看在新增一個(gè)CoffeShop實(shí)例時(shí)皱埠,非法數(shù)據(jù)的例子:
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));
請(qǐng)求數(shù)據(jù)中肮帐,city這個(gè)屬性的值Shijiazhuang
不符合validatesInclusionOf
的規(guī)則,拋出異常:
Error:
{ ValidationError: The `CoffeeShop` instance is not valid. Details: `city` is not included in the list (value: "Shijiazhuang").
... // 此處略
3. REST API
Remote Method
上文我們提到LoopBack會(huì)把PersistedModel的CRUD方法自動(dòng)暴露為REST API,但如果我們要自定義一個(gè)API训枢,則需要用到Remote Method托修。分為注冊(cè)和定義兩步:
module.exports = function(CoffeeShop) {
// 1. 注冊(cè)一個(gè)remoteMethod
CoffeeShop.remoteMethod('status', {
description: 'get the status of a CoffeeShop',
accepts: [
{arg: 'id', type: 'string', required: true, description: 'CoffeeShop Id', http: {source: 'path'}}
], // 定義請(qǐng)求參數(shù)格式,支持在path/body/query中攜帶參數(shù)
returns: {arg: 'status', type: 'object', description: '', root: true}, // 定義返回結(jié)果的格式
http: {path: '/:id/status', verb: 'get', status: 200, errorStatus: 500} // 定義HTTP相關(guān)屬性
});
// 2. 定義相應(yīng)的remoteMethod
CoffeeShop.status = function(id, cb) { // 用callback的方式返回結(jié)果
CoffeeShop.findById(id).then(shop => {
if (!shop) {
var error = new Error('Coffee Shop ' + id + ' can not be found');
error.statusCode = 404;
return cb(error); // 返回錯(cuò)誤信息
}
var status = 'Coffee Shop ' + id + ' is open now';
cb(null, status); // 返回結(jié)果
});
};
}
除了callback的方式外恒界,Remote Method也支持以promise的方式返回結(jié)果
CoffeeShop.status = function(id) { // 直接return一個(gè)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;
});
};
正確請(qǐng)求API時(shí)的返回結(jié)果
curl -X GET http://localhost:3000/api/CoffeeShop/1/status
錯(cuò)誤請(qǐng)求的結(jié)果
curl -X GET http://localhost:3000/api/CoffeeShop/4/status
API參數(shù)校驗(yàn)
上文中我們用validation方法實(shí)現(xiàn)了對(duì)Model實(shí)例數(shù)據(jù)的檢驗(yàn)睦刃。但如果要利用這個(gè)功能實(shí)現(xiàn)對(duì)API請(qǐng)求參數(shù)的校驗(yàn),則可以定義一個(gè)專用的Request Model:
{
"name": "APIRequestModel",
"base": "Model", // 基類設(shè)置為Model
"idInjection": false, // 取消id的自動(dòng)注入
"strict": true, // 必需嚴(yán)格符合屬性的定義
"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
對(duì)參數(shù)進(jìn)行校驗(yàn)十酣?第一步眯勾,在注冊(cè)Remote Method時(shí)將API的請(qǐng)求參數(shù)的類型設(shè)置為APIRequestModel
,然后API在被請(qǐng)求時(shí)婆誓,LoopBack會(huì)自動(dòng)把請(qǐng)求數(shù)據(jù)轉(zhuǎn)換為APIRequestModel
的實(shí)例吃环。第二步,在Remote Method中調(diào)用該實(shí)例的isValid
方法洋幻,觸發(fā)數(shù)據(jù)校驗(yàn):
APIModel.remoteMethod('testRequestValidation', {
description: 'test the validation of the request data',
accepts: [
{arg: 'data', type: 'APIRequestModel', required: true, description: 'Request Data', http: {source: 'body'}}
], // 請(qǐng)求參數(shù)的type一定要設(shè)置成相應(yīng)的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()) { // 調(diào)用isValid方法來(lái)校驗(yàn)輸入數(shù)據(jù)
var err = new Error('Invalid Request Data');
err.statusCode = 400;
err.stack = data.errors; // 獲取錯(cuò)誤信息
throw err;
}
return Promise.resolve(true);
};
4. 面向Model編程
通過(guò)上面的介紹我們可以看到郁轻,LoopBack里的一切功能皆圍繞著Model展開(kāi),Model承擔(dān)著傳統(tǒng)Web應(yīng)用分層架構(gòu)中Controller和DAO兩種角色文留。在實(shí)際項(xiàng)目中使用LoopBack框架時(shí)好唯,如果API的請(qǐng)求/返回?cái)?shù)據(jù)的格式和數(shù)據(jù)庫(kù)的Schema比較接近,可以允許Model同時(shí)實(shí)現(xiàn)API邏輯和ORM邏輯燥翅。但對(duì)于數(shù)據(jù)模型比較復(fù)雜的Web應(yīng)用骑篙,如果對(duì)不加以區(qū)分,可能會(huì)導(dǎo)致代碼的耦合森书。所以我們要考慮如何組織應(yīng)用程序中的Model靶端,使得代碼架構(gòu)更加合理。
一種思路是凛膏,將Model在邏輯上區(qū)分為“API Model”和“Data Model”杨名,前者并不綁定數(shù)據(jù)源,只負(fù)責(zé)暴露API方法猖毫,后者連接數(shù)據(jù)源台谍,負(fù)責(zé)CRUD∮醵希“API Model”在實(shí)現(xiàn)時(shí)趁蕊,可以同時(shí)輔以“API Request Model”和”API Response Model“,規(guī)范和校驗(yàn)API的請(qǐng)求和返回?cái)?shù)據(jù)仔役≈阑铮“Data Model”也可以在邏輯上進(jìn)行進(jìn)一步區(qū)分,將那些連接第三方服務(wù)的Model稱為“Service Data Model”骂因,以區(qū)別于用于持久化數(shù)據(jù)到數(shù)據(jù)庫(kù)的“DB Data Model”:
在大部分應(yīng)用場(chǎng)景下炎咖,一切皆可為Model,因?yàn)镸odel在本質(zhì)上講就是Class寒波。當(dāng)業(yè)務(wù)邏輯和代碼架構(gòu)都圍繞著Model展開(kāi)時(shí)乘盼,就是在“面向Model編程”。
當(dāng)然這也是一家之言俄烁,歡迎留言討論绸栅。另外,本文涉及的代碼可以到Github項(xiàng)目loopback-hello-world下載页屠。