ThinkJS 關聯模型實踐

編者注:平常開發中少不了有大量的數據庫查詢操做,而關聯模型的出現則是幫助開發人員儘可能減小重複勞動。ThinkJS 中的關聯模型功能也一直是受到你們的好評的,不過對於沒有接觸過的新同窗有時候會不太懂如何配置。今天咱們請來了 ThinkJS 用戶 @lscho 同窗爲咱們分享一下他對於關聯模型的學習,但願可以幫助你們更好的理解 ThinkJS 中的關聯模型。javascript

前言

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

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

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

場景模擬

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

建立班級表web

CREATE TABLE `thinkjs_class` (
  `id` int(10) NOT NULL,
  `name` varchar(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
複製代碼

建立學生表sql

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;

複製代碼

而後咱們按照官網文檔關聯模型一一講起,若是不熟悉官網文檔建議先看一遍文檔。json

一對一

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

咱們能夠對學生表建立一個附表,用來存放學生我的信息以便咱們進行測試。

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 支持的參數。

一對一(屬於)

說完第一種一對一關係,咱們來講第二種一對一關係。上面的一對一關係是咱們指望查詢主表後獲得關聯表的數據。也就是主表的主鍵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) 有更多關於臨時開啓或關閉關聯關係的使用方法。

多對多

前面的一對1、一對多還算很容易理解,多對多就有點繞了。想象一下,每一個學生能夠加入不少社團,而社團一樣由不少學生組成。社團與學生的關係,就是一個多對多的關係。這種狀況下,兩張表已經沒法完成這個關聯關係了,須要增長一箇中間表來處理關聯關係

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:以上代碼放在github.com/lscho/think…

相關文章
相關標籤/搜索