ThinkJS 關聯模型實踐

ThinkJS發表於2018-08-30

編者注:日常開發中少不了有大量的資料庫查詢操作,而關聯模型的出現則是幫助開發人員儘量減少重複勞動。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;

複製程式碼

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

一對一

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

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

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) 有更多關於臨時開啟或關閉關聯關係的使用方法。

多對多

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

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…

相關文章