ORM – зло или Как я пытался кэшировать Propel в Symfony

    Работая над одним проектом (соц. сетью) передо мной встала задача «подружить» модель данных с memcache. Как Вы уже поняли из заголовка, проект написан на symfony framework, а в качестве ORM используется Propel.

    Зачем кэшировать модель, спросите вы, если можно просто использовать кэш View?
    Как показала практика (сначала был использован именно кэш view) это оказалось не целесообразно. При каждом изменении данных пользователем необходимо было сбрасывать кэш сразу нескольких закэшированных блоков. Количество этих блоков с развитием проекта растет, логика усложняется. Чтобы разгрузить мозг и избежать ошибок, и было принято решение кэшировать модель.
     
    Как оказалось Propel вообще не предназначен для кэширования, и при отключенном кэша-view генерирует ОЧЕНЬ большое количество запросов.
    Мой рекорд 250!!! Почему спросите вы? Потому что Propel интересным образом реализует связи между объектами. Вместо того чтобы брать объект из единой точки ( например методом retriveByPk ), он делает запрос в базу данных!!! А это значит, что если у нас есть объекты юзера, его фоток, его записей из блога, то с каждого объекта мы получаем запрос в базу данных. Есть у нас, например, в выборке 20 фоток – получи 20 запросов в базу, чтобы узнать логин автора фотки. А если у фотки есть еще Альбом? То 40. А если это главная страница, на которой есть фотки, записей из блога и еще много чего?! Все 250 :)
     
    Что интересно в Propel’e об этом предупреждают :)
    Вот пример:
    public function getsfGuardUser(PropelPDO $con = null)
      {
        if ($this->asfGuardUser === null && ($this->user_id !== null)) {
          $c = new Criteria(sfGuardUserPeer::DATABASE_NAME);
          $c->add(sfGuardUserPeer::ID, $this->user_id);
          $this->asfGuardUser = sfGuardUserPeer::doSelectOne($c, $con);
          /* The following can be used additionally to
            guarantee the related object contains a reference
            to this object. This level of coupling may, however, be
            undesirable since it could result in an only partially populated collection
            in the referenced object.
            $this->asfGuardUser->addPhotos($this);
           */
        }
        return $this->asfGuardUser;
      }

    * This source code was highlighted with Source Code Highlighter.

    А можно всего-лишь:
     public function getsfGuardUser(PropelPDO $con = null)
     {
      return sfGuardUserPeer::retrieveByPK($this->getUserId(), $con);
     }

    * This source code was highlighted with Source Code Highlighter.

     
    Таким вот простым переопределением можно избавиться от «лишних» запросов.
    Правда, если связей много, придется изрядно покопаться в коде.
     
    Теперь перейдем непосредственно к кэшированию. Хотелось, чтобы после вызова метода retrieveByPK возвращался объект из кэша, если он там есть, а если нет, то брался из базы и сохранялся в кэш. Что самое интересное, в propel есть пул загруженных объектов. Этот пул всего лишь временное хранилище.
    Вот пример генерируемых методов для модели юзера.
    public static function addInstanceToPool(sfGuardUser $obj, $key = null);
    public static function removeInstanceFromPool($value);
    public static function getInstanceFromPool($key);
    public static function clearInstancePool();

    * This source code was highlighted with Source Code Highlighter.

     
    И если переопределить эти методы таким образом, чтобы объекты, попадающие в пул одновременно и кэшировались в memcache, то решится задача кэширования объектов модели. Правда, это не изящное решение, но в Propel объекты модели изолированы друг от друга. И если и можно что-то сделать, то на более низком уровне, уровне frameworka. А значит, что такие изменения необходимо учитывать при обновлении symfony, что очень неудобно.

    Propel не стоит использовать для построения крупных и сложных веб-проектов.

    UPD
    Join'ы не подходят, т.к. много join'в использовать не рекомендуется, это доп нагрузка на базу, т.к.запрос сложнее. При большой нагрузке это существенно. А вот если все делать простыми запросами и связи дергать по праймари ключам, то все встает на свои места. Пример. Выборка 20 фоток, у них есть альбомы, авторы, аватарки авторов, аватарки альбомов. С join'и будет запрос с соеденением 5 таблиц!!! А если таких воборок 5 на странице!? А в моем случае именно так!

    UPD2
    Внимательно читайте статью ))

    UPD3
    Продолжение истории
    Как я подружил «memcache» и Propel в Symfony
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 16

      +1
      А слабо почитать документацию и переопределить генератор кода Propel'a свои классом, который будет генерировать эти методы нужным вам образом? В Propel'е для этого все есть.
        –1
        Propel устанавливается плагинов в Symfony и обновляется непосредственно его разработчиками. Если мне необходимо переопределить генератор кода, то надо изменять плагин. А это значит я меняю код фреймворка.
        В документации по Symfony способа обойти эту ситуацию я не нашел. Если знаете, поделитесь ссылкой.

          0
          При создании проекта у вас должен создаваться файл config/propel.ini. В нем должно быть можно указывать свой класс-генератор.
            0
            Точно. Спасибо за информацию. Обязательно попробую. Единственное, что огорчает, что это решение не из коробки (
              0
              А оно и не должно быть из коробки. Ссылка может быть как на первичный ключ, так и на колонку с уникальным индексом. Предлагаете все это поддерживать? Мне кажется, что лучше уж дать возможность легко кастомизировать генератор, чем пытаться предусматривать все подряд.

              В любом случае вы можете запостить фичу на сайте пропеля: propel.phpdb.org/ :) Кстати, на нем же можно почитать что можно сделать в propel.ini.
        0
        Спасибо, как раз на днях хотел заняться чем-то похожим. Главное не забывать что при обновлении/создании объекта модели надо обновлять пул (и кэш) или он обновляется автоматически? (в недрах Propel еще не лазил)

        P.S. А в сторону Doctrine не смотрели? Там аналогичная ситуация с запросами и кэшированиемили получше?

          0
          >что при обновлении/создании объекта модели надо обновлять пул (и кэш) или он обновляется автоматически
          Обновлять пул не надо, т.к. при вызове метода save обновленный объект кладется в пул автоматически. Необходимо только переопределить 4 указанных в статье метода.

          >А в сторону Doctrine не смотрели?
          Нет не смотрел. Но там кажется эта ситуация решена.
            0
            >Необходимо только переопределить 4 указанных в статье метода.
            Спасибо

            >Нет не смотрел. Но там кажется эта ситуация решена.
            Пожалуй, надо будет с ним поразбираться для нового проекта. При изучении synfony упустил этот аспект, но в последнее время часто слышу, что Doctrine как-то поразумнее сделан, чем Propel.
          +1
          Сразу предупреждаю, с Propel не работал, и мысли тут будут общего плана.

          1. Кеширование — важная проблема, но задачи «есть у нас, например, в выборке 20 фоток – получи 20 запросов в базу, чтобы узнать логин автора фотки» решаются join'ами (+ in-запросами), а никак не кешированием. Propel не умеет делать join'ы? Ну если не умеет, тогда его и правда не стоит использовать.

          2. Проблемы с различными open-source инструментами лучше всего обсуждать в баг-трекере соответствующих проектов (если есть конкретные замечания) и в списках рассылки (если не знаешь, как сделать правильно). Если инструмент известный, популярный и хорошо написанный, очень часто выясняется, что человек просто как-то неправильно использует инструмент.

          3. ORM — не зло. Из того, что что-то неочевидно в каком-то конкретном ORM не следует, что все ORM-зло. У меня, например, очень положительный впечатления от джанговского ORM (который, правда, не совсем и ORM). По поводу Propel опасения из-за жутковатого синтаксиса есть, это да.

          4. А вот патчить исходники библиотек, ломая обновлябельность — действительно зло)
            0
            1. У меня иное немного мнение на этот счет. Дело в том, что если использовать join'ов в Propel'e продолжая пример указанный в статье на 20 объектов фоток создастся 20 экземпляров объекта юзера, а не один на всех.
            2. Согласен, но ведь это скорее рекомендация, а не необходимость )
            3. Согласен.
              +1
              уже 3 года как нет уникальных экземпляров — уникальность определяется по первичным ключам: propel.phpdb.org/trac/ticket/280

              лень разобраться а статью писать не лень?
            0
            Автор не разобрался а туда-же…
            методы get...Join… никто не отменял, и не будет 250 запросов, пропел очень хорошо ведет себя со связями — вы сами определяете где что вам нужно
              +1
              в Doctrine это решается leftJoin, посмотрите на досуге(:
                0
                угу, в пропел это тоже решается встроенными средствами, автор только не дошел
                0
                Про джоины ерунду пишете. Ну да, джоины — нагрузка на базу, но дергать все по pk отдельными запросами — гораздо большая нагрузка на базу.

                Вот когда столкнетесь с реальными проблемами с джоинами (запросы с join'ами будут узким местом при правильно расставленных индексах), а не выдуманными (с чьих-то слов «не рекомендуется»), то уже стоит думать дальше — могут помочь in-запросы со списками id, денормализация или какие-нибудь нереляционные СУБД вроде redis или couchdb. Но в 99% не столкнетесь.

                Сейчас же Вы просто свалили на ни в чем не повинный Propel все проблемы, вызванные неправильным использованием реляционной СУБД.
                  0

                  Only users with full accounts can post comments. Log in, please.