Сложное eloquent relation в laravel

Всем привет. Возник вопрос по eloquent relation в проекте на laravel 10. Допустим есть 3 таблицы:

  1. orders - таблица с данными заказов
    • id - id заказа
    • shop_id - id магазина, к которому относится заказ
  2. orders_products - таблица с данными товаров в заказах
    • order_id - id заказа
    • product_id - id товара
  3. remainings - таблица с остатками товаров на дату в разрезе магазинов
    • date - дата
    • shop_id - id магазина
    • product_id - id товара
    • remaining - остаток

Вопрос: какое отношение нужно написать в классе OrdersProduct, чтобы получить остаток по product_id, shop_id (через таблицу orders) и на самую позднюю дату? Пробовал через hasOneTrough:

public function remainings(): HasOneThrough
{
    return $this->hasOneThrough(Remaining::class, Order::class, 'id', 'shop_id', 'order_id', 'shop_id')->whereProductId($this->product_id)->latest('date');
}

но тогда на каждый вызов этого метода происходит отдельный подзапрос к БД, хотелось бы по "правильному" загрузить все данные через отношение. Или laravel пока такое не умеет?


Ответы (1 шт):

Автор решения: mydls1

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

Чтобы получить остаток по product_id и shop_id (через таблицу orders) на самую позднюю дату, вам нужно определить отношение в модели OrdersProduct. Однако из-за того, что вам нужно получить shop_id из связанной модели Order, стандартные отношения Eloquent не позволяют напрямую использовать свойства других моделей при определении отношений.

Тем не менее, вы можете использовать метод hasOne с замыканием для добавления дополнительных условий в отношение. Вот как вы можете это сделать:

class OrdersProduct extends Model
{
    // Определяем отношение к модели Order
    public function order()
    {
        return $this->belongsTo(Order::class, 'order_id');
    }

    // Определяем отношение к модели Remaining с дополнительными условиями
    public function remaining()
    {
        return $this->hasOne(Remaining::class, 'product_id', 'product_id')
            ->where('shop_id', $this->order->shop_id)
            ->orderBy('date', 'desc');
    }
}

Однако при таком определении возникнет проблема: вы не можете использовать $this->order->shop_id внутри метода remaining(), потому что при определении отношения экземпляр модели еще не загружен.

Чтобы обойти это ограничение, вы можете использовать замыкание в методе hasOne, которое принимает объект запроса и модель, и внутри него добавить необходимые условия. Вот скорректированный вариант:

public function remaining()
{
    return $this->hasOne(Remaining::class, 'product_id', 'product_id')
        ->join('orders', 'orders.shop_id', '=', 'remainings.shop_id')
        ->whereColumn('orders.id', 'orders_products.order_id')
        ->orderBy('remainings.date', 'desc')
        ->select('remainings.*');
}

В этом случае мы используем join для соединения таблиц и whereColumn для сравнения столбцов.

Однако такой подход может быть сложным и не совсем соответствовать идеологии Eloquent.

Рекомендуемый подход

Лучше определить метод, который будет выполнять нужный запрос и возвращать модель Remaining. Это позволит использовать необходимые свойства и условия без ограничений отношений. Вот как это можно сделать:

public function getRemainingAttribute()
{
    return Remaining::where('product_id', $this->product_id)
        ->where('shop_id', $this->order->shop_id)
        ->orderBy('date', 'desc')
        ->first();
}

Теперь вы можете обращаться к остатку через свойство remaining:

$ordersProduct = OrdersProduct::with('order')->find($id);
$remaining = $ordersProduct->remaining;

Пояснение

  • Определение аксессора getRemainingAttribute(): Это позволяет динамически получать остаток с учетом условий.
  • Использование метода with('order'): При загрузке OrdersProduct важно подгрузить связанную модель Order, чтобы order был доступен внутри метода.
  • Преимущества данного подхода:
    • Гибкость: Вы можете легко настроить дополнительные условия.
    • Простота: Избегаете сложных определений отношений с замыканиями и join.
    • Производительность: Запрос выполняется только при обращении к свойству remaining.

Заключение

Из-за сложности отношения и необходимости использовать свойства связанной модели, лучше определить метод или аксессор в модели OrdersProduct, который будет возвращать нужный вам остаток. Это позволит вам точно контролировать условия запроса и обеспечить корректную работу вашего приложения.

→ Ссылка