# 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设计规范