在数据库设计中,尤其是关系型数据库设计中,我们的表之间会存在各种关联。在传统行业中,当用户数量有限且可控时,我们可以使用外键进行关联来降低开发成本,利用数据库产品本身的触发器来实现表与关联表之间的数据一致性和更新。但是在web开发中,不适合使用外键。因为在并发量比较大的情况下,数据库很可能成为性能瓶颈,受限于IO能力,不能轻易横向扩展,在程序中有很多限制。所以在web开发中,一般都是在应用程序中实现各种数据表之间的关联关系。在ThinkJS中,关联模型可以很好的解决这个问题。下面我们来了解一下关联模型在ThinkJS中的应用。场景模拟让我们模拟最常见的学生、班级、社团关系的场景。创建类表CREATETABLE`thinkjs_class`(`id`int(10)NOTNULL,`name`varchar(50)NOTNULL)ENGINE=InnoDBDEFAULTCHARSET=utf8;创建学生表CREATETABLE`thinkjs_student`(`id`int(10)NOTNULL,`class_id`int(10)NOTNULL,`name`varchar(20)NOTNULL)ENGINE=InnoDBDEFAULTCHARSET=utf8;CREATETABLE`thinkjs_club`(`id`int(10)NOTNULL,`name`varchar(50)NOTNULL)ENGINE=InnoDBDEFAULTCHARSET=utf8;那么我们就从官网文档关联模型开始一一介绍。如果不熟悉官网文档,建议先阅读文档。一对一很容易理解。很多时候一张表内容太多,我们会拆分成两张表。主表用于存储使用频率高的数据,副表用于存储使用频率低的数据。数据。我们可以为学生表创建一个补充表来存储学生的个人信息,以便我们进行测试。CREATETABLE`thinkjs_student_info`(`id`int(10)NOTNULL,`student_id`int(10)NOTNULL,`sex`varchar(10)NOTNULL,`age`int(2)UNSIGNEDNOTNULL)ENGINE=InnoDB默认字符集=utf8;与主表相比,外键是student_id,所以我们可以直接按照标准命名在学生模型文件中定义关联关系。//src/model/student.jsmodule.exports=classextendsthink.Model{getrelation(){return{student_info:think.Model.HAS_ONE};}}然后我们执行查询//src/controller/student.jsmodule.exports=classextendsthink.Controller{asyncindexAction(){conststudent=awaitthis.model('student').where({id:1})。寻找();返回this.success(student);}}可以得到主表和关联表的数据{"student":{"id":1,"class_id":1,"name":"WangXiaoming","student_info":{"id":1,"student_id":1,"sex":"male","age":13}}}查看控制台,我们会发现执行了两个查询[2018-08-27T23:06:33.760][41493][INFO]-SQL:SELECT*FROM`thinkjs_student`WHERE(`id`=1)LIMIT1,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.jsmodule.exports=classextendsthink.Model{getrelation(){return{info:{type:think.Model.HAS_ONE,model:'student_info',fKey:'student_id'}}}}再次执行查询,会发现返回数据中关联表中数据的key变成了info。当然,除了配置外键和模型名称,你还可以配置查询条件、排序规则,甚至分页。具体可以参考[model.relation](https://thinkjs.org/zh-cn/doc....一对一(属于)说完第一个一对一关系,再来说说第二种一对一关系Onerelationship,上面的一对一关系就是我们期望查询到主表后得到关联表的数据,也就是主键thinkjs_student.id的主表是附表的外键thinkjs_student_info.student_id,那么我们怎么通过外键找到呢?另外一张表的数据呢?这又是一对一的关系。例如,学生和班级之间的关系可以从我们上面创建的表中看出。在学生表中,我们将thinkjs_class.id与thinkjs_student.class_id相关联。我们在student模型中设置关联关系//src/model/student.jsmodule。exports=classextendsthink.Model{getrelation(){return{class:think.Model.BELONG_TO}}}查询后可以得到相关的关联数据{"student":{"id":1,"class_id":1,"name":"王晓明","class":{"id":1,"name":"三年二班"}}}同样的,我们也可以自定义数据和表格的key关联表的名称,查询条件等。一对多的关系也很容易理解。一个班级下有多个学生。如果我们在查询班级的时候想找出关联的学生信息,那么班级和学生之间的关系就是一对多的关系。.这时候需要在类model中设置模型关系//src/model/class.jsmodule.exports=classextendsthink.Model{getrelation(){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.jsmodule.exports=classextendsthink.Model{getrelation(){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关联,这样就陷入了死循环。我们从官网文档可以看到,有一个关系可以解决这个问题。那么我们修改一下上面student模型中的BELONG_TO关联//src/model/student.jsmodule.exports=classextendsthink.Model{getrelation(){return{class:{type:think.Model.BELONG_TO,relation:false}}}}这样就可以正常处理类模型的一对多关系了。如果我们想在student模型中继续使用BELONG_TO获取关联的表数据,只需要在代码中重新启用//src/controller/student.jsmodule.exports=classextendsthink.Controller{asyncrelationAction(){letstudent=awaitthis.model('student').setRelation('class').where({id:2}).find();返回this.success(student);}}官网文档model.setRelation(name,value)暂时开启或关闭关系的方法比较多。一对一和一对多在多对多面前还算好理解,多对多就有点迷糊了。想象一下,每个学生可以加入很多社团,而社团也是由很多学生组成的。俱乐部和学生之间的关系是多对多的关系。这种情况下,两张表之间的关系无法完成,需要增加一个中间表来处理关系CREATETABLE`thinkjs_student_club`(`id`int(10)NOTNULL,`student_id`int(10)NOTNULL,`club_id`int(10)NOTNULL)ENGINE=InnoDBDEFAULTCHARSET=utf8;根据文档中多对多关系的介绍,我们在student模型中关联clubs时,rModel是一个中间表,rfKey是club_id//src/model/student.jsmodule.exports=classextendsthink.Model{getrelation(){return{club:{type:think.Model.MANY_TO_MANY,rModel:'student_club',rfKey:'club_id'}}}}如果我们想在俱乐部模型中关联学生数据,只需更改rfKey到student_id。当然多对多也会遇到循环关联的问题。我们只需要在其中一个模型上设置relation:false。关联循环上面我们多次提到关联循环问题,我们试着从代码执行过程中来理解这个特性。think-model第30行可以看到,在构造方法中,会在this[RELATION]中放置一个Relation实例。RELATION是Symbol函数生成的Symbol类型的唯一值,应该用来实现这里私有属性的功能。然后跳过newRelation()做了什么,看一下模型中select的最终查询方法。在第576行,发现constdata=awaitthis.db().select(options);query执行完之后,调用了一个this.afterFind方法。this.afterFind方法调用上述Relation实例的afterFind方法returnthis[RELATION].afterFind(data);看到这里,我们通过命名就差不多知道大概的流程了:模型正常查询后,再处理关联模型的查询。我们继续追溯代码,看到Relation的afterFind方法又调用了this.getRelationData。this.getRelationData开始解析我们在model中设置的relation属性,通过循环调用parseItemRelation得到一个Promise对象,最后通过awaitPromise.all(promises)执行all;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。
