在上一篇教程中,第一個模型 BlogEntry,位於項目目錄中的 api/models/BlogEntry.js 文件中
1. BlogEntry.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | module.exports = {
attributes: { title: { type: 'string', required: true, defaultsTo: '' }, body: { type: 'string', required: true, defaultsTo: '' } }
}; |
如果您現在想要將博客 API 擴展爲一個更加通用的 CMS,該怎麼辦?現在您還處於開發流程中足夠早的階段,這種情形尚處於可管理狀態,而且最終得到的結果會比一個博客平臺靈活得多。它不會損害在 Sails.js 中重構相對容易的事實。重構 Sails 模型
首先要將模型的名稱從 BlogEntry 更改爲某個用途更廣的名稱。(順便說一下,我聽說編程過程中最困難的三件事包括命名和差一 (off-by-one) 錯誤。我們拭目以待!)因爲該模型的類型是從其文件名中獲得的,所以將它從 BlogEntry.js 重命名爲 Entry.js 會告訴 Sails 更新模型類型。對相應的api/controllers/BlogEntryController.js 文件執行相同操作,
將它更改爲 EntryController.js,就這麼簡單:您已重構了您的模型。您的 HTTP API 現在能夠用於比網絡博客更多的用途。
但是回想一下,您使用了 sails-disk 作爲開發數據庫適配器。sails-disk 是一種直接存儲到磁盤的序列化格式;所以它沒有表、列或其他任何類似數據庫的基礎架構。這種簡單性使 sails-disk 在開發期間很容易使用,但您需要在代碼接近生產階段時將它替換爲其他格式。您可能想知道在應用程序準備好上線時,這種看似容易的重構將如何進行。
幸運的是,Sails 中的每個模型對象可保留許多模型屬性。設置模型屬性,使 Sails 能夠將模型與底層數據庫相匹配。您可以從 Sails 文檔中瞭解模型設置。就目前而言,您只需要關心 tableName。如果您對某個真實的數據庫使用了此屬性,結果將類似於:
2. Entry.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | module.exports = {
tableName: "blogentry", // this would map to a relational table by this name, // or a MongoDB collection, and so on
attributes: { id: { type: 'integer', primaryKey: true }, title: { type: 'string', required: true, defaultsTo: '' }, body: { type: 'string', required: true, defaultsTo: '' } }
}; |
在這裏,可以看到 Entry.js 指定了它所綁定的表名稱,在本例中爲“blogentry”。如果模型對象中的特定字段需要與底層數據庫中的指定列對應,您可以使用 columnName 屬性來註釋每個字段,命名它應該映射到的表列(或集合中的字段,具體取決於數據存儲類型)。
3. 將字段映射到列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | module.exports = {
tableName: "blogentry", // this would map to a relational table by this name, // or a MongoDB collection, and so on
attributes: { id: { type: 'integer', primaryKey: true, columnName: 'blogentry_pk' }, title: { type: 'string', required: true, defaultsTo: '', columnName: 'blogtitle' }, body: { type: 'string', required: true, defaultsTo: '' } }
}; |
在向系統添加更多條目類型時,需要執行一些額外的更改,但就現在而言,這就足夠了。
Sails 中的關聯
大多數發表的內容類型都會存儲和顯示一位或多位作者,所以您需要一個相關的模型。
4. 在 CMS 中重新表示作者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | module.exports = { autoPK: false, attributes: { fullName: { type: 'string', required: true }, bio: { type: 'string' }, username: { type: 'string', unique: true, required: true }, email: { type: 'email', required: true } } }; |
目前而言,Author.js 是一種簡單易懂的數據類型,而且它能夠很好地表示作者的屬性:全名、簡介、用戶名等。該模型中缺少的是作者身份(authorship)的概念:一位作者創建了一篇文章,因此每篇文章都由一位作者編寫。這比您目前處理的概念更難建模。事實上,這時就需要使用 Sails 關聯,此概念不同於簡單的屬性。
評論和標籤
作者身份不是您唯一需要爲此應用程序建模的關聯,所以在解決這個大問題之前,讓我們看看兩個更簡單的模型。每篇文章都擁有評論和一組可用於描述它的標籤。添加標籤會得到一個用於直觀顯示的“標籤雲”(tag cloud),生成一種基於元數據的主題分類系統。通過建模這些類型,您可以練習使用關聯。只需記住,實際的 CMS 需要十幾種關聯。
回想一下,我們遵循的一條編程規則(包括使用 Sails 編程)就是保持簡單(Keep it simple)。按照這種編碼精神,評論的數據模型基本上應該僅包含評論的正文,以及可選的發表評論的人的電子郵件地址,
5. Comment.js
1 2 3 4 5 6 7 8 9 10 11 12 | module.exports = {
attributes: { body: { type: 'string', required: true }, commenterEmail: { type: 'email' } } }; |
類似地,標籤的數據模型僅包含標籤的名稱(通常是內容的元數據,比如“Java”或“Sails”),不需要額外的修飾。
6. Tag.js
1 2 3 4 5 6 7 8 | module.exports = {
attributes: { name: { type: 'string' } } }; |
現在是時候開始定義您的數據模型與它們包含的數據之間的關係了。在定義了兩個數據模型後,您就掌握了定義更多模型所需的基礎知識。想要添加一個模型時,只需在api/model 目錄中創建一個模型。也可以輸入命令:sails generate model ...,Sails 就會爲您添加一個佔位符。
顯式關係
Sails 沒有采用一些數據庫系統所使用的隱式方法,它使用了顯式的關係模型。例如,對於關係數據庫系統,Sails 會使用數據來建模兩個表之間的關聯——在一個表中定義的主鍵,它的值用作另一個表中某一行的外鍵值——而不是在數據庫模式(schema)中結構化地定義它。
關係數據庫追隨者們會注意到,大部分數據庫系統都支持結構化定義的關係。我的意思是說,在 RDBMS 中,您可以使用數據庫約束來確保任何作爲外鍵值插入的值也存在於相關的表中。對於 Sails,我們使用數據(而不是某種物理結構)來表示這種關係。與關係模型相反,可以考慮一種面向文檔的數據庫,比如 MongoDB 或 CouchDB。在面向文檔的系統中,您嵌入了一個數組作爲文檔的成員,而不是將一組值與其他數據元素關聯。
在針對一種特定數據結構而建模時,隱式建模非常適合;在您的數據要使用多種數據庫類型來組織時,它就不太適合了。Sails 需要顯式理解關係,以便知道如何針對給定數據庫類型來建模和搭建語句或查詢——無論是 RDBMS、NoSQL 或其他某種類型。儘管這可能對您的模型對象提出一些不熟悉的需求,但這些要求不是太嚴格;您只需要學會更加顯式地考慮數據及其連接方式即可。
一對多關係
首先,考慮文章與作者的關係。一位作者可以編寫多篇文章,而每篇文章只能有一個作者。不出所料,Sails 將此稱爲一對多關係(one-to-many relationship)。作者與文章之間的關係也是雙向的(bidirectional),因爲應該可以檢索給定作者的所有文章,以及查看任何給定文章的作者。(事實證明,Sails 默認情況下將在查詢中自動拉取這部分附加數據,並將它發送到客戶端。)
定義一對多關係需要修改該關聯兩端的模型對象。您需要定義作爲關聯的 “一” 端上的集合的字段,以及將關聯的 “多” 端連接到這個 “一” 端的字段。這不太適合用文字描述,但在代碼中看到會簡單得多。
7. Author.js 和關聯的文章
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | module.exports = { attributes: { fullName: { type: 'string', required: true }, bio: { type: 'string' }, username: { type: 'string', unique: true, required: true }, email: { type: 'email', required: true }, entries: { collection: 'entry', via: 'author' } } }; |
8. 文章類型7 中重構的代碼表明,Author 擁有一個 entries 字段,該字段包含由若干篇文章形成的一個 Entry 對象集合。Entry 類型通過 Entry 對象上的“author”字段指向 Author 實例。所有這些意味着 Entry 類型需要看起來類似於清單 8。
這是因爲 Sails 從小寫形式的文件名(也稱爲類型的身份)獲取模型類型。小寫的類型(前面的清單中的entries 和 author)變成了所生成的藍圖路由的前綴。小寫的類型也變成了 Sails 系統中模型的正式名稱。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | module.exports = { attributes: { fullName: { type: 'string', required: true }, bio: { type: 'string' }, username: { type: 'string', unique: true, required: true }, email: { type: 'email', required: true }, entries: { collection: 'entry', via: 'author' } } }; |
注意,在清單 7 和清單 8中,引用的類型(在 Author 的 entries 字段的 collection 字段中,以及 Entry的 author 字段的 model 字段中)使用了小寫。
當我在本系列的下一篇教程中討論控制器時,您還會看到身份的概念。Sails 需要能夠在控制器級別確定控制器和模型是否具有相同的身份。它使用該信息生成正確的默認藍圖路由。就目前而言,只需注意 Sails 要求用作 model 字段值的類型應爲小寫。
鏈接模型對象
我們返回到 Author 與 Entry 之間的一對多關聯上。還需要了解如何在您的代碼中使用該關聯,包括不僅需要定義它,還需要知道在從數據庫檢索一個模型對象時期望獲得哪些信息。對我們而言,幸運的是,Sails 能非常靈活地鏈接模型對象。
當您創建一個 Author 實例時,Sails 爲它生成了一個唯一主鍵,該主鍵是在 id 字段中定義的。您可以使用新的 id 作爲關聯字段的值,Sails 會自動連接兩個對象,如清單 9 所示。
9. 連接對象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | Author.create({ fullName: "Fred Flintstone", bio: "Lives in Bedrock, blogs in cyberspace", username: "fredf", email: "[email protected]" }).exec(function (err, author) { Entry.create({ title: "Hello", body: "Yabba dabba doo!", author: author.id }).exec(function (err, created) { Entry.create({ title: "Quit", body: "Mr Slate is a jerk", author: author }).exec(function (err, created) { return res.send("Database seeded"); }); }); }); |
清單 9 中的代碼是經典的 Node.js,被稱爲“callbacks galore”。第一個調用使用您傳入的值創建了一個Author 實例。在觸發 exec() 中的回調時,您將獲取 Author 的 ID 值並設置爲您新創建的 Entry 對象中的author 字段的值。或者更簡單地講:通過設置 author 字段來引用正確的 Author,將 Entry 鏈接到Author。
作爲前面提及的“callbacks galore”的替代方案,Sails 支持藍鳥承諾。我想避免針對不同承諾風格的庫的利弊的爭議,所以我暫時對代碼採用經典回調格式。
鏈接模型對象的另一種方式
如果前一種方法不適合您,Sails 還提供了一種替代方案:無需獲取Author 的 id 字段,您可以傳遞整個 Author 對象。無論採用哪種方式,最終結果都是一樣的。
對於更習慣於考慮物理存儲模型的開發人員,直接傳入對象可能更有吸引力,而傳入 id 能更準確地反應對象之間的鏈接。如果您習慣於“對象思維”,傳遞對象可以確認對象現在已鏈接,但抽象化了它們的鏈接方式的細節。
揭示關聯
無論您如何達到這一步,一旦在數據庫中設置對象後,Sails 就會不遺餘力地明確顯示它們之間的關聯。清單 10 展示了在您運行清單 9 中的代碼,然後訪問 http://localhost:1337/author(從系統獲取所有作者的藍圖默認路由)時,您會看到的結果。
10. 返回的作者查詢結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | [ { "comments": [], "author": { "fullName": "Fred Flintstone", "bio": "Lives in Bedrock, blogs in cyberspace", "username": "fredf", "email": "[email protected]", "createdAt": "2016-02-16T21:15:55.716Z", "updatedAt": "2016-02-16T21:15:55.716Z", "id": 6 }, "title": "Hello", "body": "Yabba dabba doo!", "createdAt": "2016-02-16T21:15:55.722Z", "updatedAt": "2016-02-16T21:15:55.722Z", "id": 6 }, { "comments": [], "author": { "fullName": "Fred Flintstone", "bio": "Lives in Bedrock, blogs in cyberspace", "username": "fredf", "email": "[email protected]", "createdAt": "2016-02-16T21:15:55.716Z", "updatedAt": "2016-02-16T21:15:55.716Z", "id": 6 }, "title": "Quit", "body": "Mr Slate is a jerk", "createdAt": "2016-02-16T21:15:55.725Z", "updatedAt": "2016-02-16T21:15:55.725Z", "id": 7 } ] |
11. 返回的文章查詢結果類似地,訪問 Entry 對象相應路由的過程類似於清單 11。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | [ { "comments": [], "author": { "fullName": "Fred Flintstone", "bio": "Lives in Bedrock, blogs in cyberspace", "username": "fredf", "email": "[email protected]", "createdAt": "2016-02-16T21:15:55.716Z", "updatedAt": "2016-02-16T21:15:55.716Z", "id": 6 }, "title": "Hello", "body": "Yabba dabba doo!", "createdAt": "2016-02-16T21:15:55.722Z", "updatedAt": "2016-02-16T21:15:55.722Z", "id": 6 }, { "comments": [], "author": { "fullName": "Fred Flintstone", "bio": "Lives in Bedrock, blogs in cyberspace", "username": "fredf", "email": "[email protected]", "createdAt": "2016-02-16T21:15:55.716Z", "updatedAt": "2016-02-16T21:15:55.716Z", "id": 6 }, "title": "Quit", "body": "Mr Slate is a jerk", "createdAt": "2016-02-16T21:15:55.725Z", "updatedAt": "2016-02-16T21:15:55.725Z", "id": 7 } ] |
我們僅探索了一種關聯模型(一對多模型),但 Sails 支持所有模型:一對一、多對多,以及這些主題上的一些不太傳統的變體。它們在很大程度上具有類似的工作方式:在模型對象上定義合適的字段,將對象或其 ID 分配給關聯字段,讓 Sails 負責處理剩餘工作。儘管文章和作者是單獨存儲的,但 Sails 會利用它對這些模型對象關聯的瞭解,“填充”合適的字段並讓它們看起來像是一個平面對象。因爲 Sails 的目的是用作一個後端 HTTP API 實現庫,所以會將從應用程序 UI(移動或 Web)到數據庫的往返次數保持到最少。通過“扁平化”數據結構,您可以在一次(可能很漫長的)往返中瞭解 Author 的完整細節。這有助於實現能更有效地執行和擴展的更高效系統
。