Laravel 更新模型的一对多关系项

Laravel update model's one-to-many relation's items

我有两个 Eloquent 模型:

class User extends Model
{
    public function items()
    {
        return $this->hasMany(Item::class, "userId");
    }
}
class Item extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class, "userId");
    }
}

Item 的列是 iduserIdname

当我想更新用户(PUT /users/<id>)时我也想更新同一请求中的项目,意思是:

  1. 跳过现有项目
  2. 添加新项目
  3. 删除多余的项目

之后数据库和关系都应该有最新的项目。

请求数据示例:

{
    "userAttribute1": "Something",
    "userAttribute2": "Something else",
    "items": [
        {
            "name": "Name1"
        },
        {
            "name": "Name2"
        }
    ]
}

我试图寻找一种使用 Laravel 执行此操作的简单方法,例如对于多对多关系,您可以调用 attach/detachsync 使用 id 自动更新它们。

我为我的一对多关系做了这个可憎的事:

// Controller method for PUT /users/<id>
public function update(User $user, Request $request)
{
    // Update user attributes normal way
    $user->fill($request->validated());
    $user->save();
    
    // Array, e.g. [ { "name": "Name1" }, { "name": "Name2" } ]
    $newItems = $request->validated()["items"];
    
    // Collection of items
    $currentItems = $user->items;
    
    // Array to keep track of which items to delete later
    $removeItemIds = [];
    
    foreach ($currentItems as $i => $currentItem)
    {
        // currentItem is Item model

        $exists = false;
        
        foreach ($newItems as $j => $newItem)
        {
            // newItem is array, e.g. { "name": "Name1" }

            if ($currentItem->name === $newItem["name"])
            {
                $exists = true;
                break;
            }
        }
        
        if ($exists)
        {
            // New item already exists, remove from newItems array
            unset($newItems[$j]);
        }
        else
        {
            // Current item does't exist anymore, remove from currentItems collection and mark as deletable
            $removeItemIds[] = $currentItem->id;
            unset($currentItems[$i]);
        }
    }
    
    // Add remaining new items
    foreach ($newItems as $newItem)
    {
        $item = Item::make($newItem);
        $item->userId = $user->id;
        $item->save();
        
        $currentItems->push($item);
    }
    
    // Delete extra items
    $user->items()->whereIn("id", $removeItemIds)->delete();
    
    // Update relation so the returned data is up-to-date as well
    $user->setRelation("items", $currentItems);
    
    return [
        "user" => new UserResource($user),
    ];
}

这个用户 + 项目模型只是一个例子 - 我有多个相似的关系(其中不仅仅是 name 列)并且将这段代码复制粘贴到各处并稍微修改它似乎有点愚蠢。

Laravel 以所有这些花哨的快捷方式和易于 use/magic 的方法而闻名,所以我的问题是:是否有更简单、更短的方法来进行此更新?

您可以使用 Laravel collections.

首先,更新用户:

$user->update($request->validated());

然后,您可以同步用户项目:

$new = ['item 1', 'item 2']; // Your request items

// Get the current items your user have:
$items = $user->items->pluck('name')->toArray();

// Now, you need to delete the items your user have but are not present in the request items array:
$deleteItems = $user->items->pluck('name', 'id')
  ->reject(function($value, $id) use ($new) {
      return in_array($value, $new);
  })
  ->keys();

Item::whereIn('id', $deleteItems)->delete();

// Last but not least, you need to create the new items and attach it to the user:
collect($new)->each(function($insertData) use ($user) {
  $user->items()->firstOrcreate([
      'name' => $insertData
  ]);
});

对于具有多个字段的模型,您需要将两个数组传递给firstOrCreate方法。第一个数组将用于查找模型,如果没有找到,它将通过合并两个数组来创建:

$user->items()->firstOrcreate([
   'name' => $insertData
], [
    'description' => $description,
    'quantity' => $quantity
]);

由于您使用的是 firstOrCreate,它只会在未通过名称找到项目时创建该项目,并且您不会有重复的项目。