【问题标题】:Nested mysql transactions between 2 tables using sequelize.js使用 sequelize.js 在 2 个表之间嵌套 mysql 事务
【发布时间】:2015-01-13 16:54:47
【问题描述】:

我有 2 个续集模型(与 2 个表关联的事件和库存)。我创建了一个 Event._create 方法,以便我可以使用它在事件数据库中创建一个事件,同时在库存数据库中记录多个产品。每个库存都与新创建事件的 event_id 相关联。

因为所有这些东西应该完全成功或失败,所以我使用 sequelize 的事务来实现这一点。

最初我正在考虑做这样的事情。

sequelize.transactionPromise = Promise.promisify(sequelize.transaction, sequelize);

return sequelize.transactionPromise({autocommit: 0})
.then(function(t) {
    return Event.create(ev, {transaction: t})
    .then(function(event){
        var event_id = event.id;  // ------ (*)
        return Promise.resolve([1, ..., event_number])
        .then(function(){
            Inventory.create({product_id: some_product_id, event_id: event_id}, 
            {transaction: t});
        });
    .then(function(){ 
        return Promise.cast(t.commit())
        .then(function() { // successfully committed  
            return res.json(d);
        }).catch(function(err){ // cannot commit somehow
            return res.json(500, err.toString());
        });
    }).catch(function(err){  // error rollback
        return Promise.cast(t.rollback())
        .then(function() {
            return Promise.reject('rollback: ' + err.toString());
        });
    });
});

但这不起作用,因为在事务提交之前 (*) 没有任何值,并且给我的 event_id 为 NULL。

相反,我会执行以下操作:

var Event = sequelize.model('Event'); 
var Inventory = sequelize.model('Inventory');

var _create = function(t, ev){
    var ev_id_secret = {secret: 'some random secret'};
    return Promise.cast(Event.create(ev_id_secret))
    .then(function(d){
        ev_id_secret.id = d.id;
        return true;
    }).then(function(){
        return Promise.resolve(_.range(ev.number_of_products))
        .map(function(){
            var inventory = {
                event_id: ev_id_secret.id,
                product_id: ev.product_id
            };
            return Promise.cast(Inventory._create(t, inventory));
        });
    }).then(function(){  // thennable a transaction
            return Promise.cast(Event.update(ev, ev_id_secret, {transaction: t}));
    });
};

所以我可以做这样的事情。

sequelize.transactionPromise=Promise.promisify(sequelize.transaction, sequelize); 
return sequelize.transactionPromise({autocommit: 0})
.then(function(t) {
    return Event._create(t, ev)
    .then(function(){
        return Promise.cast(t.commit())
        .then(function() {
            return res.json(d);
        }).catch(function(err){
            return res.json(500, err.toString());
        });
    }).catch(function(err){
        return Promise.cast(t.rollback())
        .then(function() {
            return Promise.reject('rollback: ' + err.toString());
        });
    });
}).catch(function(err){
    console.log(err.stack);
    res.json(500, {error: err.toString()});
});

我对 _create 所做的只是在 Event db 中插入一个空事件(具有随机生成的秘密),在 Inventory db 中插入一些空产品,然后使用此秘密获取 event_id 来查询、相应地更新事件和库存。

当 promise 被拒绝时,事务 rollback() 被调用并在数据库中留下空的事件和产品记录。所以我必须稍后处理空记录,这真的很烦人。

所以我的问题是如何正确地在 2 个表之间进行事务处理?我在正确的轨道上吗?

p.s:作为一个附带问题,您可以看到我的代码充满了 return Promise.xxx 语句,这确保了控制流,但承诺变得非常混乱。我可以做些什么来改进我的代码吗?谢谢。

【问题讨论】:

  • 您使用的是哪个承诺库?另外,您能否发布所有代码,包括外部承诺包装器?
  • 承诺是 bluebird.js。 mysql ORM 是 sequelize.js

标签: mysql database transactions promise sequelize.js


【解决方案1】:

您最初的实现是正确的。但是,在您创建事件之后,该对象应该返回已经填充的 id 而无需提交(我假设您使用的是标准的自动递增 sequelize id)。所有 SQL 数据库都以这种方式运行。我建议调试您的应用程序以确定问题的根源。

这是我的一个应用程序中的 sequelize transaction 的示例实现。我刚刚测试并确认调用 db.Deposit.create() 解析为具有有效 id 的对象。我正在使用 Bluebird、Sequelize 2.0.0-rc2 和 Postgresql 9.3

module.exports.createDeposit = function(deposit) {
  return db.sequelize.transaction({isolationLevel: 'READ COMMITTED' })
  .then(function(t){
    var strQuery = 'UPDATE "Accounts" '
                   + 'SET "balance"="balance" + :amount, "updatedAt"=NOW() '
                   + 'WHERE "id"= :AccountId RETURNING *';

    return sql.query(strQuery,
                     db.Account.build(),
                     { raw: true, transaction : t },
                     { AccountId: deposit.AccountId, amount: deposit.amount })

    .then(function(account){
      if (!account) throw new Error('Account does not exist')
      return db.Deposit.create(deposit, { transaction: t });
    })
    .then(function(dbDeposit){
      // If successful, dbDeposit object contains a valid id
      if (!dbDeposit) throw new Error('Failed to create deposit');
      return t.commit()
      .then(function(){
        return dbDeposit;
      });
    })
    .catch(function(e){
      return t.rollback()
      .then(function(){
        throw e;
      });
    });
  });
};

【讨论】:

  • 是的,我做了一些研究,确实 .create 给了我一些带有 id 的东西。但是我能保证我不会遇到比赛条件吗?
  • 您不能使用 .create() 遇到竞争条件。当您调用 .create 时,sequelize 会发出一个插入命令,该命令会增加序列 id 键,因此分配给该对象的 id 是原子的。即使您通过回滚中止事务,也无法为其他对象分配该 ID,这是 SQL 数据库工作方式的一部分。
  • 如果我想创建一条记录,并立即在同一个事务中更新它。这行得通吗?
  • 是的,这很好。例如,您可以创建一条记录,更新另一个表中的其他记录,然后更新您创建的第一条记录,所有这些都在同一个事务中。
  • 让我再测试一下。
【解决方案2】:

据我所知,您的原始实现是正确的,但可以通过在最新版本的 Sequelize 中利用托管事务和完全承诺支持来改进:

return sequelize.transaction({autocommit: false}, function(t) {
  return Event.create(ev, {transaction: t}).then(function(event) {
    return Promise.map(_.range(event_number), function (number) {
      return Inventory.create({product_id: some_product_id, event_id: event.get('id')}, {transaction: t});
    });
  });
}).then(function(){
  // Automatically comitted at this point if the promise chain returned to the transaction successfully resolved
  return res.status(200).json(d);
}).catch(function(err){
  // Automatically rolledback at this point if the promise chain returned to the transaction was rejected
  return res.status(403).json();
});

您可以在文档中阅读有关托管事务的更多信息:http://sequelize.readthedocs.org/en/latest/docs/transactions/

【讨论】:

  • 没有比这更正式的了 :) 很高兴您喜欢事务语法。
  • 我的版本也是 "version": "2.0.0-rc2",但是没有 t.commit() sql 语句仍然不会以某种方式提交..
  • 在 2.0.0-rc1 中引入。你还在使用.then() 还是实现了我的vode?​​span>
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-30
  • 2014-08-21
  • 2020-04-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多