续集和羽毛:当关系破裂时

Sequelize and Feathers: When Relationships Fall Apart

在尝试弄清楚为什么我的 Sequelize 模型不致力于他们的关系两天之后,我决定是时候向大家征求意见了。

这是故事。

我正在使用以 Sequelize 作为驱动程序的 Postgres (9.4) 数据库编写 Feathers JS 应用程序。我 运行 通过 Feathers Docs 中的设置,并在一些引导下,我迁移到了 运行。

据我了解,必须特别考虑使用 Sequelize 获得双向关系,因为如果 ModelA 引用 ModelB,则必须已经定义 ModelB,但是如果ModelB 引用 ModelA...好吧,我们 运行 进入依赖循环。

这是因为文档对 "define your models using the method described here." 说的依赖循环(好吧,从技术上讲,它只是 "assumes" 使用了这样的结构。另外,我只能 post 2 links,否则我会 link 那个傻瓜。对此感到抱歉。)我在 Feathers demo.

中发现了相同的结构

自然地,我反映了所有这些(当然,除非我遗漏了一个小但重要的细节),但是......仍然没有骰子。

这是我正在查看的内容:

迁移

migrations/create-accounts.js

'use strict';

module.exports = {
  up: function (queryInterface, Sequelize) {
    // Make the accounts table if it doesn't already exist.
    // "If it doesn't already exist" because we have the previous migrations
    //  from Laravel.
    return queryInterface.showAllTables().then(function(tableNames) {
      if (tableNames.accounts === undefined) {
        queryInterface.createTable('accounts', {
          // Field definitions here
          id: {
            type: Sequelize.INTEGER,
            primaryKey: true,
            autoIncrement: true
          },
          name: Sequelize.STRING,
          url_name: Sequelize.STRING,
          createdAt: {
            type: Sequelize.DATE,
            allowNull: false
          },
          updatedAt: {
            type: Sequelize.DATE,
            allowNull: false
          },
          deletedAt: Sequelize.DATE
        });
      }
    });

    // See the create-user migration for an explanation of why I
    //  commented out the above code.
  },

  down: function (queryInterface, Sequelize) {
    return queryInterface.dropTable('accounts');
  }
};

migrations/create-users.js

'use strict';

module.exports = {
  up: function (queryInterface, Sequelize) {
    return queryInterface.showAllTables().then(function(tableNames) {
      if (tableNames.users === undefined) {
        queryInterface.createTable('users', {
          id: {
            type: Sequelize.INTEGER,
            primaryKey: true,
            autoIncrement: true
          },
          accountId: {
            type: Sequelize.INTEGER,
            references: {
              model: 'accounts',
              key: 'id'
            },
            allowNull: false
          },
          email: {
            type: Sequelize.STRING,
            allowNull: false
          },
          [...]
        });
      }
    });
  },

  down: function (queryInterface, Sequelize) {
    return queryInterface.dropTable('users');
  }
};

psql

然后我启动了 psql 来查看引用是否正确:

databaseName=# \d accounts:

Referenced by:
    TABLE "users" CONSTRAINT "users_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES accounts(id)

databaseName=# \d users:

Foreign-key constraints:
    "users_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES accounts(id)

到目前为止还不错吧?

让我们看看这个程序的模型部分!

型号

src/models/account.js

'use strict';

// account-model.js - A sequelize model
//
// See http://docs.sequelizejs.com/en/latest/docs/models-definition/
// for more of what you can do here.

const Sequelize = require('sequelize');

module.exports = function(app) {
  // We assume we're being called from app.configure();
  // If we're not, though, we need to be passed the app instance.
  // Fair warning: I added this bit myself, so it's suspect.
  if (app === undefined)
    app = this;
  const sequelize = app.get('sequelize');

  // The rest of this is taken pretty much verbatim from the examples
  const account = sequelize.define('account', {
    id: {
      type: Sequelize.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    name: Sequelize.STRING,
    url_name: Sequelize.STRING,
  }, {
    paranoid: true,
    timestamps: true,

    classMethods: {
      associate() {
        const models = app.get('models');
        this.hasMany(models['user'], {});
      }
    }
  });

  return account;
};

src/models/user.js

'use strict';

// user-model.js - A sequelize model
//
// See http://docs.sequelizejs.com/en/latest/docs/models-definition/
// for more of what you can do here.

const Sequelize = require('sequelize');

module.exports = function(app) {
  // We assume we're being called from app.configure();
  // If we're not, though, we need to be passed the app instance
  if (app === undefined)
    app = this;
  const sequelize = app.get('sequelize');

  const user = sequelize.define('user', {
    id: {
      type: Sequelize.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    accountId: {
      type: Sequelize.INTEGER,
      references: {
        model: 'accounts', // Table name...is that right? Made the migration work...
        key: 'id'
      }
    },
    email: Sequelize.STRING,
    [... curtailed for brevity ...]
  }, {
    // Are these necessary here, or just when defining the model to make a
    //  psuedo-migration?
    paranoid: true, // soft deletes
    timestamps: true,

    classMethods: {
      associate() {
        const models = app.get('models');
        // This outputs like I'd expect:
        // Just to be sure...From the user model, models["account"]: account
        console.log('Just to be sure...From the user model, models["account"]:', models['account']);
        this.belongsTo(models['account'], {});
      }
    }
  });

  return user;
};

src/models/index.js

// I blatantly ripped this from both the following:
// https://github.com/feathersjs/generator-feathers/issues/94#issuecomment-204165134
// https://github.com/feathersjs/feathers-demos/blob/master/examples/migrations/sequelize/src/models/index.js

const Sequelize = require('sequelize');
const _ = require('lodash');

// Import the models
const account = require('./account');
const user = require('./user');

module.exports = function () {
  const app = this;

  // Note: 'postgres' is found in config/default.json as the db url
  const sequelize = new Sequelize(app.get('postgres'), {
    dialect: app.get('db_dialect'),
    logging: console.log
  });
  app.set('sequelize', sequelize);

  // Configure the models
  app.configure(account);
  app.configure(user);

  app.set('models', sequelize.models);

  // Set associations
  Object.keys(sequelize.models).forEach(modelName => {
    if ('associate' in sequelize.models[modelName]) {
      sequelize.models[modelName].associate();
    }
  });

  sequelize.sync();

  // Extra credit: Check to make sure the two instances of sequelize.models are the same...
  // Outputs: sequelize.models after sync === app.get("models")
  // I've also run this comparison on sequelize and app.get('sequelize'); _.eq() said they also were identical
  if (_.eq(sequelize.models, app.get('models')))
    console.log('sequelize.models after sync === app.get("models")');
  else
    console.log('sequelize.models after sync !== app.get("models")');
};

齐心协力

src/app.js

为简洁起见,我从中删除了很多内容,我将模型加载到 app 中,如下所示:

const models = require('./models')
app.use(compress())
  // Lots of other statements
  .configure(models);

测试

我一直在尝试制作一个命令行实用程序来更改密码、修改用户权限和其他实用程序任务,所以我开始学习 Vorpal(同样,只有 2 links,所以你如果您不熟悉,则必须自己查找——抱歉)。以下是我的 Vorpal 程序的相关片段:

cli.js

const vorpal = require('vorpal')();
const _ = require('lodash');

// Initialize app
// This seems a bit overkill since we don't need the server bit for this, but...
const app = require('./src/app');
const models = app.get('models');

// Get the models for easy access...
const User = models['user'];
const Account = models['account'];

// Run by issuing the command: node cli test
// Outputs to terminal
vorpal.command('test', 'A playground for testing the Vorpal environment.')
  .action(function(args, callback) {
    // User.belongsTo(Account); // <-- uncomment this and it works
    User.findOne({ include: [{ model: Account }]}).then((user) => {
      console.log("user.account.name:", user.account.name);
    });
  });

vorpal.show().parse(process.argv);

问题

不好意思这么久才到这里,但我不知道这部分是相关的部分,所以我不得不吐出来。

运行 node cli test 给我一个错误

Just to be sure...From the user model, models["account"]: account
sequelize.models after sync === app.get("models")
connect: 
Unhandled rejection Error: account is not associated to user!
    at validateIncludedElement (/vagrant/node_modules/sequelize/lib/model.js:550:11)
    at /vagrant/node_modules/sequelize/lib/model.js:432:29
    at Array.map (native)
    at validateIncludedElements (/vagrant/node_modules/sequelize/lib/model.js:428:37)
    at .<anonymous> (/vagrant/node_modules/sequelize/lib/model.js:1364:32)
    at tryCatcher (/vagrant/node_modules/bluebird/js/release/util.js:16:23)
    at Promise._settlePromiseFromHandler (/vagrant/node_modules/bluebird/js/release/promise.js:504:31)
    at Promise._settlePromise (/vagrant/node_modules/bluebird/js/release/promise.js:561:18)
    at Promise._settlePromise0 (/vagrant/node_modules/bluebird/js/release/promise.js:606:10)
    at Promise._settlePromises (/vagrant/node_modules/bluebird/js/release/promise.js:685:18)
    at Async._drainQueue (/vagrant/node_modules/bluebird/js/release/async.js:138:16)
    at Async._drainQueues (/vagrant/node_modules/bluebird/js/release/async.js:148:10)
    at Immediate.Async.drainQueues (/vagrant/node_modules/bluebird/js/release/async.js:17:14)
    at runCallback (timers.js:574:20)
    at tryOnImmediate (timers.js:554:5)
    at processImmediate [as _immediateCallback] (timers.js:533:5)

啊!

但是,如果我取消注释 User.findOne() 正上方的行,它就像一个魅力。

为什么在查询关系之前必须立即显式设置关系?为什么在用户模型的 associate() 方法中建立的关系(大概)没有坚持?据我所知,它正在被调用——而且是在正确的模型上。它是否以某种方式被覆盖? app,出于某种奇怪的原因,在建立关联时在用户模型中与在 cli.js 中不一样吗?

我真的很莫名其妙。非常感谢你们能提供的任何帮助。

我不知道为什么会这样,但我确实通过进行以下更改使其正常工作。

src/models/index.js

我在导出函数末尾附近注释掉了以下块:

Object.keys(sequelize.models).forEach(modelName => {
  if ('associate' in sequelize.models[modelName]) {
    sequelize.models[modelName].associate();
  }
});

然后我把它移到 src/relate-models.js:

src/relate-models.js

/**
 * This is workaround for relating models.
 * I don't know why it works, but it does.
 *
 * @param app  The initialized app
 */
module.exports = function(app) {
  const sequelize = app.get('sequelize');

  // Copied this from src/models/index.js
  Object.keys(sequelize.models).forEach(modelName => {
    if ('associate' in sequelize.models[modelName]) {
      sequelize.models[modelName].associate();
    }
  });
}

src/app.js 中,我调用了该函数,然后... 转眼间,它就起作用了。

src/app.js`

const models = require('./models')
app.use(compress())
  // Lots of other statements
  .configure(models);

require('./relate-models')(app);

结束。如果有人能解释为什么以后做完全相同的事情会奏效,请告诉我,但现在......它奏效了。