Pull to refresh

Comments 26

Это справедливо к любым ORM на любом языке.

    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-го).

Вы почитайте исходники 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).


При таком подходе происходит всего два запроса.

Аналогичные запросы генерирует ORM, если использовать Client::with('addresses') или $client->load('addresses') для вашего примера.
Проблема в следующем — если у вас у клиента 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() — что, судя по логике, не гарантирует нам именно первый результат.
Для таких случаев подойдёт вынести поиск первой даты публикации в подзапрос, и уже результат подзапроса использовать как условие фильтрации. Но если требуется сделать это более чем для десяти пользователей — такой запрос может работать медленнее чем хотелось бы.

И MySQL, и MariaDB тоже дадут ошибку, если включена sql режим ONLY_FULL_GROUP_BY (в мускуле с 5.7.5 по дефолту включен, по марии не нашел быстро)

И наконец, давайте изменим наш контроллер и шаблон. Мы должны использовать scope в нашем контроллере для жадной загрузки первой статьи
а автор при этом из роутера напрямую лезет к модели
Да, в оригинале написан роутер, но Closure в роуте — это по сути анонимный контроллер. И этот же самый код будет справедлив, если его перенести в метод контроллера.

Вы считаете, что лучше оставить «роутер» и это будет понятнее?
Лезет из роутера через анонимку, которая и есть контроллер.

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.
Можно, но тогда зачем вам прелести ORM? Он подразумевается, что вы не будете писать чистый SQL, с диалектом определённой базы данных, все «особенности» работы с БД он берёт на себя.

ORM и DBAL формально вещи не связанные. Вполне можно написать SQL запрос руками, а потом скормить результат ORM, чтобы она разложила по объектам. Главное, суметь объяснить как раскладывать. Пока говорим о рид моделях.

К сожалению, нельзя написать SQL-запрос руками, а потом скормить ORM для раскладки, разве что в самых тривиальных случаях.
— Во-первых, мы не знаем заранее, что будет запрашиваться, а что нет — это выясняется только в процессе работы (опять же пример GraphQL тут очень показателен).
— Во-вторых, может и не быть SQL-я для части из запросов (например, читаем из memcached или какого-нибудь nosql).
— В-третьих, даже если SQL и есть, может быть шардинг. Сто тыщ датабаз, и из пары десятков надо прям щас запросить, пользователь ждет. Как тут запрос писать руками…
— В-четвертых, все, что можно запросить параллельно, надо запрашивать параллельно. Выборка из соседних шард, или даже выборка из одной и той же БД, но в соседнем коннекте. Отслеживание всех этих зависимостей руками просто невозможна (какие запросы можно пускать параллельно, а в каких надо дождаться данных от предыдущих).

И вот решение всего этого — async-await и DataLoader-элайк паттерн, причем решение без потерь — чистый выигрыш.

По-моему, крайне мало случаев, когда можно доверить "написание" запроса ORM, а разработчику нельзя. Я же не говорю о всех случаях, только о тех, когда ORM создала крайне неэффективный запрос(ы). 1+N, 2 c IN (1, 2, 3, ..., 999) и т.п. просто руками заменить на голый SQL

Во-первых, мы не знаем заранее, что будет запрашиваться, а что нет — это выясняется только в процессе работы (опять же пример GraphQL тут очень показателен).

Как раз таки в GQL заранее известно что запрашивается, а что нет, ещё ДО самого процесса работы.

Извините, но это не нормально. ORM просто шлют всю реляционность в задницу, добавляя кучу ID в текст запроса, гоняя кучу байт сначала на клиент, потом обратно на сервер. Мало того, что запрос получается очень длинным, мало того, что это кушает дополнительные CPU сервера БД на парсинг такой строки, так он рано или поздно упирается в лимит параметров движка БД: MSSQL: 2100, Oracle: 64000, Mysql: 65535.
Т.е. если вдруг у вас будет табличка на 2100 записей, то первым начнет вылетать уже MSSQL. Потом не выдержит ORACLE, затем и MySQL. Лечить на строне ORM довольно трудно, гораздо проще написать один (!) чистый запрос с обычным LEFT JOIN, который порешает сразу 99% проблем в будущем.
Вся любовь и ненависть к ORM в этом подходе к быстрым запросам, но без LEFT JOIN'ов.
В целом, я абсолютно согласен с вами, что «чистые» запросы, написанные руками почти всегда эффективнее, чем ORM. И критические участки почти всегда следует писать руками (и это мы еще не говорим про overhead самой ORM даже при условии генерации абсолютно одинаковых запросов).
Тем не менее, ORM придуманы тоже не от скуки и снимают очень много головной боли и добавляют удобства взаимодействия с кодом во многих случаях.
Поэтому использовать ORM или нет — всегда компромисс.

Конкретно выделенное вами на скриншоте — это издержки «автор использовал UUID в качестве идентификатора». Сама ORM конкретно к этому случаю не очень причастна и не сильно изменила ситуацию.

Из моего опыта, есть несколько категорий разработчиков (опускаем пограничные состояния и утрируем):
— те, кто работают только с ORM и даже не смотрят на конечные запросы (даже на их количество).
— те, кто работают с sql, особо не думая, какой он
— те, кто работают с ORM, но следят за запросами, критические участки переписывают на plain sql
— те, кто работают с sql и понимают почему именно. Как правило, это реальный highload, но таких проектов не так много и «к этому нужно прийти».

Вряд ли этой статьей я удивлю кого-то из последней категории, но вот ребятам из третьей категории может быть интересно.

Я для таких случаев пишу запрос. Чаще всего целиком запрос, на SQL. И потом распихиваю данные по моделям (можно массив, но если везде объекты, лучше объекты). Таких специфических случаев не так много обычно. В более простых случаях я согласен с подходом Eloquent, и черт с ним, что он не оптимален. Да, выполняется лишний запрос. Но это по идее должен быть WHERE primary_key IN (val1,… valN), это обычно быстро. А если у вас теакой запрос генерится с тысячами айдишек — возможно, что-то еще не так. Или, как я уже сказал, пишите хороший SQL ручками.

Sign up to leave a comment.

Articles