LoopBack3.0最佳實(shí)踐(三)——面向Model編程

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)系圖


Model inheritance

在上一篇文章中我們提到

在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下載页屠。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末粹胯,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子辰企,更是在濱河造成了極大的恐慌风纠,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,207評(píng)論 6 521
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件牢贸,死亡現(xiàn)場(chǎng)離奇詭異竹观,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)潜索,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,455評(píng)論 3 400
  • 文/潘曉璐 我一進(jìn)店門臭增,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人竹习,你說(shuō)我怎么就攤上這事誊抛。” “怎么了整陌?”我有些...
    開(kāi)封第一講書(shū)人閱讀 170,031評(píng)論 0 366
  • 文/不壞的土叔 我叫張陵拗窃,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我泌辫,道長(zhǎng)并炮,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,334評(píng)論 1 300
  • 正文 為了忘掉前任甥郑,我火速辦了婚禮逃魄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘澜搅。我一直安慰自己伍俘,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,322評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布勉躺。 她就那樣靜靜地躺著癌瘾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪饵溅。 梳的紋絲不亂的頭發(fā)上妨退,一...
    開(kāi)封第一講書(shū)人閱讀 52,895評(píng)論 1 314
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼咬荷。 笑死冠句,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的幸乒。 我是一名探鬼主播懦底,決...
    沈念sama閱讀 41,300評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼罕扎!你這毒婦竟也來(lái)了聚唐?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 40,264評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤腔召,失蹤者是張志新(化名)和其女友劉穎杆查,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體臀蛛,經(jīng)...
    沈念sama閱讀 46,784評(píng)論 1 321
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡亲桦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,870評(píng)論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了掺栅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片烙肺。...
    茶點(diǎn)故事閱讀 40,989評(píng)論 1 354
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖氧卧,靈堂內(nèi)的尸體忽然破棺而出桃笙,到底是詐尸還是另有隱情,我是刑警寧澤沙绝,帶...
    沈念sama閱讀 36,649評(píng)論 5 351
  • 正文 年R本政府宣布搏明,位于F島的核電站,受9級(jí)特大地震影響闪檬,放射性物質(zhì)發(fā)生泄漏星著。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,331評(píng)論 3 336
  • 文/蒙蒙 一粗悯、第九天 我趴在偏房一處隱蔽的房頂上張望虚循。 院中可真熱鬧,春花似錦样傍、人聲如沸横缔。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,814評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)茎刚。三九已至,卻和暖如春撤逢,著一層夾襖步出監(jiān)牢的瞬間膛锭,已是汗流浹背粮坞。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,940評(píng)論 1 275
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留初狰,地道東北人莫杈。 一個(gè)月前我還...
    沈念sama閱讀 49,452評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像跷究,于是被迫代替她去往敵國(guó)和親姓迅。 傳聞我的和親對(duì)象是個(gè)殘疾皇子敲霍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,995評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理俊马,服務(wù)發(fā)現(xiàn),斷路器肩杈,智...
    卡卡羅2017閱讀 134,722評(píng)論 18 139
  • 國(guó)家電網(wǎng)公司企業(yè)標(biāo)準(zhǔn)(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報(bào)批稿:20170802 前言: 排版 ...
    庭說(shuō)閱讀 11,016評(píng)論 6 13
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法柴我,類相關(guān)的語(yǔ)法,內(nèi)部類的語(yǔ)法扩然,繼承相關(guān)的語(yǔ)法艘儒,異常的語(yǔ)法,線程的語(yǔ)...
    子非魚(yú)_t_閱讀 31,669評(píng)論 18 399
  • 在《南都娛樂(lè)》上看了幾篇薛之謙的訪談翻斟,突然想寫(xiě)一篇關(guān)于他的文章。 我一直認(rèn)為说铃,我們是看不到一個(gè)公眾人物最真實(shí)的那面...
    小諾z閱讀 705評(píng)論 2 5
  • 170814【讀書(shū) day155】《刻意學(xué)習(xí)》 Scalers 1h 今天想要談?wù)勱P(guān)于Scalers表達(dá)的關(guān)于“強(qiáng)...
    水若_小水囈夢(mèng)閱讀 157評(píng)論 0 0