Laravel Eloquent: как построить сложный WHERE с комбинацией OR и AND?
Проблема приоритета операторов и группировка условий
Вы верно заметили главную проблему: в 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.