# ThinkJS 關聯模型實踐
編者注:日常開發中少不了有大量的數據庫查詢操作,而關聯模型的出現則是幫助開發人員盡量減少重復勞動。ThinkJS 中的關聯模型功能也一直是受到大家的好評的,不過對于沒有接觸過的新同學有時候會不太懂如何配置。今天我們請來了 ThinkJS 用戶 @lscho 同學為我們分享一下他對于關聯模型的學習,希望能夠幫助大家更好的理解 ThinkJS 中的關聯模型。
## 前言
在數據庫設計特別是關系型數據庫設計中,我們的各個表之間都會存在各種關聯關系。在傳統行業中,使用人數有限且可控的情況下,我們可以使用外鍵來進行關聯,降低開發成本,借助數據庫產品自身的觸發器可以實現表與關聯表之間的數據一致性和更新。
但是在 web 開發中,卻不太適合使用外鍵。因為在并發量比較大的情況下,數據庫很容易成為性能瓶頸,受IO能力限制,且不能輕易地水平擴展,并且程序中會有諸多限制。所以在 web 開發中,對于各個數據表之間的關聯關系一般都在應用中實現。
在 ThinkJS 中,關聯模型就可以很好的解決這個問題。下面我們來學習一下在 ThinkJS 中關聯模型的應用。
## 場景模擬
我們以最常見的學生、班級、社團之間的關系來模擬一下場景。
創建班級表
~~~
CREATE TABLE `thinkjs_class` (
`id` int(10) NOT NULL,
`name` varchar(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
復制代碼
~~~
創建學生表
~~~
CREATE TABLE `thinkjs_student` (
`id` int(10) NOT NULL,
`class_id` int(10) NOT NULL,
`name` varchar(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
復制代碼
~~~
創建社團表
~~~
CREATE TABLE `thinkjs_club` (
`id` int(10) NOT NULL,
`name` varchar(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
復制代碼
~~~
然后我們按照官網文檔[關聯模型](https://link.juejin.im/?target=https%3A%2F%2Fthinkjs.org%2Fzh-cn%2Fdoc%2F3.0%2Frelation_model.html%23toc-b25)一一講起,如果不熟悉官網文檔建議先看一遍文檔。
## 一對一
這個很好理解,很多時候一個表內容太多我們都會將其拆分為兩個表,一個主表用來存放使用頻率較高的數據,一個附表用來存放使用頻率較低的數據。
我們可以對學生表創建一個附表,用來存放學生個人信息以便我們進行測試。
~~~
CREATE TABLE `thinkjs_student_info` (
`id` int(10) NOT NULL,
`student_id` int(10) NOT NULL,
`sex` varchar(10) NOT NULL,
`age` int(2) UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
復制代碼
~~~
相對于主表來說,外鍵即是`student_id`,這樣按照規范的命名我們直接在`student`模型文件中定義一下關聯關系即可。
~~~
// src/model/student.js
module.exports = class extends think.Model {
get relation() {
return {
student_info: think.Model.HAS_ONE
};
}
}
復制代碼
~~~
然后我們執行一次查詢
~~~
// src/controller/student.js
module.exports = class extends think.Controller {
async indexAction() {
const student=await this.model('student').where({id:1}).find();
return this.success(student);
}
}
復制代碼
~~~
即可得到主表與關聯附表的數據
~~~
{
"student": {
"id": 1,
"class_id": 1,
"name": "王小明",
"student_info": {
"id": 1,
"student_id": 1,
"sex": "男",
"age": 13
}
}
}
復制代碼
~~~
查看控制臺,我們會發現執行了兩次查詢
~~~
[2018-08-27T23:06:33.760] [41493] [INFO] - SQL: SELECT * FROM `thinkjs_student` WHERE ( `id` = 1 ) LIMIT 1, Time: 12ms
[2018-08-27T23:06:33.764] [41493] [INFO] - SQL: SELECT * FROM `thinkjs_student_info` WHERE ( `student_id` = 1 ), Time: 2ms
復制代碼
~~~
第二次查詢就是 ThinkJS 中的模型功能自動幫我們完成的。
如果我們希望修改一下查詢結果關聯數據的 key,或者我們的表名、外鍵名沒有按照規范創建。那么我們稍微修改一下關聯關系,即可自定義這些數據。
~~~
// src/model/student.js
module.exports = class extends think.Model {
get relation() {
return {
info:{
type:think.Model.HAS_ONE,
model:'student_info',
fKey:'student_id'
}
}
}
}
復制代碼
~~~
再次執行查詢,會發現返回數據中關聯表的數據的 key,已經變成了`info`。
當然除了配置外鍵、模型名這里還可以配置查詢條件、排序規則,甚至分頁等。具體可以參考[model.relation](https://link.juejin.im/?target=https%3A%2F%2Fthinkjs.org%2Fzh-cn%2Fdoc%2F3.0%2Frelation_model.html%23toc-548)支持的參數。
## 一對一(屬于)
說完第一種一對一關系,我們來說第二種一對一關系。上面的一對一關系是我們期望查詢主表后得到關聯表的數據。也就是主表的主鍵`thinkjs_student.id`,是附表的外鍵`thinkjs_student_info.student_id`。那么我們如何通過外鍵查找到另外一張表的數據呢?這就是另外一種一對一關系了。
比如學生與班級的關系,從上面我們創建的表可以看到,學生表中我們通過`thinkjs_student.class_id`來關聯`thinkjs_class.id`,我們在`student`模型中設置一下關聯關系
~~~
// src/model/student.js
module.exports = class extends think.Model {
get relation() {
return {
class: think.Model.BELONG_TO
}
}
}
復制代碼
~~~
查詢后即可得到相關關聯數據
~~~
{
"student": {
"id": 1,
"class_id": 1,
"name": "王小明",
"class": {
"id": 1,
"name": "三年二班"
}
}
}
復制代碼
~~~
同樣,我們也可以自定義數據的 key,以及關聯表的表名、查詢條件等等。
## 一對多
一對多的關系也很好理解,一個班級下面有多個學生,如果我們查詢班級的時候,想把關聯的學生信息也查出來,這時候班級與學生的關系就是一對多關系。這時候設置模型關系就要在 class 模型中設置了
~~~
// src/model/class.js
module.exports = class extends think.Model {
get relation() {
return {
student:think.Model.HAS_MANY
}
}
}
復制代碼
~~~
即可得到關聯學生數據
~~~
{
"id": 1,
"name": "三年二班",
"student": [
{
"id": 1,
"class_id": 1,
"name": "王小明"
},
{
"id": 2,
"class_id": 1,
"name": "陳二狗"
}
]
}
復制代碼
~~~
當然我們也可以通過配置參數來達到自定義查詢
~~~
// src/model/class.js
module.exports = class extends think.Model {
get relation() {
return {
list:{
type:think.Model.HAS_MANY,
model:'student',
fKey: 'class_id',
where:'id>0',
field:'id,name',
limit:10
}
}
}
}
復制代碼
~~~
設置完之后我們測試一下,會發現頁面一直正在加載,打開控制臺會發現一直在循環執行幾條sql語句,這是為什么呢?
因為上面的一對一例子,我們是用 student 和 class 做了`BELONG_TO`的關聯,而這里我們又拿 class 和 student 做了`HAS_MANY`的關聯,這樣就陷入了死循環。我們通過官網文檔可以看到,有個`relation`可以解決這個問題。所以我們把上面的 student 模型中的`BELONG_TO`關聯修改一下
~~~
// src/model/student.js
module.exports = class extends think.Model {
get relation() {
return {
class: {
type:think.Model.BELONG_TO,
relation:false
}
}
}
}
復制代碼
~~~
這樣,即可在正常處理 class 模型的一對多關系了。如果我們想要在 student 模型中繼續使用`BELONG_TO`來得到關聯表數據,只需要在代碼中重新啟用一下即可
~~~
// src/controller/student.js
module.exports = class extends think.Controller {
async indexAction() {
const student = await this.model('student').setRelation('class').where({id:2}).find();
return this.success(student);
}
}
復制代碼
~~~
官網文檔[model.setRelation(name, value)](https://link.juejin.im/?target=https%3A%2F%2Fthinkjs.org%2Fzh-cn%2Fdoc%2F3.0%2Frelation_model.html%23toc-d7a)有更多關于臨時開啟或關閉關聯關系的使用方法。
## 多對多
前面的一對一、一對多還算很容易理解,多對多就有點繞了。想象一下,每個學生可以加入很多社團,而社團同樣由很多學生組成。社團與學生的關系,就是一個多對多的關系。這種情況下,兩張表已經無法完成這個關聯關系了,需要增加一個中間表來處理關聯關系
~~~
CREATE TABLE `thinkjs_student_club` (
`id` int(10) NOT NULL,
`student_id` int(10) NOT NULL,
`club_id` int(10) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
復制代碼
~~~
根據文檔中[多對多](https://link.juejin.im/?target=https%3A%2F%2Fthinkjs.org%2Fzh-cn%2Fdoc%2F3.0%2Frelation_model.html%23toc-000)關系的介紹,當我們在 student 模型中關聯 club 時,`rModel`為中間表,`rfKey`就是`club_id`了
~~~
// src/model/student.js
module.exports = class extends think.Model {
get relation() {
return {
club:{
type: think.Model.MANY_TO_MANY,
rModel: 'student_club',
rfKey: 'club_id'
}
}
}
}
復制代碼
~~~
如果我們想在 club 模型中關聯 student 的數據,只需要把`rfKey`改為`student_id`即可。
當然,多對多也會遇到循環關聯問題。我們只需要把其中一個模型設置`relation:false`即可。
## 關聯循環
在上面我們多次提到關聯循環問題,我們來試著從代碼執行流程來理解這個 feature。
在[think-model](https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2Fthinkjs%2Fthink-model)的[第30行](https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2Fthinkjs%2Fthink-model%2Fblob%2Fmaster%2Fsrc%2Fmodel.js%23L30)看到,在構造方法中,會有一個 Relation 實例放到`this[RELATION]`。
> `RELATION`是由 Symbol 函數生成的一個Symbol類型的獨一無二的值,在這里應該是用來實現私有屬性的作用。
然后略過`new Relation()`做了什么,來看一下模型中`select`這個最終查詢的方法來看一下,在[第576行](https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2Fthinkjs%2Fthink-model%2Fblob%2Fmaster%2Fsrc%2Fmodel.js%23L576)發現在執行了`const data = await this.db().select(options);`查詢之后,又調用了一個`this.afterFind`方法。而[this.afterFind](https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2Fthinkjs%2Fthink-model%2Fblob%2Fmaster%2Fsrc%2Fmodel.js%23L384)方法又調用了上面提到的`Relation`實例的`afterFind`方法`return this[RELATION].afterFind(data);`。
看到這里我們通過命名幾乎已經知道了大概流程:就是在模型正常的查詢之后,又來處理關聯模型的查詢。我們繼續追蹤代碼,來看一下`Relation`的[afterFind](https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2Fthinkjs%2Fthink-model%2Fblob%2Fmaster%2Fsrc%2Frelation%2Frelation.js%23L61)方法又調用了`this.getRelationData`。[this.getRelationData](https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2Fthinkjs%2Fthink-model%2Fblob%2Fmaster%2Fsrc%2Frelation%2Frelation.js%23L101)則開始解析我們在模型中設置的`relation`屬性,通過循環來調用`parseItemRelation`得到一個`Promise`對象,最終通過`await Promise.all(promises);`來全部執行。
而[parseItemRelation](https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2Fthinkjs%2Fthink-model%2Fblob%2Fmaster%2Fsrc%2Frelation%2Frelation.js%23L115)方法則通過調用`this.getRelationInstance`來獲得一個實例,并且執行實例的`getRelationData`方法,并返回。所以上面`this.getRelationData`方法中`Promise.all`執行的其實都是`this.getRelationInstance`生成實例的`getRelationData`方法。
[getRelationInstance](https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2Fthinkjs%2Fthink-model%2Fblob%2Fmaster%2Fsrc%2Frelation%2Frelation.js%23L128)的作用就是,解析我們設置的模型關聯關系,來生成對應的實例。然后我們可以看一下對應的[getRelationData](https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2Fthinkjs%2Fthink-model%2Fblob%2Fmaster%2Fsrc%2Frelation%2Fhas_many.js%23L8)方法,最終又執行了模型的`select`方法,形成遞歸閉環。
從描述看起來似乎很復雜,其實實現的很簡單且精巧。在模型的查詢方法之后,分析模型關聯以后再次調用查詢方法。這樣無論有多少個模型互相關聯都可以查詢出來。唯一要注意的就是上面提到的互相關聯問題,如果我們的模型存在互相關聯問題,可以通過`relation:false`來關閉。
## 后記
通過上面的實踐可以發現,ThinkJS 的關聯模型實現的精巧且強大,通過簡單的配置,即可實現復雜的關聯。而且通過`setRelation`方法動態的開啟和關閉模型關聯查詢,保證了靈活性。只要我們在數據庫設計時理解關聯關系,并且設計合理,即可節省我們大量的數據庫查詢工作。
https://github.com/lscho/thinkjs_model_demo
PS:以上代碼放在[github.com/lscho/think…](https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2Flscho%2Fthinkjs_model_demo)。
- 內容介紹
- EcmaScript基礎
- 快速入門
- 常量與變量
- 字符串
- 函數的基本概念
- 條件判斷
- 數組
- 循環
- while循環
- for循環
- 函數基礎
- 對象
- 對象的方法
- 函數
- 變量作用域
- 箭頭函數
- 閉包
- 高階函數
- map/reduce
- filter
- sort
- Promise
- 基本對象
- Arguments 對象
- 剩余參數
- Map和Set
- Json基礎
- RegExp
- Date
- async
- callback
- promise基礎
- promise-api
- promise鏈
- async-await
- 項目實踐
- 標簽系統
- 遠程API請求
- 面向對象編程
- 創建對象
- 原型繼承
- 項目實踐
- Classes
- 構造函數
- extends
- static
- 項目實踐
- 模塊
- import
- export
- 項目實踐
- 第三方擴展庫
- immutable
- Vue快速入門
- 理解MVVM
- Vue中的MVVM模型
- Webpack+Vue快速入門
- 模板語法
- 計算屬性和偵聽器
- Class 與 Style 綁定
- 條件渲染
- 列表渲染
- 事件處理
- 表單輸入綁定
- 組件基礎
- 組件注冊
- Prop
- 自定義事件
- 插槽
- 混入
- 過濾器
- 項目實踐
- 標簽編輯
- 移動客戶端開發
- uni-app基礎
- 快速入門程序
- 單頁程序
- 底部Tab導航
- Vue語法基礎
- 模版語法
- 計算屬性與偵聽器
- Class與Style綁定
- 樣式與布局
- Box模型
- Flex布局
- 內置指令
- 基本指令
- v-model與表單
- 條件渲染指令
- 列表渲染指令v-for
- 事件與自定義屬性
- 生命周期
- 項目實踐
- 學生實驗
- 貝店商品列表
- 加載更多數據
- 詳情頁面
- 自定義組件
- 內置組件
- 表單組件
- 技術專題
- 狀態管理vuex
- Flyio
- Mockjs
- SCSS
- 條件編譯
- 常用功能實現
- 上拉加載更多數據
- 數據加載綜合案例
- Teaset UI組件庫
- Teaset設計
- Teaset使用基礎
- ts-tag
- ts-badge
- ts-button
- ta-banner
- ts-list
- ts-icon
- ts-load-more
- ts-segmented-control
- 代碼模版
- 項目實踐
- 標簽組件
- 失物招領客戶端原型
- 發布頁面
- 檢索頁面
- 詳情頁面
- 服務端開發技術
- 服務端開發環境配置
- Koajs快速入門
- 快速入門
- 常用Koa中間件介紹
- 文件上傳
- RestfulApi
- 一個復雜的RESTful例子
- 使用Mockjs生成模擬數據
- Thinkjs快速入門
- MVC模式
- Thinkjs介紹
- 快速入門
- RESTful服務
- RBAC案例
- 關聯模型
- 應用開發框架
- 服務端開發
- PC端管理界面開發
- 移動端開發
- 項目實踐
- 失物招領項目
- 移動客戶端UI設計
- 服務端設計
- 數據庫設計
- Event(事件)
- 客戶端設計
- 事件列表頁面
- 發布頁面
- 事件詳情頁面
- API設計
- image
- event
- 微信公眾號開發
- ui設計規范