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…