【问题标题】:Best way to prevent simultaneous model mutations in Laravel防止 Laravel 中同时发生模型突变的最佳方法
【发布时间】:2019-01-19 04:37:41
【问题描述】:

这可能是并发用户在同一应用中工作时最常见的问题之一。

用户 A 从数据库中获取模型,并在屏幕上显示给用户。他执行一些操作,并更新该对象的值。

在某个时间,它会保存模型。用户 A 几乎不知道,在他获取模型并保存修改后的模型之间,用户 B 已经获取了相同的模型,并将他的更改提交到数据库。结果:用户 B 的更改丢失。

我已经多次解决了同样的问题,通常是这样解决的:

1/给模型添加一个属性$version

2/ 在创建新模型时,我们设置$version = 1

3/ 取模型的时候,版本属性明显也取了。

4/保存修改后的模型时,我只保存... where id = $model->id AND WHERE version = $model->version,同时我正在更新$version = $version + 1

当该更新从数据库中抛出“没有更新记录”时,我只使用 id 搜索模型;如果我可以获取,我知道该模型存在,但版本不再匹配。然后我向用户提出一个异常(说'某人或某事已经修改了模型')

我相信这是确保您始终更新对象的最新版本的唯一正确方法。您显然也可以使用 updated_at 属性来比较您仍然拥有相同版本的对象,但理论上另一个用户可能已经更新了它。

现在我正在努力解决如何在 Laravel 中设置这个逻辑。我想创建一个特征,我可以在一个模型上使用它,该模型基本上可以完成我上面描述的工作,但我正在努力解决我真正应该做的事情。

我知道我可以覆盖 protected function setKeysForSaveQuery($query) 让它更新 where id = $this->id and version = $this->version 但是当我这样做时,laravel 没有更新任何东西,我没有得到允许我确定更新失败的异常,因为版本不匹配...

我想避免编写将保存封装在事务中的代码,并且每次保存模型时都必须验证版本;寻找一种方法来将其放入我的基础模型中,方法是包含一个特征,以便它自动运行

任何意见将不胜感激。

--添加示例:

想象一下:

$modelcopy1 = App\Model::find(1);  // version = 1
$modelcopy2 = App\Model::find(1);  // version = 1

$modelcopy1->save(); // this should update version in database to 2
                     // also $modelcopy1->version should now hold 2

$modelcopy2->save(); // this should now throw an exception
                     // because copy 1 still references version 1
                     // database version is already at 2

所以基本上,我需要->save() 中的逻辑来验证版本属性的模型值是否与当前数据库版本匹配,并且我需要同步,以便在更新完成时,模型版本属性更新为最新版本。

保存应该如下所示:

UPDATE Model
SET version = version + 1, attr1 = :attr1, attr2 = :attr2, ...
WHERE version = $model->version
AND id = $model->id

IF UPDATE 成功 --> 确保 $model->version 更新为新版本值

IF UPDATE 不成功 THEN SELECT * FROM Model WHERE id = $model->id

IF 找到模型,则 THROW ERROR '模型已被其他人更新' ELSE THROW ERROR '模型不存在'

我希望这是有道理的。在我看来,这样的锁定防止机制应该是任何“成人”框架的一部分,但似乎很难将其绑定到 Laravel 框架中......

【问题讨论】:

  • 这可能更适合codereview.stackexchange.com
  • 我还没有要查看的代码 - 我只是找不到合适的地方将其插入 laravel 框架...
  • 您是否在添加 trait 或让 trait 在子模型更新时遇到问题?
  • 添加特征不是问题。问题是我看不出在 Laravel 框架的哪个位置插入是最好的选择。我唯一看到的是编写自己的 performUpdatemethod 并覆盖 eloquent 的方法,但这听起来不像是一个持久的解决方案,因为它对未来版本的不兼容性非常敏感,我相信...
  • 我正在编辑问题以添加一些示例...

标签: php laravel


【解决方案1】:

所以你想编写一个 trait 来检查模型是否在收到当前模型后更新,并返回一些你可以挂钩的东西来通知用户?我会考虑制作一个您的所有模型都扩展的基本模型,并在该基本模型中使用模型事件。如果您只是让所有模型扩展基本模型,您甚至可能不需要特征。请参见下面的示例模型事件方法:

protected static function boot(){
    parent::boot();
    static::saving(function($thisModel){
        $changes = Model::where('version', $thisModel->version)
            ->where('id', $thisModel->id)->first()
        /** if changes->version matches this one, good, if not, set some
         *  property on the model that can be checked.   
         */
    }
}

这会发生模型正在保存,如果您在方法中的任何位置return false;,保存将失败并且模型将不会被保存。

如果您仍想使用 trait,请确保启动方法遵循以下格式:bootTraitName(),否则它不会在实现类的启动时启动。这是一个有很多不同角度的棘手问题,但我希望我至少给了你一些考虑。

编辑:我从未在基本模型上使用过模型事件,所以我不确定parent::boot(); 是否必要。可能需要测试。

【讨论】:

  • 您可以在saving 方法中使用$original = $thisModel->getOriginal(); 来获取更改前的值(在$thisModel 中)
  • 虽然这是可行的——这仅在包含在事务中并且记录被锁定时才有效。这将是我的“第二个选择”,我已经开发出来了。它只是不涵盖“所有”场景;最好的选择仍然是让更新语句本身检查版本是否匹配,因为现在您在此检查和实际更新之间仍然有一个 - 尽管很小 - 窗口,另一个进程仍然可以更新数据库版本。 @ljubadr 的建议也很好,但是当你有冗长的过程时可能会成为一个真正的问题......
  • 另外,我还想牢记性能 - 您的建议会在每次保存操作时触发一个额外的查询...
  • 我们仍然会使用一个特征;我们不需要/不希望所有模型都支持这一点,所以我们只是将 trait 用于业务关键模型......
  • 是的——你需要parent::boot()——我更喜欢一个特质的另一个原因——你避免冒险忘记添加它;-)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-01
  • 2016-09-13
  • 1970-01-01
  • 2020-06-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多