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

sequelize 高级 m:n 关联——迹忆客-ag捕鱼王app官网

阅读本篇内容之前,请确保已阅读 关联

让我们从 user 和 profile 之间的多对多关系示例开始。

const user = sequelize.define('user', {
  username: datatypes.string,
  points: datatypes.integer
}, { timestamps: false });
const profile = sequelize.define('profile', {
  name: datatypes.string
}, { timestamps: false });

定义多对多关系的最简单方法是:

user.belongstomany(profile, { through: 'user_profiles' });
profile.belongstomany(user, { through: 'user_profiles' });

通过将字符串传递给上面的 through,我们要求 sequelize 自动生成名为 user_profiles 的模型作为 联结表,该模型只有两列: useridprofileid。 在这两个列上将建立一个复合唯一键。

我们还可以为自己定义一个模型,以用作联结表。

const user_profile = sequelize.define('user_profile', {}, { timestamps: false });
user.belongstomany(profile, { through: user_profile });
profile.belongstomany(user, { through: user_profile });

以上具有完全相同的效果。 注意:我们没有在 user_profile 模型上定义任何属性。 我们将其传递给 belongstomany 调用的事实告诉 sequelize 自动创建两个属性 userid 和 profileid,就像其他关联一样,也会导致 sequelize 自动向其中一个涉及的模型添加列。

然而,自己定义模型有几个优点。 例如,我们可以在联结表中定义更多列:

const user_profile = sequelize.define('user_profile', {
  selfgranted: datatypes.boolean
}, { timestamps: false });
user.belongstomany(profile, { through: user_profile });
profile.belongstomany(user, { through: user_profile });

这样,我们现在可以在联结表中跟踪额外的信息,即 selfgranted 布尔值。 例如,当调用 user.addprofile() 时,我们可以使用 through 参数传递额外列的值。

示例:

const amidala = await user.create({ username: 'p4dm3', points: 1000 });
const queen = await profile.create({ name: 'queen' });
await amidala.addprofile(queen, { through: { selfgranted: false } });
const result = await user.findone({
  where: { username: 'p4dm3' },
  include: profile
});
console.log(result);

输出:

{
  "id": 4,
  "username": "p4dm3",
  "points": 1000,
  "profiles": [
    {
      "id": 6,
      "name": "queen",
      "user_profile": {
        "userid": 4,
        "profileid": 6,
        "selfgranted": false
      }
    }
  ]
}

我们也可以在单个 create 调用中创建所有关系。

示例:

const amidala = await user.create({
  username: 'p4dm3',
  points: 1000,
  profiles: [{
    name: 'queen',
    user_profile: {
      selfgranted: true
    }
  }]
}, {
  include: profile
});
const result = await user.findone({
  where: { username: 'p4dm3' },
  include: profile
});
console.log(result);

输出:

{
  "id": 1,
  "username": "p4dm3",
  "points": 1000,
  "profiles": [
    {
      "id": 1,
      "name": "queen",
      "user_profile": {
        "selfgranted": true,
        "userid": 1,
        "profileid": 1
      }
    }
  ]
}

你可能已经注意到 user_profiles 表中没有 id 字段。 如上所述,它具有复合唯一键。 该复合唯一密钥的名称由 sequelize 自动选择,但可以使用 uniquekey 参数进行自定义:

user.belongstomany(profile, { through: user_profiles, uniquekey: 'my_custom_unique' });

如果需要的话,另一种可能是强制联结表像其他标准表一样具有主键。 为此,只需在模型中定义主键:

const user_profile = sequelize.define('user_profile', {
  id: {
    type: datatypes.integer,
    primarykey: true,
    autoincrement: true,
    allownull: false
  },
  selfgranted: datatypes.boolean
}, { timestamps: false });
user.belongstomany(profile, { through: user_profile });
profile.belongstomany(user, { through: user_profile });

上面的代码当然仍然会创建两列 userid 和 profileid,但是模型不会在其上设置复合唯一键,而是将其 id 列用作主键。 其他一切仍然可以正常工作。


联结表与普通表以及"超级多对多关联"

现在,我们将比较上面显示的最后一个"多对多"设置与通常的"一对多"关系的用法,以便最后得出 超级多对多关系 的概念作为结论。

模型回顾 (有少量重命名)

为了使事情更容易理解,让我们将 user_profile 模型重命名为 grant。 请注意,所有操作均与以前相同。 我们的模型是:

const user = sequelize.define('user', {
  username: datatypes.string,
  points: datatypes.integer
}, { timestamps: false });
const profile = sequelize.define('profile', {
  name: datatypes.string
}, { timestamps: false });
const grant = sequelize.define('grant', {
  id: {
    type: datatypes.integer,
    primarykey: true,
    autoincrement: true,
    allownull: false
  },
  selfgranted: datatypes.boolean
}, { timestamps: false });

我们使用 grant 模型作为联结表在 user 和 profile 之间建立了多对多关系:

user.belongstomany(profile, { through: grant });
profile.belongstomany(user, { through: grant });

这会自动将 userid 和 profileid 列添加到 grant 模型中。

注意 : 如上所示,我们选择强制 grant 模型具有单个主键(通常称为 id)。 对于 超级多对多关系(即将定义),这是必需的。

改用一对多关系

除了建立上面定义的多对多关系之外,如果我们执行以下操作怎么办?

// 在 user 和 grant 之间设置一对多关系
user.hasmany(grant);
grant.belongsto(user);
// 在profile 和 grant 之间也设置一对多关系
profile.hasmany(grant);
grant.belongsto(profile);

结果基本相同! 这是因为 user.hasmany(grant)profile.hasmany(grant) 会分别自动将 userid 和 profileid 列添加到 grant 中。

这表明一个多对多关系与两个一对多关系没有太大区别。 数据库中的表看起来相同。

唯一的区别是你尝试使用 sequelize 执行预先加载时。

// 使用多对多方法,你可以:
user.findall({ include: profile });
profile.findall({ include: user });
// however, you can't do:
user.findall({ include: grant });
profile.findall({ include: grant });
grant.findall({ include: user });
grant.findall({ include: profile });
// 另一方面,通过双重一对多方法,你可以:
user.findall({ include: grant });
profile.findall({ include: grant });
grant.findall({ include: user });
grant.findall({ include: profile });
// however, you can't do:
user.findall({ include: profile });
profile.findall({ include: user });
// 尽管你可以使用嵌套 include 来模拟那些,如下所示:
user.findall({
  include: {
    model: grant,
    include: profile
  }
}); // 这模拟了 `user.findall({ include: profile })`,
    // 但是生成的对象结构有些不同.
    // 原始结构的格式为 `user.profiles[].grant`,
    // 而模拟结构的格式为 `user.grants[].profiles[]`.

两全其美:超级多对多关系

我们可以简单地组合上面显示的两种方法!

// 超级多对多关系
user.belongstomany(profile, { through: grant });
profile.belongstomany(user, { through: grant });
user.hasmany(grant);
grant.belongsto(user);
profile.hasmany(grant);
grant.belongsto(profile);

这样,我们可以进行各种预先加载:

// 全部可以使用:
user.findall({ include: profile });
profile.findall({ include: user });
user.findall({ include: grant });
profile.findall({ include: grant });
grant.findall({ include: user });
grant.findall({ include: profile });

我们甚至可以执行各种深层嵌套的 include:

user.findall({
  include: [
    {
      model: grant,
      include: [user, profile]
    },
    {
      model: profile,
      include: {
        model: user,
        include: {
          model: grant,
          include: [user, profile]
        }
      }
    }
  ]
});

别名和自定义键名

与其他关系类似,可以为多对多关系定义别名。

在继续之前,请回顾关联指南上的 belongsto 别名示例。 请注意,在这种情况下,定义关联影响 include 完成方式(即传递关联名称)和 sequelize 为外键选择的名称(在该示例中,leaderid 是在 ship 模型上创建的) 。

为一个 belongstomany 关联定义一个别名也会影响 include 执行的方式:

product.belongstomany(category, { as: 'groups', through: 'product_categories' });
category.belongstomany(product, { as: 'items', through: 'product_categories' });
// [...]
await product.findall({ include: category }); // 这无法使用
await product.findall({ // 通过别名这可以使用
  include: {
    model: category,
    as: 'groups'
  }
});
await product.findall({ include: 'groups' }); // 这也可以使用

但是,在此处定义别名与外键名称无关。 联结表中创建的两个外键的名称仍由 sequelize 基于关联的模型的名称构造。 通过检查上面示例中的穿透表生成的 sql,可以很容易看出这一点:

create table if not exists `product_categories` (
  `createdat` datetime not null,
  `updatedat` datetime not null,
  `productid` integer not null references `products` (`id`) on delete cascade on update cascade,
  `categoryid` integer not null references `categories` (`id`) on delete cascade on update cascade,
  primary key (`productid`, `categoryid`)
);

我们可以看到外键是 productid 和 categoryid。 要更改这些名称,sequelize 分别接受参数 foreignkey 和 otherkey(即,foreignkey 定义联结关系中源模型的 key,而 otherkey 定义目标模型中的 key):

product.belongstomany(category, {
  through: 'product_categories',
  foreignkey: 'objectid', // 替换 `productid`
  otherkey: 'typeid' // 替换 `categoryid`
});
category.belongstomany(product, {
  through: 'product_categories',
  foreignkey: 'typeid', // 替换 `categoryid`
  otherkey: 'objectid' // 替换 `productid`
});

生成 sql:

create table if not exists `product_categories` (
  `createdat` datetime not null,
  `updatedat` datetime not null,
  `objectid` integer not null references `products` (`id`) on delete cascade on update cascade,
  `typeid` integer not null references `categories` (`id`) on delete cascade on update cascade,
  primary key (`objectid`, `typeid`)
);

如上所示,当使用两个 belongstomany 调用定义多对多关系时(这是标准方式),应在两个调用中适当地提供 foreignkey 和 otherkey 参数。 如果仅在一个调用中传递这些参数,那么 sequelize 行为将不可靠。


自参照

sequelize 直观地支持自参照多对多关系:

person.belongstomany(person, { as: 'children', through: 'personchildren' })
// 这将创建表 personchildren,该表存储对象的 id.

从联结表中指定属性

默认情况下,当预先加载多对多关系时,sequelize 将以以下结构返回数据(基于本指南中的第一个示例):

// user.findone({ include: profile })
{
  "id": 4,
  "username": "p4dm3",
  "points": 1000,
  "profiles": [
    {
      "id": 6,
      "name": "queen",
      "grant": {
        "userid": 4,
        "profileid": 6,
        "selfgranted": false
      }
    }
  ]
}

注意,外部对象是一个 user,它具有一个名为 profiles 的字段,该字段是 profile 数组,因此每个 profile 都带有一个名为 grant 的额外字段,这是一个 grant 实例。当从多对多关系预先加载时,这是 sequelize 创建的默认结构。

但是,如果只需要联结表的某些属性,则可以在 attributes 参数中为数组提供所需的属性。 例如,如果只需要穿透表中的 selfgranted 属性:

user.findone({
  include: {
    model: profile,
    through: {
      attributes: ['selfgranted']
    }
  }
});

输出:

{
  "id": 4,
  "username": "p4dm3",
  "points": 1000,
  "profiles": [
    {
      "id": 6,
      "name": "queen",
      "grant": {
        "selfgranted": false
      }
    }
  ]
}

如果你根本不想使用嵌套的 grant 字段,请使用 attributes: []:

user.findone({
  include: {
    model: profile,
    through: {
      attributes: []
    }
  }
});

输出:

{
  "id": 4,
  "username": "p4dm3",
  "points": 1000,
  "profiles": [
    {
      "id": 6,
      "name": "queen"
    }
  ]
}

如果你使用 mixins(例如 user.getprofiles())而不是查找器方法(例如 user.findall()),则必须使用 jointableattributes 参数:

someuser.getprofiles({ jointableattributes: ['selfgranted'] });

输出:

[
  {
    "id": 6,
    "name": "queen",
    "grant": {
      "selfgranted": false
    }
  }
]

多对多对多关系及更多

思考你正在尝试为游戏锦标赛建模。 有玩家和团队。 团队玩游戏。 然而,玩家可以在锦标赛中(但不能在比赛中间)更换团队。 因此,给定一个特定的游戏,有某些团队参与该游戏,并且每个团队都有一组玩家(针对该游戏)。

因此,我们首先定义三个相关模型:

const player = sequelize.define('player', { username: datatypes.string });
const team = sequelize.define('team', { name: datatypes.string });
const game = sequelize.define('game', { name: datatypes.integer });

现在的问题是:如何关联它们?

首先,我们注意到:

  • 一个游戏有许多与之相关的团队(正在玩该游戏的团队);
  • 一个团队可能参加了许多比赛.

以上观察表明,我们需要在 game 和 team 之间建立多对多关系。 让我们使用本指南前面解释的超级多对多关系:

// game 与 team 之间的超级多对多关系
const gameteam = sequelize.define('gameteam', {
  id: {
    type: datatypes.integer,
    primarykey: true,
    autoincrement: true,
    allownull: false
  }
});
team.belongstomany(game, { through: gameteam });
game.belongstomany(team, { through: gameteam });
gameteam.belongsto(game);
gameteam.belongsto(team);
game.hasmany(gameteam);
team.hasmany(gameteam);

关于玩家的部分比较棘手。 我们注意到,组成一个团队的一组球员不仅取决于团队,还取决于正在考虑哪个游戏。 因此,我们不希望玩家与团队之间存在多对多关系。 我们也不希望玩家与游戏之间存在多对多关系。 除了将玩家与任何这些模型相关联之外,我们需要的是玩家与 团队-游戏约束 之类的关联,因为这是一对(团队加游戏)来定义哪些玩家属于那里。因此,我们正在寻找的正是联结模型gameteam本身!并且,我们注意到,由于给定的 游戏-团队 指定了许多玩家,而同一位玩家可以参与许多 游戏-团队,因此我们需要玩家之间的多对多关系和gameteam!

为了提供最大的灵活性,让我们在这里再次使用"超级多对多"关系构造:

// player 与 gameteam 之间的超级多对多关系
const playergameteam = sequelize.define('playergameteam', {
  id: {
    type: datatypes.integer,
    primarykey: true,
    autoincrement: true,
    allownull: false
  }
});
player.belongstomany(gameteam, { through: playergameteam });
gameteam.belongstomany(player, { through: playergameteam });
playergameteam.belongsto(player);
playergameteam.belongsto(gameteam);
player.hasmany(playergameteam);
gameteam.hasmany(playergameteam);

上面的关联正是我们想要的。 这是一个完整的可运行示例:

const { sequelize, op, model, datatypes } = require('sequelize');
const sequelize = new sequelize('sqlite::memory:', {
  define: { timestamps: false } // 在这个例子中只是为了减少混乱
});
const player = sequelize.define('player', { username: datatypes.string });
const team = sequelize.define('team', { name: datatypes.string });
const game = sequelize.define('game', { name: datatypes.integer });
// 我们在 game 和 team 游戏和团队之间应用超级多对多关系
const gameteam = sequelize.define('gameteam', {
  id: {
    type: datatypes.integer,
    primarykey: true,
    autoincrement: true,
    allownull: false
  }
});
team.belongstomany(game, { through: gameteam });
game.belongstomany(team, { through: gameteam });
gameteam.belongsto(game);
gameteam.belongsto(team);
game.hasmany(gameteam);
team.hasmany(gameteam);
// 我们在 player 和 gameteam 游戏和团队之间应用超级多对多关系
const playergameteam = sequelize.define('playergameteam', {
  id: {
    type: datatypes.integer,
    primarykey: true,
    autoincrement: true,
    allownull: false
  }
});
player.belongstomany(gameteam, { through: playergameteam });
gameteam.belongstomany(player, { through: playergameteam });
playergameteam.belongsto(player);
playergameteam.belongsto(gameteam);
player.hasmany(playergameteam);
gameteam.hasmany(playergameteam);
(async () => {
  await sequelize.sync();
  await player.bulkcreate([
    { username: 's0me0ne' },
    { username: 'empty' },
    { username: 'greenhead' },
    { username: 'not_spock' },
    { username: 'bowl_of_petunias' }
  ]);
  await game.bulkcreate([
    { name: 'the big clash' },
    { name: 'winter showdown' },
    { name: 'summer beatdown' }
  ]);
  await team.bulkcreate([
    { name: 'the martians' },
    { name: 'the earthlings' },
    { name: 'the plutonians' }
  ]);
  // 让我们开始定义哪些球队参加了哪些比赛.
  // 这可以通过几种方式来完成,例如在每个游戏上调用`.setteams`.
  // 但是,为简便起见,我们将直接使用 `create` 调用,
  // 直接引用我们想要的 id. 我们知道 id 是从 1 开始的.
  await gameteam.bulkcreate([
    { gameid: 1, teamid: 1 },   // 该 gameteam 将获得 id 1
    { gameid: 1, teamid: 2 },   // 该 gameteam 将获得 id 2
    { gameid: 2, teamid: 1 },   // 该 gameteam 将获得 id 3
    { gameid: 2, teamid: 3 },   // 该 gameteam 将获得 id 4
    { gameid: 3, teamid: 2 },   // 该 gameteam 将获得 id 5
    { gameid: 3, teamid: 3 }    // 该 gameteam 将获得 id 6
  ]);
  // 现在让我们指定玩家.
  // 为简便起见,我们仅在第二场比赛(winter showdown)中这样做.
  // 比方说,s0me0ne 和 greenhead 效力于 martians,
  // 而 not_spock 和 bowl_of_petunias 效力于 plutonians:
  await playergameteam.bulkcreate([
    // 在 'winter showdown' (即 gameteamids 3 和 4)中:
    { playerid: 1, gameteamid: 3 },   // s0me0ne played for the martians
    { playerid: 3, gameteamid: 3 },   // greenhead played for the martians
    { playerid: 4, gameteamid: 4 },   // not_spock played for the plutonians
    { playerid: 5, gameteamid: 4 }    // bowl_of_petunias played for the plutonians
  ]);
  // 现在我们可以进行查询!
  const game = await game.findone({
    where: {
      name: "winter showdown"
    },
    include: {
      model: gameteam,
      include: [
        {
          model: player,
          through: { attributes: [] } // 隐藏结果中不需要的 `playergameteam` 嵌套对象
        },
        team
      ]
    }
  });
  console.log(`found game: "${game.name}"`);
  for (let i = 0; i < game.gameteams.length; i  ) {
    const team = game.gameteams[i].team;
    const players = game.gameteams[i].players;
    console.log(`- team "${team.name}" played game "${game.name}" with the following players:`);
    console.log(players.map(p => `--- ${p.username}`).join('\n'));
  }
})();

输出:

found game: "winter showdown"
- team "the martians" played game "winter showdown" with the following players:
--- s0me0ne
--- greenhead
- team "the plutonians" played game "winter showdown" with the following players:
--- not_spock
--- bowl_of_petunias

因此,这就是我们利用超级多对多关系技术在 sequelize 中实现三个模型之间的 多对多对多 关系的方式!

这个想法可以递归地应用于甚至更复杂的,多对多对......对多 关系(尽管有时查询可能会变慢)。

查看笔记

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