谈谈sequelize的include

博客园的写文章方式太不友好了,也不好看,可以访问自己写的静态页面查看新博客地址

也可以直接在仓库的issue查看

 

概述

文章主要讨论最近在使用sequelize和mysql的一些总结,有些细节在sequelize文档上并没有,也是经验总结。同时,也是以前端的角度对mysql的思考,不一定是专业,但是是值得讨论思考的。还希望,大家可以提出问题和建议。

什么是sequelize

sequelize 是node下面的一个关系型数据库orm框架。提供了人性化的接口去写sql。简单说,就是告别 select * from person;烦人的sql语句。采用 Person.findAll({}); 这样形式是操作数据库。

内连接,左外连接,右外连接

  • 内连接:两个表都满足条件的情况下,返回公有数据
  • 左外连接: 左表所有数据,右表匹配不到的数据为null
  • 右外连接:保证右表所有数据,左表匹配不到的数据null填充

include

include是sequelize实现连表查询的一个语法。至于具体细节,还请参考官方文档。这不是具体讨论的细节,我想记录的,是关于这样一个场景。

具体场景

 

这是一个很常见的场景,这里用购物车,商品,规格三种单元去描述。分别对应三张表Cart, Goods, Spec。 购物车和商品是多对多关系,可以理解成购物车是一个中间表,关联顾客和商品的中间表。但是在这个讨论中,顾客的层次可以忽略。所以,只要关注,Cart 有多个 Goods, Goods有多个Spec即可。 可以这样定义多对多关系(可以忽略代码, 作为参考)

Buyer.belongsToMany(Goods, {
      through: {
        model: Cart,
        unique: false, // 取消联合主键的约定
      },
      foreignKey: 'buyerId',
    });

Goods.belongsToMany(Buyer, {
  through: {
    model: Cart,
    unique: false,
  },
  foreignKey: 'goodId',
});

sequelize中,需要定义asscoiation 才能使用include关联。一个asscoiation包含一个关系加一个belongsTo 。其中,关系指的是hasMany(一对多), belongsToMany(多对多), hasOne(一对一)。假设上面的关系定义完毕。 就可以像这样获得所有的购物车:

    await Cart.findAll({
      attributes: ['id'],
      include: {
        attributes: [ 'id' ],
        model: Goods,
        include: {
          attributes: [ 'id' ],
          model: Spec
        }
      }
    })

对应转义sql语句(为了sql语句足够短,我加了attributes: [ 'id' ]的限制,只取id):

SELECT `cart`.`id`, `good`.`id` AS `good.id`, `good->specs`.`id` AS `good.specs.id` FROM `carts` AS `cart` LEFT OUTER JOIN `goods` AS `good` ON `cart`.`good_id` = `good`.`id` LEFT OUTER JOIN `specs` AS `good->specs` ON `good`.`id` = `good->specs`.`good_id`;

可以看到,默认情况下,include的方式是LEFT OUTER JOIN。就是左外级连的方式。 看关系图也知道,有一个Cart表有一个spec_id字段。这是一个必须的字段,对于购物车而言,它存储的最小单元应该是规格,而不是商品。对于没有规格的商品,这个字段可能为空,此时对应的最小单元应该是商品。 那我们为何不可以可以像这样去写这个include。

    const r2 = await Cart.findAll({
      attributes: ['id'],
      include: [
        {
          attributes: [ 'id' ],
          model: Goods,
        },
        {
          attributes: [ 'id' ],
          model: Spec,
        }
      ]
    })

对应sql :

SELECT `cart`.`id`, `good`.`id` AS `good.id`, `spec`.`id` AS `spec.id` FROM `carts` AS `cart` LEFT OUTER JOIN `goods` AS `good` ON `cart`.`good_id` = `good`.`id` LEFT OUTER JOIN `specs` AS `spec` ON `cart`.`spec_id` = `spec`.`id`;

第二种方式执行效率会更快一点,测试过程,每多一层join便会有十倍的差距。至于第二种写法,按照sequelize的要求,必须定义关于Spec和Cart两张表的association。从mysql的角度,如果A->B, B->C (意思是A和B是一对多的关系)。这样的关系,定义A->C。这样的关系是不允许的。所以,关于sequelize的定义,可以这样去写:

    Cart.belongsTo(Spec, {
      foreignKey: 'specId',
      constraints: false,
    });

contstraints: fallse 的意思是说,不建立数据库约束和索引。这样如果通过Model生成对应数据库表。也就不会有数据库约束的问题。 正常情况下,sequelize会根据define里面定义的foreignKey和targetKey去定义on clause(上面例子中的ON cart.spec_id = spec.id;)。 如果默认情况无法满足,则要自己定义on clause。在sequelize中可以这样写(写于include中):

on: {
   spec_id: {
     [Sequelize.Op.eq]: Sequelize.col('spec.id'),
  },
}

下面讲一下include中有条件的情况, 比如连接规格时候去掉已经删除掉的规格,则可以这样写:

    const r2 = await Cart.findAll({
      attributes: ['id'],
      include: [
        {
          attributes: [ 'id' ],
          model: Goods,
        },
        {
          attributes: [ 'id' ],
          model: Spec,
          where: {
            isDelete: false
          }
        }
      ]
    })

其实直接看是没有问题的,但是如果细心看下面的sql语句,会发现本来默认的左外连接变成了内连接。

SELECT `cart`.`id`, `good`.`id` AS `good.id`, `spec`.`id` AS `spec.id` F AS `cart` LEFT OUTER JOIN `goods` AS `good` ON `cart`.`good_id` = `good`.`id` INNER JOIN `specs` AS `spec` ON `car` = `spec`.`id` AND `spec`.`is_delete` = false;

有时候内连接是不符合数据要求的,这个例子正好说明情况,对于购物车的记录,购物车应该完全展示出来。所以,sequelize默认是左外连接,如果你有条件,它会给你变成内连接。这么做是有道理的,因为情况只返回内外条件都满足的数据。为了能够保持外连接,需要用到required属性,这个文档有说明: 只需要把写上required: false属性即可。

    const r2 = await Cart.findAll({
      attributes: ['id'],
      include: [
        {
          attributes: [ 'id' ],
          model: Goods,
        },
        {
          attributes: [ 'id' ],
          model: Spec,
          required: false,
          where: {
            isDelete: false
          }
        }
      ]
SELECT `cart`.`id`, `good`.`id` AS `good.id`, `spec`.`id` AS `spec.id` FROM `carts` AS `cart` LEFT OUTER JOIN `goods` AS `good` ON `cart`.`good_id` = `good`.`id` LEFT OUTER JOIN `specs` AS `spec` ON `cart`.`spec_id` = `spec`.`id` AND `spec`.`is_delete` = false;

这样就可以了。

总结

为什么要讨论这样的include案例,因为除了简单的查询,数据库连表查询是很频繁的。虽然sequelize对于复杂情况无法定制。但实际上,上面例子应该已经适应了大多数场景。另一方面,减少include层级可以有效提高sql语句的执行效率。这些东西文档上可能没有很好的描述,应用过程中也是很有用的。

posted @ 2018-08-01 16:40  Wyshon  阅读(5837)  评论(0编辑  收藏  举报