web-dev-qa-db-ja.com

Laravelで1対多の関係を同期する

多対多の関係がある場合、syncメソッドを使用して関係を更新するのは非常に簡単です。

しかし、1対多の関係を同期するために何を使用しますか?

  • テーブルpostsid, name
  • テーブルlinksid, name, post_id

ここで、各Postは複数のLinkを持つことができます。

入力されたリンクのコレクション(たとえば、リンクを追加、削除、変更できるCRUDフォームから)に対して、データベース内の特定の投稿に関連付けられているリンクを同期させたいのですが。

入力コレクションに存在しないデータベース内のリンクは削除する必要があります。データベースと入力に存在するリンクは、入力を反映するように更新する必要があります。入力にのみ存在するリンクは、データベースの新しいレコードとして追加する必要があります。

望ましい動作を要約するには:

  • inputArray = true/db = false ---作成
  • inputArray = false/db = true --- DELETE
  • inputArray = true/db = true ---- UPDATE
22
user2834172

残念ながら、1対多の関係にはsyncメソッドはありません。自分で行うのはとても簡単です。少なくとも、linksを参照する外部キーがない場合。それは、行を簡単に削除して、もう一度すべて挿入できるからです。

$links = array(
    new Link(),
    new Link()
);

$post->links()->delete();
$post->links()->saveMany($links);

(何らかの理由で)既存のものを本当に更新する必要がある場合は、質問で説明したとおりに実行する必要があります。

19
lukasgeiter

関連するエンティティを削除して再読み込みする場合の問題は、それらの子エンティティにある可能性のあるすべての外部キー制約が破られることです。

より良い解決策は、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"
    ]
]);
_

子エンティティテーブル(id)と一致するlinksを持つこの多次元配列内のすべてのレコードが更新されます。この配列に存在しないテーブル内のレコードは削除されます。テーブルに存在しない配列内のレコード(一致しないid、またはidがnullである)は、「新しい」レコードと見なされ、データベースに挿入されます。

8
alexw

私はこれを好きでした最小限のクエリと最小限の更新のために最適化されています

最初に、同期するリンク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]);
}
1
Luca C.