Laravel Eloquent с () быстрой загрузкой и условиями whereHas ()

У меня есть 2 таблицы: "Автор" и "Книга" со следующими прямыми отношениями:

class Author extends Model
{
    public function books() {
        return $this->hasMany('App\Book');
    }
}

class Book extends Model 
{
    public function author() {
        return $this->belongsTo('App\Author');
    }
}

Модель Book имеет атрибут price (число с плавающей запятой) и category (строка).

Я пытаюсь найти эффективный способ сделать эти 3 вещи в 1 запросе:

  1. получить список всех authors, у которых есть books из category комедии, created_at между date1 и date2.
  2. авторы в списке должны иметь дополнительный атрибут highest_price, который представляет собой самую высокую цену из всех книг автора, соответствующих критериям из шага 1.
  3. список книг должен быть отсортирован по убыванию по price, а конечный список авторов также должен быть отсортирован по убыванию по этому новому атрибуту highest_price.

Результат должен быть таким:

[
    {
        id: 1,
        name: 'Author ABC'
        highest_price: 15
        books: [
            {
                id: 1,
                name: 'book1',
                category: 'comedy',
                price: 15
            },
            {
                id: 2,
                name: 'book2',
                category: 'comedy',
                price: 10
            }
        ]
    },
    {
        id: 10,
        name: 'Author XYZ'
        highest_price: 12
        books: [
            {
                id: 3,
                name: 'book3',
                category: 'comedy',
                price: 12
            }
        ]
    }
]

Что я получил до сих пор:

Author::with(['books' => function($query) {
    $query->where('category', 'comedy')
        ->where('created_at', '>=', $someFromDate)
        ->where('created_at', '<=', $someToDate);
}])
->has('books', '>', 0)
->get()
->dd();

Но это просто дает мне всех авторов, у которых есть книги, это не учитывает условие при нетерпеливой загрузке...

Возможно ли это даже в 1 запросе? Любая помощь будет оценена по достоинству!

РЕШЕНИЕ

Благодаря ответам я узнал, что мне нужна комбинация with() и whereHas() (ответ нашел здесь: https://stackoverflow.com/a/29594039/5297218). Поскольку условия «где» должны быть в обоих методах, я использовал область запроса на модели Author:

public function scopeWithAndWhereHas($query, $relation, $constraint){
  return $query->whereHas($relation, $constraint)
               ->with([$relation => $constraint]);
}

Для дополнительного атрибута highest_price я использовал метод transform()в Коллекции в сочетании с методом max() чтобы получить максимальную цену. Это окончательный результат:

Author::withAndWhereHas('books', function($query) use ($category, $date){
    $query->where('category', $category)
        ->where('created_at', '>=', $date->get('start'))
        ->where('created_at', '<=', $date->get('end'))
        ->orderBy('price', 'desc');
})
->get()
->transform(function ($author){
    $author['highest_price'] = $author->books->max('price');
    return $author;
})
->sortByDesc('highest_price')
->values();

PS: это решение в 42 раза эффективнее того, что я делал раньше!


person SebSob    schedule 14.02.2019    source источник


Ответы (3)


Этот подзапрос на with() будет ограничивать то, что загружается в отношение автора к книгам, но не ограничивает возвращаемые автором отношения. Вы пробовали whereHas() вместо with()? Это вернет только авторов, у которых есть книги, соответствующие этим критериям. Это возможно с помощью одного запроса, но это будет немного зависеть от того, что вам нужно. Добавление поля highest_price может быть лучше отрегулировано для другого отношения.

Author::with('books', 'highest_price')->whereHas(['books' => function($query) use($someFromDate, $someToDate) {
$query->where('category', 'comedy')
    ->where('created_at', '>=', $someFromDate)
    ->where('created_at', '<=', $someToDate);
}])
->get()->sortByDesc('highest_price.highest_price');

Дайте знать, если у вас появятся вопросы.

person N Mahurin    schedule 14.02.2019
comment
Потрясающий! whereHas() был именно тем, что я искал. Теперь у меня есть список авторов, но как я могу добавить этот дополнительный атрибут, который содержит highest_price списка его/ее книг, соответствующих whereHas() критериям? - person SebSob; 15.02.2019
comment
Мне нравится решение, которое вы разместили @SebSob, и оно будет работать для того, что вам нужно. Я просто должен помнить, что transform() заставит PHP перебирать коллекцию, и это замедлит вас, если коллекция станет слишком большой (миллионы+, так что пока это не имеет большого значения). Однако есть несколько способов добавить мутированные поля к тому, что возвращает ваш запрос, поэтому, если вы когда-нибудь столкнетесь с замедлением в строке, вы можете добавить его без необходимости использования преобразования. - person N Mahurin; 15.02.2019
comment
Спасибо @N Mahurin - Спасибо за подсказку! Я знаю, что могу добавить мутатор getHighestPriceAttribute() в модель автора. Но это должно соответствовать заданным условиям (категория и даты); и я не был уверен, как передать их мутатору. Кроме того, будет ли мутатор запрашивать БД для каждой записи? Не уверены, что это лучше для производительности? - person SebSob; 15.02.2019
comment
Проблема с мутатором в такой модели заключается в том, что вы не можете сортировать по нему до тех пор, пока не вызовете get() для возврата коллекции и полного набора данных. Чего мы хотим избежать после того, как он станет таким большим, поскольку PHP не будет идти в ногу с БД с точки зрения скорости при определенном размере. Но к тому времени, когда у вас будет достаточно записей, чтобы оправдать это, вы также будете использовать нумерацию страниц и другие приемы, чтобы сократить время загрузки страницы. - person N Mahurin; 15.02.2019
comment
В порядке. И, конечно же, я буду отображать не более 50 авторов и не более 100 книг на каждого автора. - person SebSob; 15.02.2019

Существует два разных результата при использовании условий отношения:

1- Ограничение результатов ОТНОШЕНИЯ на основе условия (это то, что вы делаете, но не то, что вы хотите):

$authors = Author::with(['books' => function($query) {
    $query->whereNotNull('published');
}])->get();

Здесь у вас будут ВСЕ авторы, независимо от того, опубликовали они книги или нет. У тех, у кого есть опубликованные книги, будет информация о книге. Все остальные не будут.

2- Ограничение результатов PARENT на основе условия отношений (это то, что вы должны использовать):

$authors = Author::whereHas('books', function($query) {
     $query->whereNotNull('published');
})->get();

Здесь у вас будут ТОЛЬКО авторы, опубликовавшие книги.

Примечание. Если вы хотите загрузить свою модель книги, вам все равно придется использовать ->with('books')

person jvicab    schedule 14.02.2019
comment
Спасибо за четкое объяснение! whereHas() действительно то, что я искал! - person SebSob; 15.02.2019
comment
Кроме того, если я хочу загрузить модель книги, ->with('books')будет добавлять все книги автора, а не только те, которые соответствуют whereHas()критериям. Есть идеи? - person SebSob; 15.02.2019

Из результата, заданного в вашем вопросе, ниже должно быть то, что вам нужно:

Author::whereNotNull('highest_price')
    ->has('books')
    ->with(['books' => function($query) use($startDate, $endDate){
        return $query->where('category', 'comedy')
            ->whereBetween('created_at', [$startDate, $endDate])
            ->orderBy('price', 'desc')
            ->select('id', 'name', 'category', 'price', 'author_id');
    }])
    ->orderBy('highest_price', 'desc')
    ->select('id', 'name', 'highest_price')
    ->get()

Объяснение :

Самый простой способ обойти эти проблемы — сначала структурировать построитель родительских запросов, который в данном случае является автором. Поэтому в соответствии с вашим требованием мы проверили наличие наивысшей цены, отфильтровали авторов, у которых есть хотя бы 1 книга, упорядоченные по описанию наивысшей_цены, и выбрали нужные столбцы из таблицы авторов.

Теперь, изучая нетерпеливую загрузку, мы сделали то же самое, отфильтровали диапазон дат created_at, упорядочили по цене и выбрали необходимые столбцы. Я намеренно добавил дополнительные столбцы author_id, которые являются внешним ключом, чтобы отображались результаты книги.

person Mihir Bhende    schedule 15.02.2019
comment
Спасибо, но это не сработает, потому что у Автора нет столбца highest_price, это то, что нужно добавить на лету, и в нем содержится самая высокая цена книг автора, соответствующих критериям. - person SebSob; 15.02.2019