Комментарии 26
const flows = await user
.flows()
.where({ is_active: true })
.with("leads", builder => {
builder
.where("leads.created_at", ">", from_date)
.where("leads.created_at", "<", to_date)
.with("transactions", builder => {
builder
.where("transactions.created_at", ">", from_date)
.where("transactions.created_at", "<", to_date);
});
})
.with("offer")
.withCount("clicks", builder => {
builder
.where("created_at", ">", from_date)
.where("created_at", "<", to_date);
})
.orderBy("id", "desc")
.fetch();
Красоту еще не навел, только запросы тестирую.
Неправда. Чистое решение есть, называется паттерн DataLoader, но оно работает только с async-await, которые в PHP по необъяснимой причине еще не завезли (привет из 2020-го).
Завезли ещё в 5.5. Только "не только лишь всем" дозволено понимать, что async/await лишь синтаксический сахар для yield)))
$flows = yield $user
->flows()
->и т.д.
https://nikic.github.io/2012/12/22/Cooperative-multitasking-using-coroutines-in-PHP.html
Вы почитайте исходники hack-а, исходники V8, а потом посмотрите, как TS или Babel транспилируют async-await в конечный автомат или в yield-ы. Чтобы понять, что это не просто синтаксический сахар. (Ну или можно назвать любой язык программирования «лишь синтаксическим сахаром над ассемблером»).
Если говорить про JS, то ни yield-ы, ни Stream-ы невозможно нормально использовать без слез. Это тупиковые ветки развития (вернее, промежуточные ветки). Только async-await и async generators (да и они тоже не совершенны, просто ничего лучше пока нет в бескорутинном подходе).
Если говорить про JS, то ни yield-ы, ни Stream-ы невозможно нормально использовать без слез.
Мы не по JS говорим. Но в PHP использовать yield так же печально, т.к. требует реализации эвентлупа. А его в PHP из-коробки нет. С другой стороны, с помощью пары пинков его можно реализовать и получится тоже самое, что и с async/await.
Вот, на коленке запилил для обычных коллбеков. С таким же успехом можно и генераторы и промизы резолвить, просто добавляя таски в процесс: https://gist.github.com/SerafimArts/4ce7f67052fefd5efcabdcb96bb00847
На примере развития JS/TS/Babel просто хорошо иллюстрировать «эволюцию человеческой мысли» в вопросах асинхронности. (На самом-то деле это скорее была эволюция человеческого отчаяния и костылестроения, но время все лечит, и вроде бы постепенно выправляются все косяки, и сейчас более-менее все улучшилось.)
Нужно также иметь в виду, что в Фейсбуке половина из 20-гигабайтного репозитория кода на Hack (aka «php, каким он должен был бы стать, но не стал») увешана async-await и всеми этими вполне очевидными абстракциями поверх (типа DataLoader) уже лет 7-8 как. Задолго до того, как async-await появились в JS и тем более в Python. И, по сути, все уже было изобретено (даже еще раньше, это пошло с C#). Как говорится, «будущее уже давно настало, просто оно еще неравномерно распределено».
И конечные автоматы, и yield-ы фейсбучная кодовая база проходила в своей эволюции. Как, впрочем, проходил их потом и JS (и прошел), а сейчас вот проходит PHP (упомянутый вами выше пример кода из этой серии). Вот бы люди сразу же брали и делали так, как уже другие давно сделали (и «картинка сложилась»), а не шли костылями.
Я еще к чему. Проблема N+1 queries давным-давно решена (но не все об этом знают, а из тех кто знает, некоторые знают лишь кусочно — вот типа фраз “синтаксический сахар над yield”). Решение называется async-await & IO batching, а если смотреть на все с высоты end to end архитектуры, то это, конечно же, GraphQL.
Я решаю эту проблему так.
select from clients
select from addresses where client_id IN (1,2,4).
При таком подходе происходит всего два запроса.
Проблема в следующем — если у вас у клиента 1 адрес, то проблемы точно нет и этот способ хороший и удобный. А если у вас в среднем на клиента может быть 100 адресов, то это уже будет проблема, так как запросов будет два, но объектов, которые загрузит система из БД, а затем превратит в ваши классы, будет очень много. Это все память + CPU на сериализацию / десериализацию.
Так же можно сделать вот так:
select userId, id, title, createdAt, min(createdAt) as first
from Article
group by userId
having createdAt = first
LIMIT 10
Я не совсем уверен, что данный sql правильно отработает в любой базе, но в mariaDB он мне отдаёт именно желаемый результат. Некоторые базы вернут ошибку, и потребуют включить не группирующиеся колонки в агрегатную функцию, например в КХ это any() — что, судя по логике, не гарантирует нам именно первый результат.
Для таких случаев подойдёт вынести поиск первой даты публикации в подзапрос, и уже результат подзапроса использовать как условие фильтрации. Но если требуется сделать это более чем для десяти пользователей — такой запрос может работать медленнее чем хотелось бы.
И наконец, давайте изменим наш контроллер и шаблон. Мы должны использовать scope в нашем контроллере для жадной загрузки первой статьиа автор при этом из роутера напрямую лезет к модели
Вы считаете, что лучше оставить «роутер» и это будет понятнее?
UPD: а вот и я наступил на грабли «обновляй коменты перед отправкой» :)
Не рассматривали вариант с ограниченной жадной загрузкой? https://laravel.com/docs/7.x/eloquent-relationships#constraining-eager-loads
Что-то подсказывает, что так можно нативным образом подгрузить. Сейчас на вскидку поломались функции relationLoaded и возможно какие-то еще
Ограниченная жадная загрузка ограничивает всю выборку, а не выборку связанных данных.
Попробовал варианты подгрузки. Если использовать подобный скоуп:
$users = \App\User::with(['articles' => function ($query) {
$query->orderBy('created_at', 'asc')
->limit(1);
}])->get();
то генерируется примерно такой sql:
select * from `articles` where `articles`.`user_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50) order by `created_at` asc limit 1
Что генерирует не верные данные. Без лимита смысла нет (туда только сортировка уходит, а количество загружаемых моделей остается таким же), whereColumn он не умеет. Навскидку, для этого нужно будет прокидывать либо join внутрь, либо subselect, что сведется к тому же подходу, только будет выглядеть более громоздко.
Если у вас получится, буду рад, если поделитесь решением и выводами.
ORM и DBAL формально вещи не связанные. Вполне можно написать SQL запрос руками, а потом скормить результат ORM, чтобы она разложила по объектам. Главное, суметь объяснить как раскладывать. Пока говорим о рид моделях.
— Во-первых, мы не знаем заранее, что будет запрашиваться, а что нет — это выясняется только в процессе работы (опять же пример GraphQL тут очень показателен).
— Во-вторых, может и не быть SQL-я для части из запросов (например, читаем из memcached или какого-нибудь nosql).
— В-третьих, даже если SQL и есть, может быть шардинг. Сто тыщ датабаз, и из пары десятков надо прям щас запросить, пользователь ждет. Как тут запрос писать руками…
— В-четвертых, все, что можно запросить параллельно, надо запрашивать параллельно. Выборка из соседних шард, или даже выборка из одной и той же БД, но в соседнем коннекте. Отслеживание всех этих зависимостей руками просто невозможна (какие запросы можно пускать параллельно, а в каких надо дождаться данных от предыдущих).
И вот решение всего этого — async-await и DataLoader-элайк паттерн, причем решение без потерь — чистый выигрыш.
По-моему, крайне мало случаев, когда можно доверить "написание" запроса ORM, а разработчику нельзя. Я же не говорю о всех случаях, только о тех, когда ORM создала крайне неэффективный запрос(ы). 1+N, 2 c IN (1, 2, 3, ..., 999) и т.п. просто руками заменить на голый SQL
Во-первых, мы не знаем заранее, что будет запрашиваться, а что нет — это выясняется только в процессе работы (опять же пример GraphQL тут очень показателен).
Как раз таки в GQL заранее известно что запрашивается, а что нет, ещё ДО самого процесса работы.
Т.е. если вдруг у вас будет табличка на 2100 записей, то первым начнет вылетать уже MSSQL. Потом не выдержит ORACLE, затем и MySQL. Лечить на строне ORM довольно трудно, гораздо проще написать один (!) чистый запрос с обычным LEFT JOIN, который порешает сразу 99% проблем в будущем.
Вся любовь и ненависть к ORM в этом подходе к быстрым запросам, но без LEFT JOIN'ов.
Тем не менее, ORM придуманы тоже не от скуки и снимают очень много головной боли и добавляют удобства взаимодействия с кодом во многих случаях.
Поэтому использовать ORM или нет — всегда компромисс.
Конкретно выделенное вами на скриншоте — это издержки «автор использовал UUID в качестве идентификатора». Сама ORM конкретно к этому случаю не очень причастна и не сильно изменила ситуацию.
Из моего опыта, есть несколько категорий разработчиков (опускаем пограничные состояния и утрируем):
— те, кто работают только с ORM и даже не смотрят на конечные запросы (даже на их количество).
— те, кто работают с sql, особо не думая, какой он
— те, кто работают с ORM, но следят за запросами, критические участки переписывают на plain sql
— те, кто работают с sql и понимают почему именно. Как правило, это реальный highload, но таких проектов не так много и «к этому нужно прийти».
Вряд ли этой статьей я удивлю кого-то из последней категории, но вот ребятам из третьей категории может быть интересно.
Я для таких случаев пишу запрос. Чаще всего целиком запрос, на SQL. И потом распихиваю данные по моделям (можно массив, но если везде объекты, лучше объекты). Таких специфических случаев не так много обычно. В более простых случаях я согласен с подходом Eloquent, и черт с ним, что он не оптимален. Да, выполняется лишний запрос. Но это по идее должен быть WHERE primary_key IN (val1,… valN), это обычно быстро. А если у вас теакой запрос генерится с тысячами айдишек — возможно, что-то еще не так. Или, как я уже сказал, пишите хороший SQL ручками.
Решение проблемы N+1 запроса без увеличения потребления памяти в Laravel