ThinkJS關聯模型實踐

在數據庫設計特別是關係型數據庫設計中,我們的各個表之間都會存在各種關聯關係。在傳統行業中,使用人數有限且可控的情況下,我們可以使用外鍵來進行關聯,降低開發成本,藉助數據庫產品自身的觸發器可以實現表與關聯表之間的數據一致性和更新。

但是在 web 開發中,卻不太適合使用外鍵。因爲在併發量比較大的情況下,數據庫很容易成爲性能瓶頸,受IO能力限制,且不能輕易地水平擴展,並且程序中會有諸多限制。所以在 web 開發中,對於各個數據表之間的關聯關係一般都在應用中實現。

在 ThinkJS 中,關聯模型就可以很好的解決這個問題。下面我們來學習一下在 ThinkJS 中關聯模型的應用。

<!--more-->

場景模擬

我們以最常見的學生、班級、社團之間的關係來模擬一下場景。

創建班級表

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;

然後我們按照官網文檔關聯模型一一講起,如果不熟悉官網文檔建議先看一遍文檔。

一對一

這個很好理解,很多時候一個表內容太多我們都會將其拆分爲兩個表,一個主表用來存放使用頻率較高的數據,一個附表用來存放使用頻率較低的數據。

我們可以對學生表創建一個附表,用來存放學生個人信息以便我們進行測試。

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://thinkjs.org/zh-cn/doc...

一對一(屬於)

說完第一種一對一關係,我們來說第二種一對一關係。上面的一對一關係是我們期望查詢主表後得到關聯表的數據。也就是主表的主鍵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 relationAction(){
        let student=await this.model('student').setRelation('class').where({id:2}).find();
        return this.success(student);
    }
}

官網文檔 model.setRelation(name, value) 有更多關於臨時開啓或關閉關聯關係的使用方法。

多對多

前面的一對一、一對多還算很容易理解,多對多就有點繞了。想象一下,每個學生可以加入很多社團,而社團同樣由很多學生組成。社團與學生的關係,就是一個多對多的關係。這種情況下,兩張表已經無法完成這個關聯關係了,需要增加一箇中間表來處理關聯關係

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;

根據文檔中多對多關係的介紹,當我們在 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第30行 看到,在構造方法中,會有一個 Relation 實例放到 this[RELATION]

RELATION 是由 Symbol 函數生成的一個Symbol類型的獨一無二的值,在這裏應該是用來實現私有屬性的作用。

然後略過 new Relation() 做了什麼,來看一下模型中 select 這個最終查詢的方法來看一下,在第576行發現在執行了const data = await this.db().select(options);查詢之後,又調用了一個 this.afterFind 方法。而this.afterFind方法又調用了上面提到的 Relation 實例的 afterFind 方法 return this[RELATION].afterFind(data);

看到這裏我們通過命名幾乎已經知道了大概流程:就是在模型正常的查詢之後,又來處理關聯模型的查詢。我們繼續追蹤代碼,來看一下 RelationafterFind 方法又調用了 this.getRelationDatathis.getRelationData則開始解析我們在模型中設置的 relation 屬性,通過循環來調用 parseItemRelation 得到一個 Promise 對象,最終通過 await Promise.all(promises);來全部執行。

parseItemRelation方法則通過調用 this.getRelationInstance 來獲得一個實例,並且執行實例的 getRelationData 方法,並返回。所以上面 this.getRelationData 方法中 Promise.all 執行的其實都是 this.getRelationInstance 生成實例的 getRelationData 方法。

getRelationInstance的作用就是,解析我們設置的模型關聯關係,來生成對應的實例。然後我們可以看一下對應的 getRelationData 方法,最終又執行了模型的select方法,形成遞歸閉環。

從描述看起來似乎很複雜,其實實現的很簡單且精巧。在模型的查詢方法之後,分析模型關聯以後再次調用查詢方法。這樣無論有多少個模型互相關聯都可以查詢出來。唯一要注意的就是上面提到的互相關聯問題,如果我們的模型存在互相關聯問題,可以通過 relation:false 來關閉。

後記

通過上面的實踐可以發現,ThinkJS 的關聯模型實現的精巧且強大,通過簡單的配置,即可實現複雜的關聯。而且通過 setRelation 方法動態的開啓和關閉模型關聯查詢,保證了靈活性。只要我們在數據庫設計時理解關聯關係,並且設計合理,即可節省我們大量的數據庫查詢工作。

PS:以上代碼放在https://github.com/lscho/thinkjs_model_demo

本文首發於知乎 ThinkJS 專欄 [ThinkJS關聯模型實踐
](https://zhuanlan.zhihu.com/p/...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章