教程 > sequelize 中文教程 > 阅读:32

sequelize 多态关联——迹忆客-ag捕鱼王app官网

注意 : 如本指南所述,在 sequelize 中使用多态关联时应谨慎行事。 不要只是从此处复制粘贴代码,否则你可能会容易出错并在代码中引入错误。 请确保你了解发生了什么。


概念

一个 多态关联 由使用同一外键发生的两个(或多个)关联组成。

例如,考虑模型 image, video 和 comment。 前两个代表用户可能发布的内容。 我们希望允许将评论放在两者中。 这样,我们立即想到建立以下关联:

  • image 和 comment 之间的一对多关联:
    image.hasmany(comment);
    comment.belongsto(image);
    
  • video 和 comment 之间的一对多关联:
    video.hasmany(comment);
    comment.belongsto(video);
    

但是,以上操作将导致 sequelize 在 comment 表上创建两个外键: imageidvideoid。 这是不理想的,因为这种结构使评论看起来可以同时附加到一个图像和一个视频上,这是不正确的。 取而代之的是,我们真正想要的是一个多态关联,其中一个 comment 指向一个 可评论,它是表示 image 或 video 之一的抽象多态实体。

在继续配置此类关联之前,让我们看看如何使用它:

const image = await image.create({ url: "https://placekitten.com/408/287" });
const comment = await image.createcomment({ content: "awesome!" });
console.log(comment.commentableid === image.id); // true
// 我们还可以检索与评论关联的可评论类型.
// 下面显示了相关的可注释实例的模型名称.
console.log(comment.commentabletype); // "image"
// 我们可以使用多态方法来检索相关的可评论内容,
// 而不必关心它是图像还是视频.
const associatedcommentable = await comment.getcommentable();
// 在此示例中,`associatedcommentable` 与 `image` 是同一件事:
const isdeepequal = require('deep-equal');
console.log(isdeepequal(image, commentable)); // true

配置一对多多态关联

要为上述示例(这是一对多多态关联的示例)设置多态关联,我们需要执行以下步骤:

  • 在 comment 模型中定义一个名为 commentabletype 的字符串字段;
  • 在 image/video 和 comment 之间定义 hasmany 和 belongsto 关联;
    • 禁用约束(即使用 { constraints: false }),为同一个外键引用了多个表;
    • 指定适当的 关联作用域;
  • 为了适当地支持延迟加载,请在 comment 模型上定义一个名为 getcommentable 的新实例方法,该方法在后台调用正确的 mixin 来获取适当的注释对象;
  • 为了正确支持预先加载,请在 comment 模型上定义一个 afterfind hook,该 hook 将在每个实例中自动填充 commentable 字段;
  • 为了防止预先加载的 bug/错误,你还可以在相同的 afterfind hook 中从 comment 实例中删除具体字段 image 和 video,仅保留抽象的 commentable 字段可用。

这是一个示例:

// helper 方法
const uppercasefirst = str => `${str[0].touppercase()}${str.substr(1)}`;
class image extends model {}
image.init({
  title: datatypes.string,
  url: datatypes.string
}, { sequelize, modelname: 'image' });
class video extends model {}
video.init({
  title: datatypes.string,
  text: datatypes.string
}, { sequelize, modelname: 'video' });
class comment extends model {
  getcommentable(options) {
    if (!this.commentabletype) return promise.resolve(null);
    const mixinmethodname = `get${uppercasefirst(this.commentabletype)}`;
    return this[mixinmethodname](options);
  }
}
comment.init({
  title: datatypes.string,
  commentableid: datatypes.integer,
  commentabletype: datatypes.string
}, { sequelize, modelname: 'comment' });
image.hasmany(comment, {
  foreignkey: 'commentableid',
  constraints: false,
  scope: {
    commentabletype: 'image'
  }
});
comment.belongsto(image, { foreignkey: 'commentableid', constraints: false });
video.hasmany(comment, {
  foreignkey: 'commentableid',
  constraints: false,
  scope: {
    commentabletype: 'video'
  }
});
comment.belongsto(video, { foreignkey: 'commentableid', constraints: false });
comment.addhook("afterfind", findresult => {
  if (!array.isarray(findresult)) findresult = [findresult];
  for (const instance of findresult) {
    if (instance.commentabletype === "image" && instance.image !== undefined) {
      instance.commentable = instance.image;
    } else if (instance.commentabletype === "video" && instance.video !== undefined) {
      instance.commentable = instance.video;
    }
    // 防止错误:
    delete instance.image;
    delete instance.datavalues.image;
    delete instance.video;
    delete instance.datavalues.video;
  }
});

由于 commentableid 列引用了多个表(本例中为两个表),因此我们无法向其添加 references 约束。 这就是为什么使用 constraints: false 参数的原因。

注意,在上面的代码中:

  • image -> comment 关联定义了一个关联作用域: { commentabletype: 'image' }
  • video -> comment 关联定义了一个关联作用域: { commentabletype: 'video' }

使用关联函数时,这些作用域会自动应用(如关联作用域指南中所述)。 以下是一些示例及其生成的 sql 语句:

  • image.getcomments():

    select "id", "title", "commentabletype", "commentableid", "createdat", "updatedat"
    from "comments" as "comment"
    where "comment"."commentabletype" = 'image' and "comment"."commentableid" = 1;
    

    在这里我们可以看到 comment.commentabletype = 'image' 已自动添加到生成的 sql 的 where 子句中. 这正是我们想要的行为.

  • image.createcomment({ title: 'awesome!' }):

    insert into "comments" (
    "id", "title", "commentabletype", "commentableid", "createdat", "updatedat"
    ) values (
    default, 'awesome!', 'image', 1,
    '2018-04-17 05:36:40.454  00:00', '2018-04-17 05:36:40.454  00:00'
    ) returning *;
    
  • image.addcomment(comment):

    update "comments"
    set "commentableid"=1, "commentabletype"='image', "updatedat"='2018-04-17 05:38:43.948  00:00'
    where "id" in (1)
    

多态延迟加载

comment 上的 getcommentable 实例方法为延迟加载相关的 commentable 提供了一种抽象 - 无论注释属于 image 还是 video,都可以工作。

通过简单地将 commentabletype 字符串转换为对正确的 mixin( getimage 或 getvideo)的调用即可工作。

注意上面的 getcommentable 实现:

  • 不存在关联时返回 null;
  • 允许你将参数对象传递给 **getcommentable(options)**,就像其他任何标准 sequelize 方法一样。 对于示例,这对于指定 where 条件或 include 条件很有用。

多态预先加载

现在,我们希望对一个(或多个)注释执行关联的可评论对象的多态预先加载。 我们想要实现类似以下的东西:

const comment = await comment.findone({
  include: [ /* ... */ ]
});
console.log(comment.commentable); // 这是我们的目标

解决的办法是告诉 sequelize 同时包含图像和视频,以便上面定义的 afterfind hook可以完成工作,并自动向实例对象添加 commentable 字段,以提供所需的抽象。

示例:

const comments = await comment.findall({
  include: [image, video]
});
for (const comment of comments) {
  const message = `found comment #${comment.id} with ${comment.commentabletype} commentable:`;
  console.log(message, comment.commentable.tojson());
}

输出:

found comment #1 with image commentable: { id: 1,
  title: 'meow',
  url: 'https://www.jiyik.com/',
  createdat: 2019-12-26t15:04:53.047z,
  updatedat: 2019-12-26t15:04:53.047z }

注意 - 可能无效的 预先/延迟 加载!

注释 foo,其 commentableid 为 2,而 commentabletype 为 image。 然后 image a 和 video x 的 id 都恰好等于 2。从概念上讲,很明显,video x 与 foo 没有关联,因为即使其 id 为 2,foo 的 commentabletype 是 image,而不是 video。 然而,这种区分仅在 sequelize 的 getcommentable 和我们在上面创建的 hook 执行的抽象级别上进行。

这意味着如果在上述情况下调用 comment.findall({ include: video }),video x 将被预先加载到 foo 中。 幸运的是,我们的 afterfind hook将自动删除它,以帮助防止错误。 你了解发生了什么是非常重要的。

防止此类错误的最好方法是 不惜一切代价直接使用具体的访问器和mixin (例如 .image.getvideo().setimage() 等),总是喜欢我们创建的抽象,例如 .getcommentable().commentable。如果由于某种原因确实需要访问预先加载的 .image.video 请确保将其包装在类型检查中,例如 comment.commentabletype === 'image'


配置多对多多态关联

在上面的示例中,我们将模型 image 和 video 抽象称为 commentables,其中一个 commentable 具有很多注释。 但是,一个给定的注释将属于一个 commentable - 这就是为什么整个情况都是一对多多态关联的原因。

现在,考虑多对多多态关联,而不是考虑注释,我们将考虑标签。 为了方便起见,我们现在将它们称为 taggables,而不是将它们称为 commentables。 一个 taggable 可以具有多个标签,同时一个标签可以放置在多个 taggables 中。

为此设置如下:

  • 明确定义联结模型,将两个外键指定为 tagid 和 taggableid(这样,它是 tag 与 taggable 抽象概念之间多对多关系的联结模型);
  • 在联结模型中定义一个名为 taggabletype 的字符串字段;
  • 定义两个模型之间的 belongstomany 关联和 标签:
    • 禁用约束 (即, 使用 { constraints: false }), 因为同一个外键引用了多个表;
    • 指定适当的 关联作用域;
  • 在 tag 模型上定义一个名为 gettaggables 的新实例方法,该方法在后台调用正确的 mixin 来获取适当的 taggables。

实践:

class tag extends model {
  gettaggables(options) {
    const images = await this.getimages(options);
    const videos = await this.getvideos(options);
    // 在单个 taggables 数组中合并 images 和 videos
    return images.concat(videos);
  }
}
tag.init({
  name: datatypes.string
}, { sequelize, modelname: 'tag' });
// 在这里,我们明确定义联结模型
class tag_taggable extends model {}
tag_taggable.init({
  tagid: {
    type: datatypes.integer,
    unique: 'tt_unique_constraint'
  },
  taggableid: {
    type: datatypes.integer,
    unique: 'tt_unique_constraint',
    references: null
  },
  taggabletype: {
    type: datatypes.string,
    unique: 'tt_unique_constraint'
  }
}, { sequelize, modelname: 'tag_taggable' });
image.belongstomany(tag, {
  through: {
    model: tag_taggable,
    unique: false,
    scope: {
      taggabletype: 'image'
    }
  },
  foreignkey: 'taggableid',
  constraints: false
});
tag.belongstomany(image, {
  through: {
    model: tag_taggable,
    unique: false
  },
  foreignkey: 'tagid',
  constraints: false
});
video.belongstomany(tag, {
  through: {
    model: tag_taggable,
    unique: false,
    scope: {
      taggabletype: 'video'
    }
  },
  foreignkey: 'taggableid',
  constraints: false
});
tag.belongstomany(video, {
  through: {
    model: tag_taggable,
    unique: false
  },
  foreignkey: 'tagid',
  constraints: false
});

constraints: false 参数禁用引用约束,因为 taggableid 列引用了多个表,因此我们无法向其添加 references 约束。

注意下面:

  • image -> tag 关联定义了一个关联范围: { taggabletype: 'image' }
  • video -> tag 关联定义了一个关联范围: { taggabletype: 'video' }

使用关联函数时,将自动应用这些作用域。 以下是一些示例及其生成的 sql 语句:

image.gettags():

select
  `tag`.`id`,
  `tag`.`name`,
  `tag`.`createdat`,
  `tag`.`updatedat`,
  `tag_taggable`.`tagid` as `tag_taggable.tagid`,
  `tag_taggable`.`taggableid` as `tag_taggable.taggableid`,
  `tag_taggable`.`taggabletype` as `tag_taggable.taggabletype`,
  `tag_taggable`.`createdat` as `tag_taggable.createdat`,
  `tag_taggable`.`updatedat` as `tag_taggable.updatedat`
from `tags` as `tag`
inner join `tag_taggables` as `tag_taggable` on
  `tag`.`id` = `tag_taggable`.`tagid` and
  `tag_taggable`.`taggableid` = 1 and
  `tag_taggable`.`taggabletype` = 'image';

在这里我们可以看到 tag_taggable.taggabletype = 'image' 已被自动添加到生成的 sql 的 where 子句中。 这正是我们想要的行为。

tag.gettaggables():

select
  `image`.`id`,
  `image`.`url`,
  `image`.`createdat`,
  `image`.`updatedat`,
  `tag_taggable`.`tagid` as `tag_taggable.tagid`,
  `tag_taggable`.`taggableid` as `tag_taggable.taggableid`,
  `tag_taggable`.`taggabletype` as `tag_taggable.taggabletype`,
  `tag_taggable`.`createdat` as `tag_taggable.createdat`,
  `tag_taggable`.`updatedat` as `tag_taggable.updatedat`
from `images` as `image`
inner join `tag_taggables` as `tag_taggable` on
  `image`.`id` = `tag_taggable`.`taggableid` and
  `tag_taggable`.`tagid` = 1;
select
  `video`.`id`,
  `video`.`url`,
  `video`.`createdat`,
  `video`.`updatedat`,
  `tag_taggable`.`tagid` as `tag_taggable.tagid`,
  `tag_taggable`.`taggableid` as `tag_taggable.taggableid`,
  `tag_taggable`.`taggabletype` as `tag_taggable.taggabletype`,
  `tag_taggable`.`createdat` as `tag_taggable.createdat`,
  `tag_taggable`.`updatedat` as `tag_taggable.updatedat`
from `videos` as `video`
inner join `tag_taggables` as `tag_taggable` on
  `video`.`id` = `tag_taggable`.`taggableid` and
  `tag_taggable`.`tagid` = 1;

请注意,上述 gettaggables() 的实现允许你将选项对象传递给 getcommentable(options),就像其他任何标准 sequelize 方法一样。 例如,这对于指定条件或包含条件很有用。

在目标模型上应用作用域

在上面的示例中,scope 参数(例如 scope: { taggabletype: 'image' })应用于 联结 模型,而不是 目标 模型,因为它是在 through 下使用的参数。

我们还可以在目标模型上应用关联作用域。 我们甚至可以同时进行。

为了说明这一点,请考虑上述示例在标签和可标记之间的扩展,其中每个标签都有一个状态。 这样,为了获取图像的所有待处理标签,我们可以在 image 和 tag 之间建立另一个 belognstomany 关系,这一次在联结模型上应用作用域,在目标模型上应用另一个作用域:

image.belongstomany(tag, {
  through: {
    model: tag_taggable,
    unique: false,
    scope: {
      taggabletype: 'image'
    }
  },
  scope: {
    status: 'pending'
  },
  as: 'pendingtags',
  foreignkey: 'taggableid',
  constraints: false
});

这样,当调用 image.getpendingtags() 时,将生成以下 sql 查询:

select
  `tag`.`id`,
  `tag`.`name`,
  `tag`.`status`,
  `tag`.`createdat`,
  `tag`.`updatedat`,
  `tag_taggable`.`tagid` as `tag_taggable.tagid`,
  `tag_taggable`.`taggableid` as `tag_taggable.taggableid`,
  `tag_taggable`.`taggabletype` as `tag_taggable.taggabletype`,
  `tag_taggable`.`createdat` as `tag_taggable.createdat`,
  `tag_taggable`.`updatedat` as `tag_taggable.updatedat`
from `tags` as `tag`
inner join `tag_taggables` as `tag_taggable` on
  `tag`.`id` = `tag_taggable`.`tagid` and
  `tag_taggable`.`taggableid` = 1 and
  `tag_taggable`.`taggabletype` = 'image'
where (
  `tag`.`status` = 'pending'
);

我们可以看到两个作用域都是自动应用的:

  • tag_taggable.taggabletype = 'image' 被自动添加到 inner join;
  • tag.status = 'pending' 被自动添加到外部 where 子句。

查看笔记

扫码一下
查看教程更方便
网站地图