mirror of
https://github.com/flarum/framework.git
synced 2024-11-24 15:22:27 +08:00
feat: calculate post numbers via subquery
On heavily volatile communities running on distributed systems, if two replies are posted at approximately the same time, each node will try to use the same post number, which results in a failed integrity constraint. Potential options for fixing this were discussed in https://github.com/flarum/framework/issues/3350. We settled on calculating the post number via subquery during post instance creation, which should solve this issue since DB transactions are atomic. To facilicate this, we needed to implement support for DB attributes that are calculated via subquery at insert, but otherwise stored and accessed normally. This PR adds a `InsertViaSubqueryTrait` which adds exactly this feature.
This commit is contained in:
parent
6df4101bae
commit
b17f26e6a8
73
framework/core/src/Database/InsertsViaSubqueryTrait.php
Normal file
73
framework/core/src/Database/InsertsViaSubqueryTrait.php
Normal file
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Database;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\MySqlConnection;
|
||||
|
||||
trait InsertsViaSubqueryTrait
|
||||
{
|
||||
/**
|
||||
* A list that maps attribute names to callables that produce subqueries
|
||||
* used to calculate the inserted value.
|
||||
*
|
||||
* Each callable should take an instance of the model being saved,
|
||||
* and return an Eloquent query builder that queries for the subquery
|
||||
* generated value. The result of the query should be one row of one column.
|
||||
*
|
||||
* Subquery attributes should be added in the static `boot` method of models
|
||||
* using this trait.
|
||||
*
|
||||
* @var array<string, callable(AbstractModel): Builder>
|
||||
*/
|
||||
protected static $subqueryAttributes = [];
|
||||
|
||||
/**
|
||||
* Overriden so that some fields can be inserted via subquery.
|
||||
* The goal here is to construct a subquery that returns primitives
|
||||
* for the provided `attributes`, and uses additional subqueries for
|
||||
* statically-specified subqueryAttributes.
|
||||
*/
|
||||
protected function insertAndSetId(Builder $query, $attributes)
|
||||
{
|
||||
$subqueryAttrNames = array_keys(static::$subqueryAttributes);
|
||||
|
||||
$literalAttributes = array_diff_key($attributes, array_flip($subqueryAttrNames));
|
||||
|
||||
/** @var Builder */
|
||||
$insertRowSubquery = static::query()->limit(1);
|
||||
|
||||
foreach ($literalAttributes as $attrName => $value) {
|
||||
$parameter = $query->getGrammar()->parameter($value);
|
||||
$insertRowSubquery->addBinding($value, 'select');
|
||||
$insertRowSubquery->selectRaw("$parameter as $attrName");
|
||||
}
|
||||
|
||||
foreach (static::$subqueryAttributes as $attrName => $callback) {
|
||||
$insertRowSubquery->selectSub($callback($this), $attrName);
|
||||
}
|
||||
|
||||
$attrNames = array_merge(array_keys($literalAttributes), $subqueryAttrNames);
|
||||
$query->insertUsing($attrNames, $insertRowSubquery);
|
||||
|
||||
// This should be accurate, as it's the same mechanism used by Laravel's `insertGetId`.
|
||||
// See https://github.com/laravel/framework/blob/master/src/Illuminate/Database/Query/Processors/Processor.php#L30-L37.
|
||||
/** @var MySqlConnection */
|
||||
$con = $query->getQuery()->getConnection();
|
||||
$idRaw = $con->getPdo()->lastInsertId($keyName = $this->getKeyName());
|
||||
$id = is_numeric($idRaw) ? (int) $idRaw : $idRaw;
|
||||
|
||||
$this->setAttribute($keyName, $id);
|
||||
|
||||
// This is necessary to get the computed value of saved attributes.
|
||||
$this->exists = true;
|
||||
$this->refresh();
|
||||
}
|
||||
}
|
|
@ -74,7 +74,7 @@ class PostReplyHandler
|
|||
|
||||
// If this is the first post in the discussion, it's technically not a
|
||||
// "reply", so we won't check for that permission.
|
||||
if ($discussion->post_number_index > 0) {
|
||||
if ($discussion->first_post_id !== null) {
|
||||
$actor->assertCan('reply', $discussion);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
namespace Flarum\Post;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Database\InsertsViaSubqueryTrait;
|
||||
use Flarum\Database\ScopeVisibilityTrait;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Foundation\EventGeneratorTrait;
|
||||
|
@ -41,6 +42,7 @@ class Post extends AbstractModel
|
|||
{
|
||||
use EventGeneratorTrait;
|
||||
use ScopeVisibilityTrait;
|
||||
use InsertsViaSubqueryTrait;
|
||||
|
||||
protected $table = 'posts';
|
||||
|
||||
|
@ -102,6 +104,10 @@ class Post extends AbstractModel
|
|||
});
|
||||
|
||||
static::addGlobalScope(new RegisteredTypesScope);
|
||||
|
||||
static::$subqueryAttributes['number'] = function (Post $post) {
|
||||
return static::query()->where('discussion_id', $post->discussion_id)->selectRaw('max(number)+1');
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue
Block a user