Есть ли способ автоматического добавления имени таблицы в методы запросов Eloquent?
Я разрабатываю приложение на Laravel 5.5 и сталкиваюсь с проблемой с конкретной областью запросов. У меня есть следующая структура таблицы (некоторые поля опущены):
orders
---------
id
parent_id
status
parent_id
ссылается на id
из той же таблицы. У меня есть эта область запроса для фильтрации записей, у которых нет детей:
public function scopeNoChildren(Builder $query): Builder
{
return $query->select('orders.*')
->leftJoin('orders AS children', function ($join) {
$join->on('orders.id', '=', 'children.parent_id')
->where('children.status', self::STATUS_COMPLETED);
})
->where('children.id', null);
}
Этот объем отлично работает при использовании в одиночку. Однако, если я попытаюсь объединить его с любым другим условием, он выдает исключение SQL:
Order::where('status', Order::STATUS_COMPLETED)
->noChildren()
->get();
Приводит к следующему:
SQLSTATE [23000]: Нарушение ограничения целостности: 1052 Столбец "статус" в том месте, где предложение неоднозначно
Я нашел два способа избежать этой ошибки:
Решение # 1: префикс всех других условий с именем таблицы
Выполнение чего-то подобного:
Order::where('orders.status', Order::STATUS_COMPLETED)
->noChildren()
->get();
Но я не думаю, что это хороший подход, так как не ясно, что имя таблицы требуется в случае, если другой разработчик или даже сам попытается снова использовать эту область в будущем. Вероятно, они в конечном итоге выяснят это, но это не кажется хорошей практикой.
Решение №2: используйте подзапрос
Я могу хранить двусмысленные столбцы в подзапросе. Тем не менее, в этом случае и по мере роста таблицы производительность ухудшится.
Это стратегия, которую я использую. Потому что это не требует каких-либо изменений в других областях и условиях. По крайней мере, не так, как я применяю это сейчас.
public function scopeNoChildren(Builder $query): Builder
{
$subQueryChildren = self::select('id', 'parent_id')
->completed();
$sqlChildren = DB::raw(sprintf(
'(%s) AS children',
$subQueryChildren->toSql()
));
return $query->select('orders.*')
->leftJoin($sqlChildren, function ($join) use ($subQueryChildren) {
$join->on('orders.id', '=', 'children.parent_id')
->addBinding($subQueryChildren->getBindings());
})->where('children.id', null);
}
Идеальное решение
Я думаю, что отличное решение для использования запросов без префикса с именем таблицы, не полагаясь на подзапросы.
Вот почему я спрашиваю: есть ли способ, чтобы имя таблицы автоматически добавлялось к методам запросов Eloquent?
Ответы
Ответ 1
Я бы использовал отношения:
public function children()
{
return $this->hasMany(self::class, 'parent_id')
->where('status', self::STATUS_COMPLETED);
}
Order::where('status', Order::STATUS_COMPLETED)
->whereDoesntHave('children')
->get();
Выполняет следующий запрос:
select *
from 'orders'
where 'status' = ?
and not exists
(select *
from 'orders' as 'laravel_reserved_0'
where 'orders'.'id' = 'laravel_reserved_0'.'parent_id'
and 'status' = ?)
Он использует подзапрос, но он короткий, простой и не вызывает никаких проблем с неоднозначностью.
Я не думаю, что производительность будет актуальной проблемой, если у вас нет миллионов строк (я полагаю, что нет). Если производительность подзапроса будет проблемой в будущем, вы можете вернуться к решению JOIN. До тех пор я хотел бы сосредоточиться на читаемости и гибкости кода.
Способ повторного использования отношения (как указывает OP):
public function children()
{
return $this->hasMany(self::class, 'parent_id');
}
Order::where('status', Order::STATUS_COMPLETED)
->whereDoesntHave('children', function ($query) {
$query->where('status', self::STATUS_COMPLETED);
})->get();
Или способ с двумя отношениями:
public function completedChildren()
{
return $this->children()
->where('status', self::STATUS_COMPLETED);
}
Order::where('status', Order::STATUS_COMPLETED)
->whereDoesntHave('completedChildren')
->get();
Ответ 2
Вы должны создать SomeDatabaseBuilder
расширяющий исходный Illuminate\Database\Query\Builder
и SomeEloquentBuilder
расширяющий Illuminate\Database\Eloquent\Builder
и, наконец, BaseModel
расширяющий Illuminate\Database\Eloquent\Model
и перезаписывая эти методы:
/**
* @return SomeDatabaseBuilder
*/
protected function newBaseQueryBuilder()
{
$connection = $this->getConnection();
return new SomeDatabaseBuilder(
$connection, $connection->getQueryGrammar(), $connection->getPostProcessor()
);
}
/**
* @param \Illuminate\Database\Query\Builder $query
* @return SameEloquentBulder
*/
public function newEloquentBuilder($query)
{
return new SameEloquentBulder($query);
}
Затем, в SomeDatabaseBuilder
и SameEloquentBulder
, измените методы для классификации столбцов по умолчанию (или сделайте это необязательным).
Ответ 3
В MySQL есть два хороших способа найти листовые узлы (строки) в списке смежности. Один из них - метод LEFT-JOIN-WHERE-NULL (antijoin), который вы сделали. Другой - подзапрос NOT EXISTS. Оба метода должны иметь сопоставимую производительность (теоретически они делают то же самое). Однако решение подзапроса не приведет к появлению новых столбцов.
return $query->select('orders.*')
->whereRaw("not exists (
select *
from orders as children
where children.parent_id = orders.id
and children.status = ?
)", [self::STATUS_COMPLETED]);