php 在 Laravel 中同步一对多关系
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/27204485/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Synchronizing a one-to-many relationship in Laravel
提问by user2834172
If I have a many-to-many relationship it's super easy to update the relationship with its sync
method.
如果我有一个多对多的关系,用它的sync
方法更新关系是非常容易的。
But what would I use to synchronize a one-to-many relationship?
但是我会用什么来同步一对多的关系呢?
- table
posts
:id, name
- table
links
:id, name, post_id
- 表
posts
:id, name
- 表
links
:id, name, post_id
Here, each Post
can have multiple Link
s.
在这里,每个都Post
可以有多个Link
s。
I'd like to synchronize the links associated with a specific post in the database, against an inputted collection of links (for example, from a CRUD form where I can add, remove, and modify links).
我想将与数据库中特定帖子关联的链接与输入的链接集合(例如,来自我可以添加、删除和修改链接的 CRUD 表单)同步。
Links in the database that aren't present in my input collection should be removed. Links that exist in the database and in my input should be updated to reflect the input, and links that are only present in my input should be added as new records in the database.
数据库中不存在于我的输入集合中的链接应该被删除。数据库和我的输入中存在的链接应该更新以反映输入,并且只存在于我的输入中的链接应该作为数据库中的新记录添加。
To summarize the desired behavior:
总结所需的行为:
- inputArray = true / db = false ---CREATE
- inputArray = false / db = true ---DELETE
- inputArray = true / db = true ----UPDATE
- inputArray = true / db = false ---CREATE
- inputArray = false / db = true ---DELETE
- inputArray = true / db = true ----更新
回答by lukasgeiter
Unfortunately there is no sync
method for one-to-many relations. It's pretty simple to do it by yourself. At least if you don't have any foreign key referencing links
. Because then you can simple delete the rows and insert them all again.
不幸的是,没有sync
一对多关系的方法。自己做很简单。至少如果您没有任何外键引用links
。因为那样你就可以简单地删除行并再次插入它们。
$links = array(
new Link(),
new Link()
);
$post->links()->delete();
$post->links()->saveMany($links);
If you really need to update existing one (for whatever reason) you need to do exactly what you described in your question.
如果您确实需要更新现有的(无论出于何种原因),您需要完全按照您在问题中描述的内容进行操作。
回答by alexw
The problem with deleting and readding the related entities, is that it will break any foreign key constraints you might have on those child entities.
删除和读取相关实体的问题在于,它会破坏您对这些子实体可能具有的任何外键约束。
A better solution is to modify Laravel's HasMany
relationship to include a sync
method:
更好的解决方案是修改 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;
}
}
You can override Eloquent's Model
class to use HasManySyncable
instead of the standard HasMany
relationship:
你可以覆盖 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
);
}
Supposing that your Post
model extends MyBaseModel
and has a links()
hasMany
relationship, you can do something like:
假设您的Post
模型扩展MyBaseModel
并具有links()
hasMany
关系,您可以执行以下操作:
$post->links()->sync([
[
'id' => 21,
'name' => "LinkedIn profile"
],
[
'id' => null,
'label' => "Personal website"
]
]);
Any records in this multidimensional array that have an id
that matches the child entity table (links
) will be updated. Records in the table that are not present in this array will be removed. Records in the array that are not present in the table (Have a non-matching id
, or an id
of null) will be considered "new" records and will be inserted into the database.
此多维数组中具有id
与子实体表 ( links
)匹配的任何记录都将被更新。表中不存在于该数组中的记录将被删除。数组中不存在于表中的记录(具有不匹配id
或id
空值)将被视为“新”记录并将被插入到数据库中。
回答by Luca C.
I did like this, and it is optimized for minimal query and minimal updates:
我确实喜欢这个,它针对最少的查询和最少的更新进行了优化:
first, put link ids to sync in an array: $linkIds
and the post model in its own variable: $post
首先,将要同步的链接 ID 放入一个数组中:$linkIds
并将 post 模型放入其自己的变量中:$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]);
}