Несколько простых запросов вместо одного большого для загрузки связей в ORM

    Сразу оговорюсь, это не обучающий пост и не провозглашение новой парадигмы )), скорее решение, к которому я пришел, и хочется его обсудить в широкой и честной дискуссии.
    Теперь к сути, представьте, что есть некая ORM, написанная на PHP, в которой описана модель Posts, имеющая связи многие-ко-многим через промежуточные таблицы с другими моделями: Comments, Tags, Categories. Вопрос в том, каким способом лучше поднимать связанные данные, всё сразу или с отложенной загрузкой?

    В сообществе БД преобладает мнение, что данные лучше поднимать одним запросом с кучей join'ов, мол СУБД умная, она сама разберет, как быстрее всего это сделать и чем меньше запросов к базе тем лучше. В моей же практике были случаи, когда на высоконагруженных проектах с большими таблицами несколько простых запросов работали быстрее, чем один большой с несколькими объединениями.
    Со стороны ORM подъем всех данных одним запросом тоже не лучший вариант, потому что, почти всегда, будут подниматься лишние данные, которые в этом месте не нужны (или даже могут помешать, и тогда их еще придется удалять из набора), либо надо иметь набор методов вроде findWithComments, findWithCategoriesAndTags, findWithAllRelations с неизбежным дублированием.
    Таким образом, имеем три способа загрузки связей (методы модели):
    • Один метод find($id), который всегда загружает все данные в одном запросе.
    • Несколько методов find($id), findWithComments($id), findWithTagsAndCategories($id) ...
    • Метод find($id), который загружает только текущую модель + явные методы для загрузки связей getComments(), getTags()… при чем последние методы работают одинаково, как для одного объекта, так и для коллекции объектов (похоже на Composite).

    Если говорить о проектировании какой-то универсальной ORM, последний вариант мне представляется более правильным, причем другого подхода ORM не должна позволять. Расскажу о преимуществах.
    • Кеширование на уровне БД. Маленькие порции данных должны чаще браться из кеша (из-за переиспользования их в разных запросах), чем большие уникальные запросы.
    • Кеширование на уровне приложения. Как правило, данные из разных таблиц имеют разную скорость протухания, если использовать первый способ, то приходиться ориентироваться на данные, которые быстрее всего становятся неактуальными. Если использовать третий способ, то для каждой модели можно указывать собственное время жизни кеша, плюс это дает более гибкое (по-модельное) управление очищением кеша (по событию или в ручную).
    • При росте объемов данных или количества запросов мы имеем готовую архитектуру для вертикального шардинга БД.
    • В каждом случае мы можем загружать только те данные, которые нам нужны.
    • Готовая архитектура для смешанного использования SQL и noSQL, например какие-то модели мы можем перенести в mongo или redis, переписав для этого заранее предсказуемое количество методов.

    Слабое звено такой жесткой изоляции моделей — как делать гибкий поиск сразу по многим критериям, например постов по тегам и категориям.
    В общем, приглашаю к дискуссии, какие еще есть минусы, может кто-то приходил / уходил от такого решения, использовали бы вы такую ORM в ваших проектах?
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 27
    • 0
      Вопрос в том, каким способом лучше поднимать связанные данные, всё сразу или с отложенной загрузкой?

      Я думаю тут должен быть во главе вопрос «зачем», а не «как». Использовать или нет ленивую загрузку (lazy loading) надо решать в каждом конкретном случае.
      Например Doctrine по умолчанию использует ленивую загрузку, но позволяет загрузить все сразу когда это необходимо.

      при чем последние методы работают одинаково, как для одного объекта, так и для коллекции объектов (похоже на Composite).

      Не уловил мысли. Примерно так:
      $post = $repository->find(1);
      $comments = $post->getComments(); // комментарии к посту с id 1
      
      $posts = $repository->findBy(['author' => 'Вася']);
      $comments = $posts->getComments(); // комментарии к постам Васи
      

      Это было бы более чем интересной функциональностью, но я подобного нигде не встречал.
      • +1
        Что-то подобное я видел в YII.
        • 0
          именно это я имел ввиду, наверно так можно сделать если модель и связанная с ней коллекция будут иметь общего предка
          • 0
            ActiveRecord — умеет.
            Для PHP смотри реализации в Yii и Kohana, или в составе Limb
        • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            Зачем же пускаться в крайности?) Используя ORM'ку, никто не мешает неоптимальные/высоконагруженные части писать/потом переписать на raw sql. Таким образом, можно и соптимизировать в скорости написания/поддержки, и не шибко потерять в производительности.
            • 0
              Ну да, это вечная дилемма разработчика — скорость разработки или скорость работы скрипта, но хочется найти какую-нибудь серебряную пулю, которая бы подходила большинству проектов. А нет куска кода на PHP с реальным и удачным DataMapper-ом?
              • 0
                Возможно вам пригодится эта статья.
                • 0
                  Да отличный сайт и статьи, спасибо!
                  • 0
                    Интересно почему во многих примерах про Data Mapper используют явные методы getName(), setName(), а не __get() и __set()?
                    Еще вопрос, если использовать DM, то где должен располагаться код, реализующий сохранение связей? Вопрос с подковыркой ), к примеру, есть модельTag, теги могут одинаковым способом (меняется только название таблицы связей или fk) привязываться к другим моделям Post, QA, Book. С точки зрения понятности кода, привычнее
                    $post = new Post;
                    $post->addTags( [1,2,3] );
                    
                    $qa = new QA;
                    $qa->addTags( [1,2,3] );
                    

                    но с точки зрения дублирования кода, лучше
                    $post = new Post;
                    $qa = new QA;
                    Tag::addTo( $post , [1,2,3]);
                    Tag::addTo( $qa , [1,2,3]);
                    

                    Не бейте сильно, хочется разобраться раз и навсегда )).
                    • НЛО прилетело и опубликовало эту надпись здесь
                      • 0
                        спасибо за развернутый ответ, плюсануть не могу, к сожалению (
                        • 0
                          Код, реализующий сохранение связей, должен быть в методах сущности, которая «владеет» этими связями (не в подчинённых объектах).


                          подумав )), всё таки не могу согласиться с вами до конца. Вариант, когда код привязки располагается в той модели которую привязывают ( и не только модель, но соответствующие шаблоны и контроллеры) имеет очевидные преимущества:
                          1. Одна и та же модель, как правило привязывается ко всем другим одинаково (по моей статистике), т.е. картинки привязываются одним способом ко всем моделям, теги могут привязываться вторым способом, но всё равно одинаковым для всех моделей.
                          2. Дисциплинированность разработчиков: описав один раз привязку тегов — можно быть уверенным, что они будут привязываться единообразно ко всем моделям, и таблицы связей с полями будут единообразные.
                          3. Некоторые модели могут привязываться к десятку других моделей абсолютно одинаково, и в этом случае единственность кода, где это делается, имеет первостепенное значение с точки зрения вероятности ошибок и быстроты внесения правок.
                          4. Гибкость подключения / отключения новых связей, например, не было сначала тегов, потом появилась в них необходимость — добавили модель и контроллер с кодом привязки и всё — можно привязывать теги к любой модели просто указав у неё наличие связи с тегами ( ну и создав таблицу связей)
                          • НЛО прилетело и опубликовало эту надпись здесь
                        • –1
                          По хорошему должно быть что-то вроде
                          $post = new Post;
                          $qa = new QA;
                          $tag1 = new Tag(1);
                          $tag2 = new Tag(2);
                          $tag3 = new Tag(3);
                          
                          $post->addTag($tag1);
                          $post->addTag($tag2);
                          $post->addTag($tag3);
                          
                          $qa->addTag($tag1);
                          $qa->addTag($tag2);
                          $qa->addTag($tag3);
                          


                          Как вариант сама связь

                          $post->addTags([$tag1, $tag2, $tag3]);
                          $qa->addTags([$tag1, $tag2, $tag3]);
                          


                          А с точки зрения дублирования кода можно создать трэйт Taggable с методом addTag/addTags и т. п.

                          Использовать new или фабрику — дело вкуса и требований к тестируемости кода. Я обычно использую new, поскольку потому как юнит тестами контроллеры (а по моему убеждению инстанцы моделей должны создаваться только в них или в вызывающем их коде)толком не покрыть всё равно, а функциональным без разницы.
                          • 0
                            Расскажите за что минус, всем интересно, что ни так?
                  • +1
                    Sphinx вам поможет с поиском =)
                    • +1
                      Хорошая подборка стандартных заблуждений. Начиная с каких-то наивных, про findWithComments, findWithCategoriesAndTags…

                      С незапамятных времён в любом MySQL есть оптимизации специально для формирования вот таких запросов:

                      select *
                      from adv_db_stat
                      where
                         (variant_id = %(A)s or -1 = %(A)s)
                         and
                         (campaign_id = %(B)s or -1 = %(B)s);

                      Тут мы подставляем два параметра А и B. Если A==-1, то подходит любой A. Аналогично для B. Причём, если вы передадите A=1, B=-1, то запрос:

                      select *
                      from adv_db_stat
                      where
                         (variant_id = 1 or -1 = 1)
                         and
                         (campaign_id = -1 or -1 = -1);

                      Будет соптимизирован до

                      select *
                      from adv_db_stat
                      where
                         variant_id = 1;


                      То есть ваша findWithComments — это findWithCategoriesAndTags(comment=55, tag=-1).

                      Ну и так далее.

                      Я бы советовал всё же не ругать SQL, а пользоваться им.
                      • +1
                        наверно мы не поняли друг друга, find() — загрузить просто пост без связей, findWithComments — значит загрузить пост с его комментариями
                        а как ваш пример с этим связан?
                        • 0
                          Да. С этим не связан. Но тогда ваш пример ещё более искусственный.

                          Во-первых. Эти две функции возвращают разные типы данных. В этом случае, наличие двух функций весьма оправдано. Многие зыки программирования будут вас всячески подталкивать к тому, что это разные функции. В этом нет ничего плохого.

                          Во-вторых. Как вы представляете select, который вытащит и пост и все комментарии к нему? Это либо два разных селекта (и это хорошо), либо один безумный селект «все комментарии этого поста и сам пост вместе с каждым комментарием» (так делать не надо).

                          Одним словом, вас не устраивает то, что SQL (да и многие другие языки) для получения разных типов данных предлагает использовать разные функции? Вы хотите, чтобы одна функция (один запрос) могла возвращать разные структуры данных? И как их потом обрабатывать? Писать следующую функцию, которая может обработать что угодно? Вот теперь я вас точно не понимаю.
                          • 0
                            Вовсе не обязательно они должны возвращать разные типы данных. просто $result->comments может быть неинициализрирована или инициализированным специальным значением.

                            SELECT * FROM post LEFT JOIN comment WHERE post.id = comment.post_id
                            

                            Что безумного?
                            • 0
                              То что post.* будет повторяться столько раз, сколько комментариев. Если комментариев 1000, то 999 раз всё содержимое поста будет зря гонять трафик и засирать память, процессор, и кэш mysql.
                      • 0
                        при чем последние методы работают одинаково, как для одного объекта, так и для коллекции объектов

                        Какое-то у меня недоверие к такому подходу.И если ленивая загрузка в getComment() ещё оправдана для одного объекта, то для коллекции хотя бы метод должен называться по другому, например loadComments(). По-моему выражение $comments = $posts->getComment(); очень неочевидное. Что оно возвращает? Меняется ли $posts: Имхо, лучше $posts->loadComments(); или $posts->fillComments();
                        • 0
                          согласен с loadComments(), подразумевается, что он загружает данные в коллекцию или в один объект, т.е. и там и там должен быть loadComments().
                          • 0
                            По хорошему этот метод не должен быть в самом объекте. Да и в коллекции тоже. Если взять пример из первого коммента zloyusr, то я делал бы как-то так:

                            $post = $repository->find(1);
                            $comments = $post->geComments(); // lazy load
                            
                            $post = $repository->find(2);
                            $repository->loadComments([$post]); // direct load
                            
                            $posts = $repository->findBy(['author' => 'Вася']);
                            $repository->loadComments($posts); // direct load
                            
                            
                            • 0
                              По хорошему этот метод не должен быть в самом объекте.

                              Его и не будет в объекте модели. Он по сути должен быть в объекте коллекции моделей. Но реализация такого будет я думаю более чем нетривиальна, если делать ее универсальной для всех коллекций.

                              Ваш вариант с отдельными методами в репозиториях значительно проще в реализации, и достаточно удобен на мой взгляд.
                          • 0
                            При чем это физически один метод, но сам должен определять в каком контексте он вызван.

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