在不假定外键列名称的情况下在 sequelize 中创建行时如何引用关联?

How do I reference an association when creating a row in sequelize without assuming the foreign key column name?

我有以下代码:

#!/usr/bin/env node
'use strict';
var Sequelize = require('sequelize');
var sequelize = new Sequelize('sqlite:file.sqlite');

var User = sequelize.define('User', { email: Sequelize.STRING});
var Thing = sequelize.define('Thing', { name: Sequelize.STRING});
Thing.belongsTo(User);

sequelize.sync({force: true}).then(function () {
  return User.create({email: 'asdf@example.org'});
}).then(function (user) {
  return Thing.create({
    name: 'A thing',
    User: user
  }, {
    include: [User]
  });
}).then(function (thing) {
  return Thing.findOne({where: {id: thing.id}, include: [User]});
}).then(function (thing) {
  console.log(JSON.stringify(thing));
});

我得到以下输出:

ohnobinki@gibby ~/public_html/turbocase1 $ ./sqltest.js
Executing (default): INSERT INTO `Users` (`id`,`email`,`updatedAt`,`createdAt`) VALUES (NULL,'asdf@example.org','2015-12-03 06:11:36.904 +00:00','2015-12-03 06:11:36.904 +00:00');
Executing (default): INSERT INTO `Users` (`id`,`email`,`createdAt`,`updatedAt`) VALUES (1,'asdf@example.org','2015-12-03 06:11:36.904 +00:00','2015-12-03 06:11:37.022 +00:00');
Unhandled rejection SequelizeUniqueConstraintError: Validation error
    at Query.formatError (/home/ohnobinki/public_html/turbocase1/node_modules/sequelize/lib/dialects/sqlite/query.js:231:14)
    at Statement.<anonymous> (/home/ohnobinki/public_html/turbocase1/node_modules/sequelize/lib/dialects/sqlite/query.js:47:29)
    at Statement.replacement (/home/ohnobinki/public_html/turbocase1/node_modules/sqlite3/lib/trace.js:20:31)

似乎指定 {include: [User]} 指示 Sequelize 创建一个新的 User 实例匹配 user 的内容。那不是我的目标。事实上,我很难相信这样的行为会有用——至少我没有用。我希望能够在数据库中拥有一个长期存在的 User 记录,并在任意时间创建新的 Thing 引用 User。在我显示的示例中,我等待创建 User,但在实际代码中它可能是通过 User.findOne().

重新加载的

我看到 and 说我必须在 Thing.create() 调用中明确指定隐式创建的 UserId 列。当 Sequelize 提供像 Thing.belongsTo(User) 这样的 API 时,我 不应该 必须知道创建 Thing.UserId 字段的事实。那么 什么是干净的 API-尊重创建新 Thing 的方法,它引用特定的 User 而不必猜测 UserId 的名称field? 当我加载一个 Thing 并指定 {include: [User]} 时,我通过 thing.User 属性 访问加载的用户。我认为我不应该知道或尝试访问 thing.UserId 字段。在我的 Thing.belongsTo(User) 调用中,我从不指定 UserId,我只是将其视为我不应该关心的实现细节。在创建 Thing?

时,如何继续避免关心该实现细节

Thing.create() 调用有效但在我看来是错误的:

Thing.create({
  name: 'A thing',
  UserId: user.id
});

选项 1 - 存在数据库不一致的风险

Sequelize 动态生成用于设置实例关联的方法,例如thing.setUser(user);。在您的用例中:

sequelize.sync({force: true})
.then(function () {
  return Promise.all([
    User.create({email: 'asdf@example.org'}),
    Thing.create({name: 'A thing'})  
  ]);
})
.spread(function(user, thing) {
  return thing.setUser(user);
})
.then(function(thing) {
  console.log(JSON.stringify(thing));
});

选项 2 - 不 work/buggy

它没有记录在案,但从代码潜水来看,我认为以下内容应该有效。不是,但这似乎是因为一些错误:

// ...
.then(function () {
  return models.User.create({email: 'asdf@example.org'});
})
.then(function(user) {
  // Fails with SequelizeUniqueConstraintError - the User instance inherits isNewRecord from the Thing instance, but it has already been saved
  return models.Thing.create({
    name: 'thingthing',
    User: user
  }, {
    include: [{
      model: models.User
    }],
    fields: ['name'] // seems nec to specify all non-included fields because of line 277 in instance.js - another bug?
  });
})

models.User.create替换为models.User.build不起作用,因为已构建但未保存的实例的主键为空。 Instance#_setInclude 如果实例的主键为空,则忽略该实例。

选项 3

在事务中包装事物的 create 可防止出现不一致的状态。

sq.sync({ force: true })
.then(models.User.create.bind(models.User, { email: 'asdf@example.org' }))
.then(function(user) {
  return sq.transaction(function(tr) {
    return models.Thing.create({name: 'A thing'})
    .then(function(thing) { return thing.setUser(user); });
  });
})
.then(print_result.bind(null, 'Thing with User...'))
.catch(swallow_rejected_promise.bind(null, 'main promise chain'))
.finally(function() {
  return sq.close();
});

我已经上传了演示选项 2 和选项 3 的脚本 here

sequelize@6.5.1 sqlite3@5.0.2 上测试我可以使用 User.associations.Comments.foreignKey 作为:

const Comment = sequelize.define('Comment', {
  body: { type: DataTypes.STRING },
});
const User = sequelize.define('User', {
  name: { type: DataTypes.STRING },
});
User.hasMany(Comment)
Comment.belongsTo(User)
console.dir(User);
await sequelize.sync({force: true});
const u0 = await User.create({name: 'u0'})
const u1 = await User.create({name: 'u1'})
await Comment.create({body: 'u0c0', [User.associations.Comments.foreignKey]: u0.id});

创建时也会返回关联,所以你还可以:

const Comments = User.hasMany(Comment)
await Comment.create({body: 'u0c0', [Comments.foreignKey]: u0.id});

在多对多的表中,你得到 foreignKeyotherKey 作为第二个外键。

User.associations.Comments.foreignKey 包含 foreignKey UserId.

或类似于别名:

User.hasMany(Post, {as: 'authoredPosts', foreignKey: 'authorId'});
Post.belongsTo(User, {as: 'author', foreignKey: 'authorId'});

User.hasMany(Post, {as: 'reviewedPosts', foreignKey: 'reviewerId'});
Post.belongsTo(User, {as: 'reviewer', foreignKey: 'reviewerId'});
await sequelize.sync({force: true});

// Create data.
const users = await User.bulkCreate([
  {name: 'user0'},
  {name: 'user1'},
])

const posts = await Post.bulkCreate([
  {body: 'body00', authorId: users[0].id, reviewerId: users[0].id},
  {body: 'body01', [User.associations.authoredPosts.foreignKey]: users[0].id, 
                   [User.associations.reviewedPosts.foreignKey]: users[1].id},
])

但是语法太长了,我很想在所有地方对键进行硬编码。