Архитектура LS:: Мапперы

Представляю первую статью цикла статей об архитектуре livestreet, я думаю будут и продолжения. Исходные коды примера, приведенные здесь доступны на github.

1. Что такое маппер
Ну-с начнем, и начнем с того, что определим, что такое маппер? Маппер, или конкретнее Data Mapper является одним из стандартных паттернов проектирования программных средств, определенный в LS как PHP-объект наследуемый от базового, для всех мапперов класса Mapper. Основным назначением маппера является отделение логики хранения данных от бизнес-логики самого приложения. Маппер содержит методы, реализующие операции CRUD (Create-Read-Update-Delete) над одной или несколькими таблицами по «просьбе» объекта. Маппер является интерфейсом между объектами и базой данных, средством, повышающим уровень абстракции в архитектуре приложения.

Для чего нужен маппер? Прежде чем определим его необходимость и функции, рассмотрим следующие два положения:
  1. Все данные, используемые LS, хранятся в таблицах базы данных, причем в реляционных таблицах, а движок LS написан с использованием объектно-ориентированного подхода.
  2. Для хранения данных используется MySQL, хотя на самом деле способ хранения данных не должен влиять на работу CMS, не должен определять ее поведение. Объекты, определяющие функционал CMS должны иметь возможность получать необходимые данные вне зависимости от способа организации хранения данных.
Если обратиться к теории баз данных, и применить предложенную ANSI/SPARC обобщенную трёхуровневую модель архитектуры СУБД на архитектуру LS, то объект, построенный по паттерну Data Mapper, реализует ее, этой модели, концептуальный уровень.

Концептуальный уровень предназначен для единого (одинакового) внешнего вида базы данных, общего для всех используемых ее объектов. То есть любой объект LS может напрямую обратиться к базе данных, используя средства PHP с помощью языка SQL – это не сложно, но неправильно потому, что:
  1. Объект имеет прямой доступ к БД минуя типовые механизмы CMS по взаимодействию с БД, а мапперы и реализуют этот механизм. Следовательно, увеличивается сложность сопровождения такой разработки, поскольку требуется иметь в виду и типовые механизмы и такую «авторскую реализацию».
  2. Базовый класс маппера содержит проверку операций с данными на ошибки, а прямой доступ к данным приведет к дублированию кода проверки или к сложностям с отладкой.
  3. При отсутствии маппера приходиться учитывать особенности реализации базы данных в своих разработках, а не только структуру имеющихся данных, что приводит к дополнительным сложностям в разработке.
  4. При изменении способа организации БД нужно будет менять логику работы дополнения, в случае использования маппера изменению подлежит только он. Следовательно, уменьшается сложность модернизации программного средства.
Резюме: Использование мапперов приводит к повышению уровня абстракции в архитектуре LS, делая возможным модулям движка и плагинов не обращаться к БД напрямую а указывать мапперам на необходимость получения/изменения данных, не вникая в сложности их хранения и обработки.

2. Реализация маппера в LS

Маппер реализован как часть модуля и физически располагается в папке модуля в подкаталоге mapper. Файл маппера имеет имя: [ИмяМодуля].mapper.class.php. Каждый модуль в LS имеет максимум один маппер. Сам класс маппера реализован как потомок класса Mapper – родительского для всех мапперов. Имя класса маппера строиться по следующему принципу:
Module[ИмяМодуля]_Mapper[ИмяМаппера]
Или, если в составе плагина
Plugin[ИмяПлагина]_Module[ИмяМодуля]_Mapper[ИмяМаппера]

Рассмотрим процесс создания маппера на следующем примере: Пусть есть таблица, хранящая сведения о ранге пользователя, то есть его месте в общем рейтинге пользователей, имеющая следующую структуру:

Путь для реализации функционала по работе с рангом пользователя создан модуль Rang. В таком случае мы будем использовать файл маппера с именем: Rang.mapper.class.php, расположенный в каталоге classes/rang/mapper/. Сам файл будет иметь следующее содержание:

Или, если модуль реализован в составе плагина, то такое:

Если Ваш модуль работает с таблицами БД, то для него должен быть реализован соответствующий маппер.

3. Функционал маппера: реализация операций CRUD
Весь функционал по работе с базой данных реализует объект класса DbSimple_Generic_Database (http://en.dklab.ru), реализованный в файле: lib/external/dbSimple/Generic.php. DbSimple поддерживает следующие СУБД: MySQL, PostgreSQL и InterBase/FireBird, то есть реализация маппера через данный объект позволит без особых сложностей сменить СУБД.
Рассмотрим пример типовых операций CRUD, реализуемых в классе маппера:

3.1. Добавление записи:

Для успешного выполнения запроса в конфигурационном файле общем, или файле config.php плагина должно быть указано имя таблицы, в этом примере так:

Метод $this->oDb->query($sql, $iUserId, $iUserRang) получает первым параметром строку SQL-запроса, остальными параметрами – параметры запроса, в его тексте указанные как ?, в том порядке, в котором эти ? и следуют.

3.2. Чтение записи

Чтение данных реализуется через метод $this->oDb->selectRow($sql, $iUserId), который также получает первым параметром строку SQL-запроса, остальными параметрами – параметры запроса. Метод возвращает массив, содержащий первую строку результата запроса.

3.3. Редактирование записи

Принцип тот же: формируем строку запроса, отправляем ее с помощью метода query.

3.4. Удаление записи


4. Использование маппера
Мапперы используются в модулях с помощью свойства модуля oMapper, представляющий собой объект класса маппера, определенного для этого модуля. Это свойство отсутствует у родительского объекта, поэтому его необходимо создать и инициализировать. На картинке ниже в методе инициализации модуля указан код создания маппера. То есть у объекта модуля, если предполагается использование маппера, он создается и определяется как свойство oMapper, которое доступно стандартным способом: $this->oMapper. То есть сам модуль не обращается к базе данных, он лишь запрашивает данные у маппера, дает ему указания выполнить определенные операции и ничего не знает о механизмах их исполнения. Эти механизмы инкапсулирует маппер, предоставляя модуль лишь интерфейс взаимодействия с БД.
Здесь остается открытым вопрос: можно ли определить несколько мапперов для одного объекта. В составе движка я такого кода не видел, но принципиально это возможно.
Ну и в качестве финального аккорда, в модуле Rang, для которого определен маппер, в методе его инициализации используем все созданные операции так:

34 комментария

avatar
Хорошая статья. Правда, прогер это и так знает, но для юзера много новых слов тоже будет полезно узнать. Хотя, например, буковки инкапсулирует маппер, мало что ему скажут :)
  • aex
  • +3
avatar
Абсолютно толковый комментарий. Статья очень исчерпывающая и это хорошо :)
avatar
Полностью согласен с aex. Не стоит забывать, что есть новички в программировании. И им будет полезна информация преподнесенная в таком виде.
avatar
вам рекомендую внимательнее изучить плейсолдеры.

? — для строковых величин.
для целых есть ?d
avatar
*плейсхолдеры
avatar
Я знаю про них. При написании статьи я не ставил перед собой задачу рассмотрения функционала DbSimple. Сам DbSimple в статье упомянут, ссылка на него есть.
avatar
я о том, что у вас же на скриншотах ошибка.
раз уж вы пишете статью для новичков (я ведь правильно понимаю), то следует и примеры все приводить точные.
avatar
Все примеры рабочие. Ошибка на одной картинке не так назван класс (Rang на одной и Ranger на другой). На последней картинке не вызван метод повторного получения данных, print_r выведет одинаковый результат. Подскажите, пожалуйста, где еще ошибка, что я пропустил?
avatar
в любом методе маппера вы подставляете целые числа как строковые в плейсхолдерах. вот к чему мой первый комментарий.
avatar
ну а в updateRecord() перед переменной два $$
avatar
Картинки придется переделывать(
avatar
Вопрос, на самом деле, принципиальный. Я считаю, что в моих примерах корректно срабатывает преобразование типов и ошибки не происходит, и это действительно так. Вопрос же в другом: допустим подход, который я использую — необязательное указание типа в плейсхолдере или нет?
И ответ: В родном маппере User.mapper.class.php в методе
public function CreateSession(ModuleUser_EntitySession $oSession) {
    $sql = "REPLACE INTO ".Config::Get('db.table.session')."
        SET
            session_key = ? ,
            user_id = ? ,
            session_ip_create = ? ,
            session_ip_last = ? ,
            session_date_create = ? ,
            session_date_last = ?
    ";
    return $this->oDb->query($sql,$oSession->getKey(), $oSession->getUserId(), $oSession->getIpCreate(), $oSession->getIpLast(), $oSession->getDateCreate(), $oSession->getDateLast());
}

Для поля user_id указан "?", а не "?d" — разработчики могут так делать, значит могу и я. Где я не прав?
avatar
Вы неправы только в том что комментарием выше показываете, что понятия не имеете что такое плейсхолдеры и для чего их используют.
разработчики могут так делать, значит могу и я
Своей головы нет?
avatar
порядок?.. мы же используем нотацию для названия переменных, хотя вполне можно их называть k,i,j,p etc.
разработчики могут так делать, значит могу и я.
«разработчики» — обширное понятие. ЛС дорабатывается (делаются пул реквесты) многими людьми в т.ч. и мною и вполне возможно кто-то не досмотрел и на автомате поставил строковой тип, но это вовсе не означает что следует использовать заведомо неверные способы/методы разработки, игнорируя правила написания кода. Если бы дело было настолько простым что можно было обойтись только ?, то тогда, наверное, не придумали бы других типов…
avatar
Я думаю, что этот порядок надо где-то зафиксировать, потому, что нет ничего хуже, чем негласные правила — они просто ведут к беспределу. В репозитоии DbSimple у Дмитрия Котерова в мануалах намисано:
Listing 15: One row fetching

$row = $DB->selectRow('SELECT * FROM ?_user WHERE user_id=?', $uid);
. А в его Listing 13 видно, что user_id — числовое поле. Ну если разработчики — это общее понятие, то автор DbSimple — конкретный человек. И на счет ПОРЯДКА: я полностью согласен с Вами, должно быть какое-то соглашение об использовании плэйсхолдеров именно в LS, но применительно к общим подходам прав, наверное, я.
avatar
прав, наверное, я.

1. строковый как у вас:
SELECT * FROM user WHERE user_id='5'

или

2. числовой как нужно:
SELECT * FROM user WHERE user_id=5
avatar
разработчики могут не доглядеть, но намеренно так делать не будут
если число, то нужно использовать ?d
avatar
Согласен, и с PSNet насчет Правил согласился, но только с моим положением о корректности использования плейсхолдера "?" для чисел никто не соглашается(. Почему-то даже примеры в readme автора DbSimple в расчет не беруться, а между тем этот плейсхолдер лишь экранирует спецсимволы и обрамляет значение кавычками, то есть представляет собой общий универсальный плейсхолдер, который позволяет вставлять даже бинарные данные, но конечно в виде строки и, к тому же, подходит для числа. Числовой плейсхолдер проверяет число на корректность — и в этом его особенность и использовать для чисел нужно, преемущественно его, но в моих примерах, я считаю, нет методологической ошибки.
Во всем этом диалоге мне никто не привел контраргументов об ошибочности используемого подхода и о его полной неприменимости. И я немогу понять: все говорят, что так делать нельзя, а почему нельзя — неизвестно. Поверьте, я не считаю этот диалог спором и не считаю свою точку зрения лучшей в нем (исправить в статье "?" на "?d" несложно) я хочу понять — нужно ли?
avatar
У меня два аргумента:
1. зачем отдавать на откуп БД преобразование типов, если это можно сделать в коде?
2. читаемость кода, видя запросы в маппер сразу понятно, что поле имеет числовой тип
avatar
У плейсхолдера ?d есть одна особенность, при ошибке преобразования в число, он возвращает 0. То есть в моем примере, если я напишу "?d" и вызову метод
$this->oMapper->CreateRecord("gfgg", 100);
, то ошибки не произойдет и запись добавится для 0-го пользователя, если же использовать "?", то при преобразовании типов в БД будет ошибка и запись не добавится. Мой пример корректен и покрывает Ваши аргументы. Дополнительные методы проверок лишь добавят строки кода.
avatar
То есть в моем примере, если я напишу "?d" и вызову метод
это уже работа корректного кода чтобы не допустить в запись строки, преобразование типов никто не отменял, причем на этапе получения данных, а не в конце, перед самой вставкой в БД ибо держать на всем пути обработки одни данные, а при вставке в бд «выровнять» их — глупо.
avatar
Вы, кажется, не замечаете мои аргументы и смысл того, что я хочу сказать, ну я приведу еще одни. Далее цитаты из официальной документации DbSimple. Плейсхолдеры "?" и "?a" относятся к группе основных плейсхолдеров, а "?d" и другие к группе дополнительных. И далее цитата:
Для удобства работы DbSimple поддерживает еще целый набор видов placeholder-ов, которые будут описаны далее. Они используются значительно реже и, как правило, позволяют просто сократить письмо. Если вы думаете, что все это слишком сложно для понимания, — не используйте дополнительные placeholder-ы.
.
То есть позволяют просто сократить письмо и не используйте дополнительные placeholder-ы. Я еще в самом начале написал, что В статье не ставилось целью рассказывать о работе DbSimple — поэтому используются основные плейсхолдеры, использование которых корректно согласно ОФИЦИАЛЬНОМУ МАНУАЛУ. Но и не буду кривить душой, там еще есть строки:
Может возникнуть вопрос, зачем нужны целочисленные placeholder-ы, если СУБД и так умеют преобразовывать строки в числа? Например, MySQL конвертирует '10' в 10 при вставке в числовое поле. Оказывается, это верно не для всех существующих в мире СУБД. Кроме того, предложение FIRST? SKIP? FireBird (или LIMIT ?,? MySQL) требует обязательной подстановки чисел, а не строк.
Но в моей статье я НЕ ИСПОЛЬЗУЮ этого. Я повторю еще раз:
Я полностью согласен с Вами, должно быть какое-то соглашение об использовании плэйсхолдеров именно в LS, но применительно к общим подходам прав, наверное, я.
.
Предлагаю закончить спор. Контраргументы к своей линии привел лишь ort и я сними согласился. Но и в моей статье плейсхолдеры используются правильно.
avatar
В последнем комментарии ошибка. преобразование типов идет одинаково и в БД и при использовании плейсхолдера "?d", за исключением чисел представленных в экспоненциальном виде (в "?d" неправильно), что на практике, думаю, неважно. Аргументы принимаю. Спасибо за конструктивный диалог.
avatar
Для поля user_id указан "?", а не "?d" — разработчики могут так делать, значит могу и я. Где я не прав?
Разработчики тоже люди. Увидели ошибку => fork repo, fix bug, commit, push, send pull request. И только так
avatar
Полезная статья, даже распечатал, буду перечитывать пока не пойму :)
Есть ли похожие статьи про архитектуру?
avatar
При архитектуру? Это тебе надо Корбюазье, Нимейера, Расстелли и прочих великих зодчих? :)
avatar
При архитектуру?

А тебе не мешало бы почитать классику: Толстого, Достевского, Чехова, ну или Пушкина на крайний случай :)

Ну а если серьезно, где взять подобные мануалы / литературу про мапперы и прочее?
avatar
Оно тебе надо, эти мапперы-шмапперы? :) Ну здесь почитай.
avatar
А вот здесь, можешь почитать о плейсхолдерах, которые выше обсуждают.
avatar
Это не те плейсхолдеры, те здесь.
avatar
Не важно, ему это для общего развития :)
avatar
А здесь о PHP, а здесь о Smarty. Думаю, на первое время, тебе этого хватит :)
avatar
Какая интересная дискуссия получилась. Прямо разбор диссертации соискателя на научном совете :) Как по-мне, с точки зрения рядового юзера, если все работает и ничего не глючит, то и фиг с ним, как оно там написано. Я вот кучу сайтов делал на простом PHP, без всяких Смарти, мапперов и разных там плейсхолдеров. И то, что в ЛС надо делать и мудрить в нескольких файлах, у меня помещалось в одном. И все прекрасно работает и по сей день, быстро и надежно. Ну это я так, к слову :)
  • aex
  • 0
avatar
Очень полезная статья для новичков, которые собираются разобраться и дорабатывать какие-либо возможности базового движка или писать плагины. Надо помнить, что есть определенные стандарты в разработке и их необходимо придерживаться. Стандарты дают возможность разрабатывать приложения с легко масштабируемым и переносимым кодом. Если все написано по определенному шаблону, то такой код легко обслуживать. Изучайте побольше современных доков, технологии развиваются очень стремительно. Иные просто не успевают за ними))).
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.