Сучасна диджитал-освіта для дітей — безоплатне заняття в GoITeens ×
Mazda CX 30
×

Реализация Data Mapper

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Добрый день. Реализую паттерн Data Mapper в своем приложении. Также использую Identity Map. Непонятно как правильно («правильно» наверное не существует, значит как лучше) реализовать создание зависимых объектов.

Ссылка на полное описание проблемы toster.ru/q/142779

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

>> 1) Тут же вопрос, правильно ли то, что UserMapper точно знает какие нужны поля для передачи их в FileMapper.

Вообще говоря, нет. Если у вас $data формируется по результату запроса типа «SELECT * FROM user LEFT JOIN file ON user.avatar_id = file.id», то методу $fileMapper->createObject нужно передавать просто изначальные data, а уж он должен выгрести что ему нужно. А вообще, по хорошему, $data не должен быть плоским массивом содержащим данные разных объектов. Он должен формироваться согласно структуре БД, то есть что-то вроде

$data = [
  'id' => 0,
  'first_name' => '',
  'last_name' => '',
  'email' => '',
  'avatar_id' => 0,
  'avatar' = [
    'id' => 0,
    'path' => '',
    'size' => 0,
    'title' => '',
    'type' => '', 
  ],
]
Либо, можно обойтись без явной трансляции плоского массива от БД в древовидный, но разделение результатов запросы на массивы инициализирующих данных объектов должно происходить на уровне Repository. То есть схема примерно такая:
1. Клиент ORM (контроллер, например) вызывает метод типа UserRepository::getUsersWIthEmail()
2. Метод формирует и выполняет запрос SELECT * FROM user LEFT JOIN file ON user.avatar_id = file.id WHERE user.email IS NOT NULL
3. Метод разбирает результаты (плоскую таблицу) запроса в цикле по строкам:
3.1 Cначала для user.id проверяет в IdentityMap наличие этого user.id, если есть, то использует данные оттуда, игнорируя все остальные поля запроса (как вариант — обновляя объект в IdentityMap), если нет, то вызывает user.id UserMapper->doCreateObject с полями относящимися только к User ( или doCreateObject игнорирует неотносящиеся) и занесением созданного объекта в IdentityMap. Результат присваивается $user
3.2 Для каждого file.id аналогично. Результат присваивается в $file
3.3 Для $user с ненулевым avatar_id устанавливается $user->avatar в значение $file
3.4 $user добавляется в массив или иную коллекцию
4. Возвращается результирующий массив или коллекция

Для связей один ко многим (или многие к одному), аналогично, лишь на шаге 3.3 значение не устанавливается, а добавляется в массив или иную коллекцию. А для связи многие-ко-многим добавляются значения во встречные коллекции.

Чтобы избежать путаницы нужно строго разграничивать ответственности инфраструктурных классов (применительно к чтению здесь): *Mapper должен уметь создавать объекты из строки запроса (максимум формировать запрос для единичного объекта по его id), IdentityMap хранить связи id->объект, а *Repository формировать массовые запросы и вызывать
Mapper и IdentityMap. Так же нужно особое внимание уделять хранению связанных коллекций, строго следя, чтобы не было частичного их заполнения с фильтрацией до обращений, подразумевающих отсутствие фильтрации на стороне «многие». Самый простой способ (пускай и избыточный) — перед каждым обращением к коллекции заполнять её по новой согласно конкретному обращению. Другой метод: заполнять со стороны «многие» всегда всё, фильтруя уже на уровне объекта модели (плохо подходит когда в рамках одного действия происходит работа с «многие-ко-многим» только как «один-ко-многим», что чаще всего и бывает, скажем, нужны группы конкретного пользователя или пользователи, составляющие группу, значительно чаще чем полный список пользователей и групп со всеми связями — в случае PHP без использования разделяемой памяти это будет означать полную загрузку при каждом чихе).

Большое вам спасибо за столь полный ответ.

По факту:
1) Если попытаться отойти от Repository, то какой выход у меня есть?
— передавать полный объект data в FileMapper
либо
— преобразовывать на каком-то этапе массив в древовидную структуру (но тогда получается, что эта задача ложится на UserMapper, и он должен знать какие поля нужны для avatar, что не есть хорошо. Или есть другой вариант?).

2) Где можно посмотреть на хорошую реализацию Repository, приближенно к данному контексту? Т.е. чтобы реализовать архитектуру подобным образом, как вы описали?

1) Вообще надо понимать, что канонично вход у методов DataMapper (применительно к созданию объектов или их коллекций на основе SELECT-запросов к СУБД) не данные, а параметры (заданные явно и/или через имя метода) генерируемого внутри него запроса, а потому передача данных в FileMapper из UserMapper нарушает каноничность, создаёт больше одной ответственности у *Mapper. Вы пытаетесь использовать FileMapper максимум как общий (не Data) Mapper, а то и просто как фабрику. То есть у вас должен быть либо единый метод для генерации и маппинга запросов типа SELECT user.*, file.* FROM ... (поля в SELECT определяющие тип конкретного маппинга, FROM и т. д. — вторичны), знающий и как устроен запрос, и как устроены оба типа, User и File, либо два метода для маппинга отдельно User и отдельно File, общающиеся через параметры (например список avatar_id или запрос их выбирающий).

2) У Фаулера :)

Честно говоря не совсем понял. Если вас не затруднит, поясните пожалуйста:
— Что значит единый метод для генерациия и маппинга запросов? Можно подробнее?
— 2й вариант (2 метода для маппинга) подразумевает 2 запроса к БД вместо одного? Или я неверно понял?

И вопрос по реализации с Repository. Получается в моем случае даже метод find (который ищет сущность по Id) имеет смысл выносить в Repository (т.е. абсолютно все методы поиска в Repository)?

1) Что-то вроде (чисто датамаппер, код для иллюстрации на коленке)

public function getUsersWithAvatar() {
  $data = $this->db->getQueryResul ('SELECT <a href="http://user.id" target="_blank">user.id</a> user_id, user.first_name user_first_name, ..., user.avatar_id user_avatar_id, <a href="http://file.id" target="_blank">file.id</a> file_id, file.path file_path  FROM user LEFT JOIN file  ON user.avatar_id = <a href="http://file.id" target="_blank">file.id</a>');
  $users_with_avatars = [];
  foreach ($data AS $datum) {
    $user = (new User())->setId($datum['user_id'])->setFirstName($datum['user_first_name'])->...->setAvatarId($datum['user_avatar_id']);
    if (!is_null($user->getAvatarId())) {
      $avatar = (new File())->setId($datum['file_id'])->setPath($datum['path'])->...;
      $user->setAvatar($file);
    }
    $users[] = $user;
  }
}

2. Именно. Если решаем что один класс маппера маппит только один класс модели, то два запроса в таких случаях — норма (хорошо что не N+1 если подходить совсем тупо). Или обходим как-то, передавая куски массивов, например, мапя с них, а не с БД напрямую. И это меньшее зло чем когда два маппера знают об одном классе. А если, скажем, у вас поле creator_id есть в каждой таблице (например, для ACL), то в каждом маппере нужно не забыть заджойнить юзеров по нему, а при изменении класса или таблиці внести изменения повсюду.

DataMapper — решение для сложных систем, на простых его городить, особенно хардкодом, смысла обычно нет. А на сложных системах главная проблема — сложность кода, как ни странно, а не количество запросов (особенно учитывая, что два простых запроса могут выполниться гораздо быстрее чем один большой, особенно на MySQL, где возможности оптимизации средствами СУБД весьма скромные, например нет функциональных индексов)

Получается по большому счету, если все равно выполнять 2 запроса, то имеет смысл поле avatar тянуть вообще используя Lazy Load?

И я на всякий случай продублирую вопрос по Repository:

Получается в моем случае даже метод find (который ищет сущность по Id) имеет смысл выносить в Repository (т.е. абсолютно все методы поиска в Repository)?
Таким образом для получения сущности я буду использовать не Data Mapper, а Repository.
Т.е. вместо $userMapper->find(1) будет $userRepository->find(1), верно? А в итоге в методе find на сколько я понимаю будет что-то вроде того, что вы описали в ответе выше, только вместо создания объекта напрямую, это будете делегироваться соответствующему Mapper объекту.
Я правильно понимаю?

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

Однозначного ответа, думаю, нет. На начальных этапах нормально будет с подходом типа: на каждое сочетание полей в SELECT (читай — каждую «иерархию» агрегации) пишем метод поиска в маппере, аналогичный вышеуказанному, но потом придётся что-то делать с большим количеством однотипного кода. Возможно использовать «рефлексию», чтобы маппер конкретного класса модели мог «рассказать» агрегирующему его о том каких данных он ждёт из запроса, например, передавая ему кусок SQL.

На массовых выборках Lazy Load приведёт к 1+N запросу.

Вообще, да, все методы поиска в репозиторий выносятся, он формирует параметры и дергает маппер. Передавать параметры поиска мапперу или уже результаты запроса — особенности реализации, если прямое использование маппера не предполагается. Идеологически верно формировать в методах поиска репозитория какой-нибудь Criteria, и передавать его в DataMapper для генерации запроса.

Спасибо, для меня вы немного прояснили ситуацию. Пока как я понял лучшим решением является Repository, который сам создает и выполняет запрос, а потом по результатам запроса тот же Repository создает с помощью Mapper соответствующие объекты.

Почему не взять Doctrine 2 и не изобретать велосипед, или хотя бы глянуть на реализацию?

Как раз на данный момент нужно изобрести.

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

Я Вас не понимаю. Чем Doctrine ограничивает? И в чем универсальность? Классическая ORM. Джависты ведь не изобретают Hibernate.

Скажем, она плохо приспособлена для работы с наследуемыми, агрегируемыми и денормализованными таблицами, вообще для работы с маппингом отличным от «объект<->запись, класс<->таблица».

Но Mysql вообще не приспособлен для наследуемых и аггреруемых таблиц!

Вполне нормально работает. Тем более Doctrine много СУБД поддерживает.

Вполне нормально работает
Что-то не припоминаю INHERITS в mysql. А если Вы про велосипеды из stackoverflow, то тогда и Doctrine нормально поддерживает наследование.
Тем более Doctrine много СУБД поддерживает.
Автор спрашивал о Mysql на Тостере.

Я про классический маппинг иерархии классов на РСУБД.

Я в курсе. Но конструкции типа
/**
* @Entity
* @AssociationOverrides({
* @AssociationOverride(name="groups",
* joinTable=@JoinTable(
* name="users_admingroups",
* joinColumns=@JoinColumn(name="adminuser_id"),
* inverseJoinColumns=@JoinColumn(name="admingroup_id")
* )
* ),
* @AssociationOverride(name="address",
* joinColumns=@JoinColumn(
* name="adminaddress_id", referencedColumnName="id"
* )
* )
* })
*/
сложно назвать “хорошо приспособленные”

Приспособленные под что? Single / Joined table работают хорошо. А перезапись ассоциаций я использовал в production проектах 0 раз — слишком велик соблазн увлечься архитектурой ради архитектуры. Если нужен overskill, по-моему Андрей выбрал не ту базу и не тот язык. Пусть посмотрит на HIbernate 4 и Oracle 11g. А Вы только усугубляете дичь данного велосипеда.

Подобный подход не велосипед. Велосипедом он был бы если бы обсуждали построение своей универсальной ОРМ-библиотеки.

А паттерны для чего придумали? Чтобы их создавать снова и снова?

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

как лучше реализовать в конкретных условиях
Интересно, каких? Вы знаете стек технологий, предметную область, суть задачи?

А мне не надо это знать — ТС уже пришёл к выводу, что этот паттерн ему нужно реализовать. Какая разница какие предпосылки у него были?

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

На данный момент я занимаюсь разработкой некоммерческого проекта, в котором действительно можно было бы обойтись использованием той же Doctrine. Но! На данный момент я в деталях изучаю подходы к разработке приложений, т.е. я использую Doctrine в своих проектах, понимаю как она работает, но не в деталях, не до мелочей + также знаю, что часто универсальное решение оказывается хуже частного. Поэтому любые советы сейчас пойдут на пользу. Никто не говорит, что я принимаю их как истину в последней инстанции, я прекрасно понимаю, что одну и ту же задачу, можно реализовать различными способами. На данный момент меня просто интересуют детали реализации того или иного решения, которое мне пока жизненно не необходимо, но может понадобиться в будущем. И что плохого в том, что мне подсказывают в какую сторону смотреть? Даже если направление по итогу окажется неверным, я думаю и этот опыт будет полезен.

А в чем проблема? в PHP объекты — ссылочный тип, так что хранить в объектах ссылку друг на друга не проблема. Главное — создавать инфу об объектах в идентити мапе до того, как начнете тянуть зависимости

Если перейдете по ссылке — увидите, я подробно там описал проблему.

Підписатись на коментарі