在 Laravel 中同步一对多关系
Posted
技术标签:
【中文标题】在 Laravel 中同步一对多关系【英文标题】:Synchronizing a one-to-many relationship in Laravel 【发布时间】:2015-01-28 01:08:01 【问题描述】:如果我有一个多对多关系,则使用其sync
方法更新关系非常容易。
但是我将使用什么来同步一对多关系?
表posts
:id, name
表links
:id, name, post_id
这里,每个Post
可以有多个Link
s。
我想根据输入的链接集合(例如,从我可以添加、删除和修改链接的 CRUD 表单)同步与数据库中特定帖子关联的链接。
应该删除我的输入集合中不存在的数据库中的链接。应该更新数据库和我的输入中存在的链接以反映输入,并且应该将仅存在于我的输入中的链接添加为数据库中的新记录。
总结期望的行为:
inputArray = true / db = false ---CREATE inputArray = false / db = true ---删除 inputArray = true / db = true ----更新【问题讨论】:
【参考方案1】:这是受 @alexw
启发的更新答案,适用于 laravel 7+ 也使用复合主键
在您的app/Providers/AppServiceProvider.php
的boot
方法中添加此宏
Illuminate\Database\Eloquent\Relations\HasMany::macro( 'sync', function ( $data, $deleting = true )
$changes = [
'created' => [], 'deleted' => [], 'updated' => [],
];
/**
* Cast the given keys to integers if they are numeric and string otherwise.
*
* @param array $keys
*
* @return array
*/
$castKeys = function ( array $keys )
return (array)array_map( function ( $v )
return is_numeric( $v ) ? (int)$v : (string)$v;
, $keys );
;
$relatedKeyName = $this->related->getKeyName();
$getCompositeKey = function ( $row ) use ( $relatedKeyName )
$keys = [];
foreach ( (array)$relatedKeyName as $k )
$keys[] = data_get( $row, $k );
return join( '|', $keys );
;
// First we need to attach any of the associated models that are not currently
// in the child entity table. We'll spin through the given IDs, checking to see
// if they exist in the array of current ones, and if not we will insert.
$current = $this->newQuery()->get( $relatedKeyName )->map( $getCompositeKey )->toArray();
// Separate the submitted data into "update" and "new"
$updateRows = [];
$newRows = [];
foreach ( $data as $row )
$key = $getCompositeKey( $row );
// We determine "updateable" rows as those whose $relatedKeyName (usually 'id') is set, not empty, and
// match a related row in the database.
if ( ! empty( $key ) && in_array( $key, $current ) )
$updateRows[$key] = $row;
else
$newRows[] = $row;
// Next, we'll determine the rows in the database that aren't in the "update" list.
// These rows will be scheduled for deletion. Again, we determine based on the relatedKeyName (typically 'id').
$updateIds = array_keys( $updateRows );
if ( $deleting )
$deleteIds = [];
foreach ( $current as $currentId )
if ( ! in_array( $currentId, $updateIds ) )
$deleteIds[$currentId] = array_combine( (array)$relatedKeyName, explode( '|', $currentId ) );
// Delete any non-matching rows
if ( count( $deleteIds ) > 0 )
/**
* @var \Illuminate\Database\Query\Builder $q
*/
$q = $this->newQuery();
$q->where(function ($q) use ( $relatedKeyName, $deleteIds)
foreach ( $deleteIds as $row )
$q->where( function ( $q ) use ( $relatedKeyName, $row )
foreach ( (array)$relatedKeyName as $key )
$q->where( $key, $row[$key] );
, null, null, 'or' );
);
$q->delete();
$changes['deleted'] = $castKeys( array_keys( $deleteIds ) );
// Update the updatable rows
foreach ( $updateRows as $id => $row )
$q = $this->getRelated();
foreach ( (array)$relatedKeyName as $key )
$q->where( $key, $row[$key] );
$q->update( $row );
$changes['updated'] = $castKeys( $updateIds );
// Insert the new rows
$newIds = [];
foreach ( $newRows as $row )
$newModel = $this->create( $row );
$newIds[] = $getCompositeKey( $newModel );
$changes['created'] = $castKeys( $newIds );
return $changes;
);
复合主键模型示例
class PermissionAdmin extends Model
public $guarded = [];
public $primaryKey = ['user_id', 'permission_id', 'user_type'];
public $incrementing = false;
public $timestamps = false;
然后你就可以使用 sync 方法,就像你通常使用它与 belongsToMany 关系一样
$user->roles()->sync([
[
'role_id' => 1
'user_id' => 12
'user_type' => 'admin'
],
[
'role_id' => 2
'user_id' => 12
'user_type' => 'admin'
]
]);
【讨论】:
【参考方案2】:您可以使用UPSERT 插入或更新重复键,也可以使用关系。
这意味着您可以将旧数据与新数据进行比较,并使用包含要更新的数据的数组以及要插入到同一查询中的数据。
您也可以删除其他不需要的 id。
这里是一个例子:
$toSave = [
[
'id'=>57,
'link'=>'...',
'input'=>'...',
],[
'id'=>58,
'link'=>'...',
'input'=>'...',
],[
'id'=>null,
'link'=>'...',
'input'=>'...',
],
];
// Id of models you wish to keep
// Keep existing that dont need update
// And existing that will be updated
// The query will remove the rest from the related Post
$toKeep = [56,57,58];
// We skip id 56 cause its equal to existing
// We will insert or update the rest
// Elements in $toSave without Id will be created into the relationship
$this->$relation()->whereNotIn('id',$toKeep)->delete();
$this->$relation()->upsert(
$toSave, // Data to be created or updated
['id'], // Unique Id Column Key
['link','input'] // Columns to be updated in case of duplicate key, insert otherwise
);
这将创建下一个查询:
delete from
`links`
where
`links`.`post_id` = 247
and `links`.`post_id` is not null
and `id` not in (56, 57, 58)
还有:
insert into
`links` (`id`, `link`, `input`)
values
(57, '...', '...'),
(58, '...', '...'),
(null, '...', '...')
on duplicate key update
`link` = values(`link`),
`input` = values(`input`)
这就是您可以在 2 个查询中更新关系的所有元素的方法。例如,如果您有 1,000 个帖子,并且您想要更新所有帖子的所有链接。
【讨论】:
【参考方案3】:另一个手动同步过程:
添加模型
class Post extends Model
protected $fillable = ["name"];
function links()
return $this->hasMany("App\Link");
class Link extends Model
protected $fillable = ["name", "post_id"];
function post()
return $this->belongsTo("App\Post");
class PostLink extends Model
protected $fillable = ["post_id", "link_id"];
function post()
return $this->belongsTo("App\Post");
function link()
return $this->belongsTo("App\Link");
我们来了
// list ids from request
$linkIds = $request->input("link");
if (!empty($linkIds))
// delete removed id from list in database
PostLink::where('post_id','=', $post->id)->whereNotIn('post_id', $linkIds)->delete();
// list remain id in database
$postLinkIds = $post->links()->pluck('post_id')->toArray();
// remove ids that already on db
$linkIds = array_diff($linkIds, $postLinkIds);
// check if still have id that must be save
if (!empty($linkIds))
foreach ($linkIds as $id)
// save id to post
$post->links()->create(['post_id' => $id]);
【讨论】:
【参考方案4】:删除和读取相关实体的问题在于,它会破坏您可能对这些子实体拥有的任何外键约束。
更好的解决方案是修改 Laravel 的 HasMany
关系以包含 sync
方法:
<?php
namespace App\Model\Relations;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @link https://github.com/laravel/framework/blob/5.4/src/Illuminate/Database/Eloquent/Relations/HasMany.php
*/
class HasManySyncable extends HasMany
public function sync($data, $deleting = true)
$changes = [
'created' => [], 'deleted' => [], 'updated' => [],
];
$relatedKeyName = $this->related->getKeyName();
// First we need to attach any of the associated models that are not currently
// in the child entity table. We'll spin through the given IDs, checking to see
// if they exist in the array of current ones, and if not we will insert.
$current = $this->newQuery()->pluck(
$relatedKeyName
)->all();
// Separate the submitted data into "update" and "new"
$updateRows = [];
$newRows = [];
foreach ($data as $row)
// We determine "updateable" rows as those whose $relatedKeyName (usually 'id') is set, not empty, and
// match a related row in the database.
if (isset($row[$relatedKeyName]) && !empty($row[$relatedKeyName]) && in_array($row[$relatedKeyName], $current))
$id = $row[$relatedKeyName];
$updateRows[$id] = $row;
else
$newRows[] = $row;
// Next, we'll determine the rows in the database that aren't in the "update" list.
// These rows will be scheduled for deletion. Again, we determine based on the relatedKeyName (typically 'id').
$updateIds = array_keys($updateRows);
$deleteIds = [];
foreach ($current as $currentId)
if (!in_array($currentId, $updateIds))
$deleteIds[] = $currentId;
// Delete any non-matching rows
if ($deleting && count($deleteIds) > 0)
$this->getRelated()->destroy($deleteIds);
$changes['deleted'] = $this->castKeys($deleteIds);
// Update the updatable rows
foreach ($updateRows as $id => $row)
$this->getRelated()->where($relatedKeyName, $id)
->update($row);
$changes['updated'] = $this->castKeys($updateIds);
// Insert the new rows
$newIds = [];
foreach ($newRows as $row)
$newModel = $this->create($row);
$newIds[] = $newModel->$relatedKeyName;
$changes['created'] = $this->castKeys($newIds);
return $changes;
/**
* Cast the given keys to integers if they are numeric and string otherwise.
*
* @param array $keys
* @return array
*/
protected function castKeys(array $keys)
return (array) array_map(function ($v)
return $this->castKey($v);
, $keys);
/**
* Cast the given key to an integer if it is numeric.
*
* @param mixed $key
* @return mixed
*/
protected function castKey($key)
return is_numeric($key) ? (int) $key : (string) $key;
您可以覆盖 Eloquent 的 Model
类以使用 HasManySyncable
而不是标准的 HasMany
关系:
<?php
namespace App\Model;
use App\Model\Relations\HasManySyncable;
use Illuminate\Database\Eloquent\Model;
abstract class MyBaseModel extends Model
/**
* Overrides the default Eloquent hasMany relationship to return a HasManySyncable.
*
* @inheritDoc
* @return \App\Model\Relations\HasManySyncable
*/
public function hasMany($related, $foreignKey = null, $localKey = null)
$instance = $this->newRelatedInstance($related);
$foreignKey = $foreignKey ?: $this->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
return new HasManySyncable(
$instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey
);
假设您的 Post
模型扩展了 MyBaseModel
并具有 links()
hasMany
关系,您可以执行以下操作:
$post->links()->sync([
[
'id' => 21,
'name' => "LinkedIn profile"
],
[
'id' => null,
'label' => "Personal website"
]
]);
将更新此多维数组中具有与子实体表 (links
) 匹配的 id
的任何记录。表中不存在于该数组中的记录将被删除。数组中不存在于表中的记录(具有不匹配的id
,或为空的id
)将被视为“新”记录,并将被插入到数据库中。
【讨论】:
会不会影响laravel在后续操作中默认的has-many关系? 这看起来不错!但由于这是一个旧答案,我想知道它在 Laravel 的新版本中是否仍然可行。测试和工作。我将在我的项目中实现这一点。 @Ashish 不,它不会影响 laravel 的默认 has-many 关系操作,因为您只是向 laravel 的 HasMany 类添加一个名为 sync 的新函数,并且 不改变默认laravel的代码/行为。 @Pratik149 我知道。正如我两年前提出的这个问题。无论如何,谢谢。 @Ashish 哈哈酷,我没想到你会回复。实际上,对于将来会参考此答案并且与您有同样疑问的人,我实际上放弃了该评论,因此至少他们不会无人回答。【参考方案5】:我喜欢这样做,它针对最少的查询和最少的更新进行了优化:
首先,将要同步的链接 ID 放入数组中:$linkIds
,并将帖子模型放入其自己的变量中:$post
Link::where('post_id','=',$post->id)->whereNotIn('id',$linkIds)//only remove unmatching
->update(['post_id'=>null]);
if($linkIds)//If links are empty the second query is useless
Link::whereRaw('(post_id is null OR post_id<>'.$post->id.')')//Don't update already matching, I am using Raw to avoid a nested or, you can use nested OR
->whereIn('id',$linkIds)->update(['post_id'=>$post->id]);
【讨论】:
请记住,像这样的批量方法不会更新时间戳或触发模型事件。【参考方案6】:不幸的是,一对多关系没有sync
方法。自己做很简单。至少如果您没有任何引用 links
的外键。因为这样您就可以简单地删除行并再次将它们全部插入。
$links = array(
new Link(),
new Link()
);
$post->links()->delete();
$post->links()->saveMany($links);
如果您真的需要更新现有的(无论出于何种原因),您需要完全按照您在问题中描述的方式进行操作。
【讨论】:
不要这样做,因为将来您可能需要存储数据透视数据......或更糟 - 另一个编码器将存储数据透视数据而不知道同步是假的。 抱歉,我的头在别处了。并不意味着“枢轴”数据。不过,重点仍然存在。 在 AUTOINCREMENT 的情况下,它不会更快地耗尽主键容量吗? 如果您只有两个相关模型,则很有用。不幸的是,就我而言,我有 3 个或更多模型取决于记录的 ID - 所以我不能删除它并重新创建。以上是关于在 Laravel 中同步一对多关系的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Laravel 中从多对多关系的一对多关系中获取项目?