Сущностей много — код один

    Как известно в работе вёб программиста часто нужно делать что то достаточно быстро, для этого придумали множество всяческих инструментов ООП, фреймворки и много всего, но как не порождать монстров, а делать всё быстро и качественно?
    Часто увлекаясь проектированием получается не то что хотелось бы, от проекта к проекту навык совершенствуется и опыт растёт.
    Сейчас хотел бы поделиться своим опытом.

    Рассмотрим банальный пример

    Посты — Комментарии и пользователи которые всё это генерируют.
    У всего есть рейтинг, это и будет нашей проблемой.

    мы бы могли поступить например так:

    Чем не вариант? Для каждой таблицы есть своя таблица с рейтингом с точки зрения баз данных всё верно.
    Но вам вряд ли захочется описывать класс для каждой таблицы с рейтингом тем более учитывая то что этот код будет повторятся из раза в раз, а если у вас будет куда больше сущностей для которых потребуется рейтинг? В результате всё расплодится до невероятных масштабов.

    Потому предлагаю более гибкий вариант:


    Таблица с рейтингом у нас теперь одна, и туда внесено дополнительное поле где указано от чего этот рейтинг.
    Правда так проще?

    Класс для установки рейтинга будет очень прост (решения для Zend Framework):

    class Rating extends Zend_Db_Table_Abstract
    {
      public function setRating($item_id, $user_id, $table)
      {
         /*
         * здесь записываем в нашу таблицу с рейтингом, кто проголосвал и за что проголосовал
         * $table - нужно например для того чтобы определить item_type, это может быть именем таблицы
         */
      }
      
      public function getRating($item_id, $table)
      {
        /*
         * Здесь получаем значение рейтинга для какого либо итема,
         * Например при помощи mysql функции count
         */
      }
      
      public function isVoted($item_id, $user_id, $table)
      {
         //Определяем голосовал ли такой то пользователь за такой то итем
      }
    }


    * This source code was highlighted with Source Code Highlighter.


    Теперь вы можете добавить рейтинг к любой сущности в вашем проекте, но пример примером, а если немного отвлечься то по этой схеме можно организовать много чего и достаточно быстро и просто например: теги, комментарии к чему либо не было, да и вообще мало ли что взбредёт в голову.
    Если к этому добавить некоторые помощники вида которые будут помогать выводить вам одно и то же несколько раз задача облегчается ещё в несколько раз.

    И это только малая толика того как можно облегчить свой труд, хотелось бы услышать ещё различные варианты решения для различных проблем.

    P.S: сорри за отвратные картинки.

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 36

    • UFO just landed and posted this here
      • UFO just landed and posted this here
          0
          я тоже сначала подумал о наследовании.
            0
            в коде наверное наследование будет неплохим вариантом, однако в структуре базы данных это будет одна таблица рейтинга — на каждую сущность.

            а автор предлагает использовать одну таблицу для всех сущностей, но указывать в этой таблице тип сущности для которой делается рейтинг.

            однако на месте автора я бы привел цифры по нагрузочному тестированию такого подхода к рейтингу.

            возможно быстрее будет выбирать данные из разных мелких таблиц чем из одной большой.
              0
              Всем понятно что мой вариант будет работать медленнее, но кэширование может исправить сютуацию.
                0
                кэширование всегда исправляет ситуацию… но когда (если) столкнетесь с реальной нагрузкой — вы ещё и шардить начнёте рейтинги одной сущности по разным таблицам… потому что лучше 5 таблиц с индексом по одному полю и по 20 млн. записей, чем 1 таблица с индексом по двум полям и на 100 млн.
          +1
          Неужели, полиморфные модели?
            0
            труд можно СИИИИЛЬНО облегчить не плодя лишних сущностей
              0
              И снова бы злесь Капитан Боян!@nike-17
              ru.wikipedia.org/wiki/%D0%91%D1%80%D0%B8%D1%82%D0%B2%D0%B0_%D0%9E%D0%BA%D0%BA%D0%B0%D0%BC%D0%B0
              0
              Кстати таблицу Post и Comment волне можно и соединить (в одном из внутрикорпоротивных движков блого-форума я так и сделал).
              единая таблица:
              Post:
              id
              parent_id
              user_id


              если parent_id — 0/null — то это пост/начало форума, инача коммент, ответ.
              Я не говорю что это правильно и так нужно делать, просто ради прикола в одном небольшом проекте я такое забацал.
                0
                Ну часто бывает так, что у поста и комментария разные параметры могут быть, а так да вполне интересный вариант.
                • UFO just landed and posted this here
                    0
                    Неужели, однотабличное наследование? =)
                • UFO just landed and posted this here
                  0
                  Информацию о рейтинге я бы однозначно закешировал в соответствующем поле у сущности. А делать выборку из таблицы Rating следовало бы только при голосовании (чтобы исключить повторные)
                    0
                    Да я так и делаю тем более что дописать кэширование в этот код достаточно просто.
                    0
                    как в рельсах =)
                      +1
                      Походит на ContentTypes-фреймворк в джанге. Такие связи там называются generic relations. Только там все это зашло далеко вперед, с достаточно полной поддержкой со стороны ORM. Очень широко применяется при написании подключаемых приложений.
                        0
                        Gibbzy пишет: «Как известно в работе вёб программиста часто нужно делать что то достаточно быстро, для этого придумали множество всяческих инструментов ООП, фреймворки и много всего, но как не порождать монстров, а делать всё быстро и качественно?»

                        Просто знать и использовать правильные фреймворки. Хорошо разобраться в фреймворке сложнее, чем написать велосипед. Но, как мне кажется, полезнее.

                        И в джанге, и в рельсах (как пишет LightAlloy) это все давно реализовано и широко применяется (на рельсах не писал, но, думаю, что там тоже), с разными удобными плюшками и много-раз-наступленными граблями.

                        P.S. Этот конкретный велосипед считаю полезным, ход мыслей — верным.
                          +1
                          в целом решение имеет место быть, но:
                          1) когда сервис с высокой активностью, общая таблица будет излишне переполнена.
                          2) раз уж зашел разговор про Zend Framework… Не место этим методам в этом классе — мы не рейтингу рейтинг выставляем и иже с ним.
                          Следовательно нужены:
                          — абстрактный класс от которого будут наследоваться классы конкретных таблиц
                          abstract class VotingTable  extends Zend_Db_Table_Abstract {
                              protected $_rowClass = 'Voting';
                          
                            /**
                            * @return Zend_Db_Select
                            */
                            abstract function getTableType();
                          }
                          

                          — каждый класс наследующий VotingTable должен содержать определение зависимостей через $_dependentTables и реализовывать специфичный select (определение метода getTableType) в которым бы выборка производилась с учетом типа таблицы.
                          — Класс Rating должен содержать правила взаимосвязей с таблицами по которым ведется голосование ($_referenceMap)
                          — класс Voting extends Zend_Db_Table_Row_Abstract, содержащий методы работы с рейтингом с немного иной сигнатурой, которые в свою очередь будут проксировать вызов магических методов ($row->findTableClassViaIntersectionTableClassByRule1) родного механизма выборки многие ко многим [http://framework.zend.com/manual/en/zend.db.table.relationships.html#zend.db.table.relationships.fetching.many-to-many] и и создавать новые инстанции Rating (методом setRating).
                          class Voting extends Zend_Db_Table_Row_Abstract {
                            protected $_tableType;
                          
                            public function init() {
                               $this->_tableType = $this->getTable()->getTableType();
                            }
                          
                            public function setRating($user, $amount)  { /* ... */ }
                            public function getRating() { /* ... */ }
                            public function isVoted($user_id) { /* ... */ }
                          
                          }
                          


                          Как-то так…
                            0
                            «1) когда сервис с высокой активностью, общая таблица будет излишне переполнена.»
                            в чём кардинальная разница, 3 таблицы с N записями суммарно или 1 таблица с N записями?
                            • UFO just landed and posted this here
                              • UFO just landed and posted this here
                                  0
                                  а вы сможете эту разницу измерить? вы уверены, что вы сейчас не экономите на списках?
                                  озвучьте, пожалуйста, абсолютную разницу в секундах между этими 2 вариантами?

                                  пусть, к примеру, в каждой из таблиц будет миллионов по 50 записей.
                                  заполните и выложите результаты. потом сделайте вывод о целесообразности.
                                  0
                                  Ну и ещё дополнительное условие поиска WHERE field_type=? тоже немного замедляет…
                                    0
                                    вы можете это «немного» измерить?
                                    уточняю: не могли бы вы вместо аморфного и жутко сферического «немного» указать цифры, в секундах, насколько 1 таблица будет медленнее?
                                  0
                                  Прикаждом изменении таблицы придется также индексы пересчитывать — в маленькой таблице быстрее.
                                  При большом объеме вставок будет лочиться вся таблица и больше запросов будут висеть в оидании разлока — несколько таблиц выгоднее.
                                  любой join с бОльшей таблице будет медленнее чем с маленькой.

                                  В общем — с точки зрения удобства программирования — здорово, с точки зрения производительности — нет, imo.
                                    0
                                    уже дважды попросил товарищей сверху измерить эти сферические «быстрее» и «медленнее».
                                    попрошу и вас. можете показать, насколько это будет «медленнее»? будет ли стоить овчинка выделки?
                                    0
                                    пока я пребывал в царстве морфея без меня уже все рассказали :) но дело не только в скорости, а и в объеме. простой пример: одно поле int в таблице с 50000000. а плюс к этому по нему еще *обязательно* нужно будет создавать индекс. Диски и память хоть вещь и резиновая, но все нужно расходовать с умом :)
                                      0
                                      если у вас проблемы с тем, что индекс не помещается в память — то не нужно этим бравировать вынужденную денормализацию в схеме и предметной области. просто возьмите FEDERATED и пусть этой работой занимается mysql.
                                        0
                                        у меня вообще нет проблем :)
                                        а если по тексту:
                                        1) slovari.yandex.ru/dict/mikhelson/article/mi11/mi1-0617.htm — как следствие «вынужденной денормализацией»
                                        2) ru.wikipedia.org/wiki/%D0%9D%D0%BE%D1%80%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F_%D1%84%D0%BE%D1%80%D0%BC%D0%B0. А теперь вопрос: какая именно форма нарушена разделением одной огромной таблицы на несколько помельче?
                                        3) К вопросу об помериться скоростью выборки — предложение сделать кубического коня в вакууме, прошу прощения, и не имеет с реальной действительностью практически ничего общего. в реальности одновременно с выборкой конкретно в данную таблицу будет вестись вставка (хоть и с несколько меньшей интенсивностью), кешировать данные этой таблицы можно, но осторожно и не на долго. В контексте ORM к этой таблице будет десяток последовательных обращений. И при этом это будет делаться ну хотя бы раз 200-300 одновременно например в течении часа. наблюдения и опыт показывают, что разница может быть в пару порядков. да и почему сразу разговор про MySQL? Но если упираться в него, то есть еще как минимум шардинг.
                                        4) ну и под конец читаем *внимательно* последнее предложение ;)
                                  +1
                                  В нескольких проектах поступал так же, при записи\удалении в таблицу рейтинга, триггер обновлял поле рейтинг для конкретной сущности (что бы НЕ суммировать рейтинг при каждой выборке сущности). Пока полет нормальный!
                                    0
                                    Есть достаточно наивный вариант, чтобы не хранить информацию о типе записи в каждой таблице.

                                    Создадим таблицу Entity (сущности), в которой будем хранить информацию о всех идентификаторах и типах. Дополнительно создадим таблицу EntityType для хранения данных о самом типе (например, имени типа 'name'). Структуру табиц буду записывать в формате: имя_таблицы: (список полей), PK(поле первичного ключа), FK (внешний ключ)

                                    Entity: (id:autoincrement, type:int) , PK(id), FK(type -> EntityType.id)
                                    EntityType: (id:int, name:string), PK(id), FK(id -> Entity.id)

                                    Поле EntityType.id не генерирует значения идентификатора, а ссылается на Entity.id. Т.е. тип это тоже какая-то сущность.

                                    Аналогично — пользователи это тоже сущности.
                                    User: (id:int, firstname:string)
                                    , PK(id), FK(id -> Entity.id)

                                    Пользователем, который запостил статью, может быть не любая абстрактная сущность, а лишь некто из таблицы User:
                                    Article: (id:int, user:int)
                                    , PK(id), FK(id -> Entity.id), FK(user -> User.id)

                                    Продолжим:
                                    Comment: (id:int, user:int, item:int)
                                    , PK(id), FK(id -> Entity.id), FK(user -> User.id), FK(item -> Entity.id)
                                    Vote: (id:int, user:int, item:int)
                                    , PK(id), FK(id -> Entity.id), FK(user -> User.id), FK(item -> Entity.id)

                                    В таблице Comment поле item ссылается на абстрактный Entity.id. Поэтому комментировать можно любые сущности: статьи, пользователей, типы,…, какие-то другие сущности, которые появятся в будущем. Аналогично для Vote.

                                    В приципе, на одну и ту же сущность могут ссылаться id из разных таблиц. Для хранения этих данных создадим таблицу Table, где в поле name будем хранить имя табицы. А для связи таблицы таблиц с таблицей сущностей используем отношение многие-к-многим через таблицу Entity2Table:
                                    Entity2Table: (entity:int, class:int)
                                    , PK(entity, class), FK(entity -> Entity.id, ), FK(class -> Table.id)
                                    Table: (int:int, name:string)
                                    , PK(id), FK(id -> Entity.id)

                                    Таким образом для записи инфомации о новой сущности (допустим User) придется делать следующие вставки: в таблицу Entity, в таблицу User и в таблицу Entity2Table для связи вновь созданного юзера и таблицы User.

                                    Такая структура отображается на соответствующие понятия ООП: Entity => Object, Table => Class, EntityType => DataType, Entity2Table => extends (classes).

                                    зы: сори за сумбурность. это скорее мемори дамп.
                                      0
                                      ну вот и напутал. конечно же связывать нужно не сущности с таблицами, а их типы с таблицами. т.е. Entity2Table это бред. читать:
                                      EntityType2Table: (entity_type:int, table:int)
                                      , PK(entity_type, table), FK(entity_type -> EntityType.id, ), FK(table -> Table.id)
                                      Table: (int:int, name:string)
                                      , PK(id), FK(id -> Entity.id)

                                      а extends из ООП extends это EntityType2Table.

                                      В этом случае, для в ставки юзера потребуется лишь два запроса: в Entity и в User.
                                      0
                                      funca, есть реляционные модели, а то что вы предложили более походит на объектную модель
                                      а, автору да, пять, несомненное открытие америки
                                        0
                                        И не лень же было рисовать схемки в графическом редакторе (явно не предназначенном для составления блок-схем судя по линиям и прочим деталям)

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