Laravel 8: One To Many Polymorphic: связать одного и того же ребенка с разными родителями

Я использовал полиморфные отношения «один ко многим», как описано в laravel документацию, чтобы можно было связать разные родительские модели с одной дочерней моделью. Я предположил, что смогу назначить разные родительские модели одной и той же дочерней модели. но это не работает. как только я создаю новое отношение с другой родительской моделью к тому же ребенку, старое отношение заменяется.

Пример:

A, B и C являются родительскими моделями, каждая из которых имеет одну запись данных (id=1).

X — дочерняя модель с одной записью данных (id=1)

Я не могу сделать что-то подобное с помощью общих методов:

A(id=1) <-> X(id=1)

B(id=1) <-> X(id=1)

C(id=1) <-> X(id=1)

Поскольку последнее создание отношения всегда перезаписывает предыдущее. В этом примере останется одно отношение (C(id=1) ‹-> X(id=1))

Я могу сделать это с помощью полиморфной реализации «многие ко многим», но на самом деле это не то, чего я хочу, поскольку я не хочу, чтобы родительские модели могли иметь более одного отношения к дочерней модели. (хотя я мог бы исключить это, создав составной ключ в таблице *able для соответствующих полей)

Это фактический код, который должен назначать одно изображение нескольким родительским моделям (но остается только последнее сохранение внутри цикла - если я добавляю разрыв в конце цикла, первое сохраняется):

public function store(StoreImageRequest $request)
{
    $validated = $request->validated();

    $image = $validated['image'];
    $name = isset($validated['clientName']) ? $image->getClientOriginalName() : $validated['name'];
    $fileFormat = FileFormat::where('mimetype','=',$image->getClientMimeType())->first();

    $path = $image->store('images');

    $imageModel = Image::make(['name' => $name, 'path' => $path])->fileFormat()->associate($fileFormat);
    $imageModel->save();

    $relatedModels = Image::getRelatedModels();

    foreach($relatedModels as $fqcn => $cn) {
        if(isset($validated['model'.$cn])) {
            $id = $validated['model'.$cn];
            $models[$fqcn] = call_user_func([$fqcn, 'find'], [$id])->first();
            $models[$fqcn]->images()->save($imageModel);
        }
    }
}

person lsblsb    schedule 30.06.2021    source источник
comment
возможно, имеет смысл также добавить реализацию getRelatedModels, чтобы действительно полностью понять ваш код. На первый взгляд я бы сказал, что проблема в строке $models[$fqcn]->images()->save($imageModel);. Я предполагаю, что imageModel обновляется после сохранения, и его многократный вызов также обновит ссылки. Возможно, вы также добавите в код определения своих отношений. Вы можете быстро попробовать $models[$fqcn]->images()->sync([$imageModel->id]);   -  person Frnak    schedule 02.07.2021
comment
@Frnak Я не думаю, что это должно быть проблемой, так как я хочу сохранить разные ссылки. Поэтому модель imageModel должна обрабатывать это правильно. Но я попробую. Вы уверены в предполагаемом поведении этого типа отношений - должен ли он иметь возможность связать конкретного ребенка с несколькими родителями, как я предполагаю?   -  person lsblsb    schedule 06.07.2021


Ответы (1)


Что ж, теперь немного более понятно, что вы пытаетесь сделать. Дело в том, что даже если вы хотите обеспечить, чтобы максимум один из каждого родителя был прикреплен к дочернему элементу, вы все равно пытаетесь создать отношения ManyToMany PolyMorphic. У родителей может быть несколько изображений, а у изображений может быть несколько родителей (по одному от каждого).

Не зная структуры данных, это могло бы быть жизнеспособным, если бы все родители имели схожие структуры, чтобы объединить их в одну таблицу, и посмотреть на отношение HasOne ofMany, чтобы обеспечить, чтобы изображение имело только одно от каждого родителя.

Если вы настаиваете на полиморфных отношениях, я бы сделал следующее

    // child
    Schema::create('images', function (Blueprint $table) {
        $table->id();
        $table->timestamps();
    });

    // parents
    Schema::create('parent_ones', function (Blueprint $table) {
        $table->id();
        $table->timestamps();
    });


    Schema::create('parent_twos', function (Blueprint $table) {
        $table->id();
        $table->timestamps();
    });

    Schema::create('parent_trees', function (Blueprint $table) {
        $table->id();
        $table->timestamps();
    });

    // morph table
    Schema::create('parentables', function (Blueprint $table) {
        $table->id();
        $table->unsignedBigInteger('image_id');
        $table->foreign('image_id')->references('id')->on('images');
        $table->string('parentable_type');
        $table->unsignedBigInteger('parentable_id');
        $table->timestamps();
        $table->unique(['image_id', 'parentable_type', 'parentable_id'], 'uniq_parents');
    });

Ограничение уникальности для таблицы требует, чтобы изображение могло иметь только по одному элементу каждого родительского типа.

Отношения модели

   // Child

   public function parent_one(): MorphToMany
    {
        return $this->morphedByMany(ParentOne::class, 'parentable');
    }

    public function parent_two(): MorphToMany
    {
        return $this->morphedByMany(ParentTwo::class, 'parentable');
    }

    public function parent_tree(): MorphToMany
    {
        return $this->morphedByMany(ParentTree::class, 'parentable');
    }


    // parents
    public function images(): MorphToMany
    {
        return $this->morphToMany(Image::class, 'parentable')->withTimestamps();
    }

Затем ваш код должен обрабатывать, чтобы он не пытался прикрепить родителя к изображению, если у него уже есть родитель этого типа.

Это можно сделать либо в вашем контроллере, либо, если родитель не заменим, вы можете добавить его в качестве проверки в своем запросе.

Решением может быть установка этого пакета (я не проверял). MorphToOne


Старый ответ

Мне кажется, что вы перевернули отношение «один ко многим» в неправильном направлении.

Ваша дочерняя модель должна реализовывать morphTo, а ваши родительские модели должны реализовывать morphMany

a 
    id - integer
    title - string
    body - text

b
    id - integer
    title - string
    url - string

c
    id - integer
    title - string
    url - string

x
    id - integer
    body - text
    commentable_id - integer
    commentable_type - string

Детская модель

public class X extends Model
{
    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}

Родительские модели

public class A extends Model
{
    public function comments(): MorphMany
    {
        return $this->morphMany(X::class, 'commentable');
    }
}

Добавление к отношению:

$a = A::firstOrFail();
$x = X::firstOrFail();

// Attaches
$a->comments()->attach($x);

// Sync, removes the existing attached comments
$a->comments()->sync([$x]);

// Sync, but do not remove  existing
$a->comments()->sync([$x], false);
$a->comments()->syncWithoutDetaching([$x]);

person Steffen Thomsen    schedule 30.06.2021
comment
Привет Штеффен, нет, это не так. Я строю, как ты. Но я добавляю отношение не с помощью attach() или sync() (см. мой обновленный вопрос с фактическим кодом). Может ведет себя как синхронизация с удалением - попробую. - person lsblsb; 30.06.2021
comment
@lsblsb - см. мой обновленный ответ - person Steffen Thomsen; 15.07.2021
comment
Привет Стеффен, спасибо. Но моя проблема в другом. Я хочу назначить разные родительские идентификаторы одному конкретному дочернему идентификатору - и именно это не работает в моем случае с полиморфной связью "один ко многим". - person lsblsb; 17.07.2021
comment
Основываясь на информации, которую вы предоставили, я бы сказал, что мой ответ правильный. Родитель может иметь несколько детей, а ребенок может иметь только одного от каждого родителя. Это отношение «многие ко многим», ограниченное одним переходом от дочернего элемента к каждому родителю. Если это не так, вам нужно расширить первоначальный вопрос со структурой вашей модели. - person Steffen Thomsen; 17.07.2021