Avatar ·

Laravel Eloquent: как построить сложный WHERE с комбинацией OR и AND?

Всем привет! Я новичок в Laravel и Eloquent, и у меня возникла проблема с построением сложного SQL-запроса с условиями WHERE, где используются одновременно OR и AND. У меня есть таблица `products` с полями: `status`, `category_id`, `price`, `is_featured`. Мне нужно выбрать товары, которые удовлетворяют одному из двух условий: 1. Товар активен (status = 'active') И имеет цену меньше 1000 (price < 1000); 2. ИЛИ товар является избранным (is_featured = 1) И принадлежит категории с id = 5 (category_id = 5). То есть в SQL это выглядело бы так: ```sql SELECT * FROM products WHERE (status = 'active' AND price < 1000) OR (is_featured = 1 AND category_id = 5); ``` Я пробовал использовать `where()` и `orWhere()` в Eloquent, но получается какая-то каша. Например, так: ```php $products = Product::where('status', 'active') ->where('price', '<', 1000) ->orWhere('is_featured', 1) ->where('category_id', 5) ->get(); ``` Этот код выдаёт не то, что я ожидаю. Он, кажется, делает что-то вроде: ```sql WHERE status = 'active' AND price < 1000 OR is_featured = 1 AND category_id = 5 ``` Но из-за приоритета операторов это не то же самое, что моё условие с правильной группировкой скобок. Помогите разобраться, как правильно сгруппировать условия в Eloquent? Я слышал про `where(function($query) {...})`, но не до конца понимаю, как это работает в данном случае. Заранее спасибо! <<>>

Проблема приоритета операторов и группировка условий

Вы верно заметили главную проблему: в SQL операторы AND имеют более высокий приоритет, чем OR. Поэтому ваш исходный запрос:

WHERE status = 'active' AND price < 1000 OR is_featured = 1 AND category_id = 5

Фактически выполняется как:

WHERE (status = 'active' AND price < 1000) OR (is_featured = 1 AND category_id = 5)

...но только потому, что AND группируется сам. Однако если бы у вас было три условия, логика сломалась бы. Главное — в Eloquent нужно явно указывать группировку с помощью замыканий (closure).

Правильное решение: группировка через замыкания

Eloquent предоставляет метод where(function($query) { ... }) для создания вложенных групп условий. Внутри замыкания вы можете использовать любые методы where, orWhere и т.д., и они будут обёрнуты в скобки.

Вот как должен выглядеть ваш запрос:

use App\Models\Product;

$products = Product::where(function ($query) {
    $query->where('status', 'active')
          ->where('price', '<', 1000);
})->orWhere(function ($query) {
    $query->where('is_featured', 1)
          ->where('category_id', 5);
})->get();

Этот код сгенерирует именно тот SQL, который вам нужен:

SELECT * FROM products 
WHERE (status = 'active' AND price < 1000) 
   OR (is_featured = 1 AND category_id = 5)

Как это работает

  • Первый where(function($query) {...}) создаёт группу (status = 'active' AND price < 1000). Все where() внутри этого замыкания соединяются через AND.
  • Затем orWhere(function($query) {...}) добавляет вторую группу с OR. Внутри неё условия также соединяются через AND.
  • Eloquent автоматически расставляет скобки вокруг каждой группы.

Дополнительные примеры

Вы можете комбинировать сколько угодно уровней вложенности. Например, если нужно добавить ещё одно условие с AND ко всему запросу:

$products = Product::where(function ($query) {
    $query->where('status', 'active')
          ->where('price', '<', 1000);
})->orWhere(function ($query) {
    $query->where('is_featured', 1)
          ->where('category_id', 5);
})->where('created_at', '>=', now()->subDays(30)) // это AND ко всему
  ->get();

Результат:

SELECT * FROM products 
WHERE (status = 'active' AND price < 1000) 
   OR (is_featured = 1 AND category_id = 5)
   AND created_at >= '2024-01-01'

Распространённые ошибки

  • Забыть обернуть условия в замыкание — тогда orWhere будет применяться ко всему предыдущему условию, а не к группе.
  • Использовать orWhere без группировки — как в вашем первом примере, это приводит к неправильной логике.
  • Путать where и orWhere внутри группы — внутри группы все where по умолчанию соединяются через AND, если не указано иное.

Этот подход — стандартный для Laravel и используется в большинстве сложных запросов. Освоив его, вы сможете строить запросы любой сложности.

<<>>

Альтернативный подход: использование whereRaw

Если вам нужно быстрое решение без глубокого погружения в замыкания, можно использовать метод whereRaw(). Он позволяет вставить сырой SQL-фрагмент прямо в запрос Eloquent:

$products = Product::whereRaw(
    '(status = ? AND price < ?) OR (is_featured = ? AND category_id = ?)',
    ['active', 1000, 1, 5]
)->get();

Плюсы этого подхода:

  • Простота и наглядность — вы видите точный SQL.
  • Полный контроль над группировкой скобок.
  • Защита от SQL-инъекций через параметры (знак вопроса).

Минусы:

  • Меньшая читаемость при большом количестве условий.
  • Потеря преимуществ Eloquent (например, автоматическое преобразование имён полей).
  • Сложнее поддерживать — при изменении схемы БД придётся править сырой SQL.

Используйте whereRaw для простых случаев или когда нужно быстро прототипировать. Для продакшена и сложных запросов лучше применять группировку через замыкания, как описано в первом ответе — это более идиоматичный и безопасный способ работы с Laravel.