====== DataMappers - Мапперы ====== ===== Краткое описание ===== **Мапперы** используются для записи данных из объектов в базу данных (persistence layer) и, наоборот, из persistence layer в объекты. Основное назначение мапперов - избавить объекты от знания информации о том, как и где они хранятся. Мапперы реализуют простейший интерфейс маппера: * **save()** (состоит из **insert()** и **update()**) - для сохранения объекта * **delete()** - для удаления объекта * **mapRecordToObject($dataspace, $object)** - для загрузки объекта из данных, хранящихся в контейнере данных $dataspace. Начиная с версии alpha3 мапперы также реализуют интерфейсы finder-ов. Чаще всего это метод **findById($id)**, однако в некоторых случаях подобных методов может быть достаточно много. В ядро системы входят мапперы, которые работают с базой данных в качестве persistence layer-а, однако ничего не мешает сделать маппер, который будет, например, хранить объекты в файле, сессии или куках. Для получения мапперов клиенты могут использовать метод **lmbBaseToolkit :: createDataMapper($name)**. Например, так: $toolkit = Limb :: toolkit(); $mapper = $toolkit->createDataMapper('ArticleMapper'); Для поиска мапперов по умолчанию используется **DataMapperFileResolver**, который ищет мапперы в папке LPKG_CORE_DIR/core/orm/data_mappers/ или /data_mappers/ в пакетах. ===== Диаграмма классов ===== Данная диаграмма показывает несколько классов мапперов, которые входят в ядро Limb. Стоит также отметить, что, роль интерфейса у нас выполняет класс lmbAbstractDataMapper, который на диаграмме не показан. {{{ limb:architecture:limb3.0_data_mappers.png }}} ===== Комментарии и краткое описание классов на диаграмме ===== ^Маппер^Описание^ |**TreeNodeMapper**|Работает с объектом класса lmbTreeNode (элемент дерева). Использует объект с интерфейсом lmbTree для записи данных в дерево и обратно.| |**lmbOneTableObjectMapper**|Записывает информацию об объектах, чьи атрибуты хранятся в одной записи одной таблицы. Получает название класса lmbDbTable, который записывает и считывает данные из этой таблицы.| |**VersionedObjectMapper**|Может использоваться для управления процессом создания версий объектов. Он реализует паттерн //Decorator//, в качестве делегируемого маппера можно ему передать любой маппер, в том числе и сложный (композитный). **VersionedObjectMapper** при вызове метода **update** будет вызывать у делегируемого маппера метод **insert**. Таким образом создаются версии (копии) объекта, и в будущем можно будет получить все версии одного объекта. В реальном приложении данный маппер ни разу не использовался, так что ничего более конкретного мы сказать не можем.| Остальные мапперы обычно пишутся для конкретных приложений, так как маппер обычно очень сильно привязан к определенной структуре базы данных, а мы хотим сделать Limb по-настоящему абстрактной библиотекой. ===== Пример маппера доменного объекта, имеющего другие доменные объекты в качестве атрибутов ===== Приведем пример маппера доменного объекта класса **Recipe**. Recipe имеет атрибут **$recipe_items** - коллекцию ингредиентов, а также атрибут **$dish** (посуда, на котором все это хозяйство подается)- объект класса **Dish**. Пусть посуда имеет отношение 1..N с рецептами, то есть одна посуда может быть использована в нескольких рецептах. Путь между Recipe и Ingredient есть отношение N..M и оно моделируется через объект класса RecipeItem (например, с аттрибутами $amount, $time, $order). Класс Recipe показан на [[.domain_objects|странице документации по доменным объектам]]. class RecipeMapper extends lmbOneTableObjectMapper { function _defineClassName() { return 'Recipe'; } function _doMapRecordToObject($ds, $object, $prefix) { $object = parent :: _doMapRecordToObject($ds, $object, $prefix); if(!$this->_isMapped($object->getDish()) && ($dish_id = $object->get('dish_id'))) { $dish = $this->_generateProxyFor('Dish', $dish_id); $object->setDish($dish); } if(!$this->_isMapped($object->getRecipeItems())) { $dao = new RecipeItemsDAO($object->getId()); $mapper = $this->_createMapperFor('RecipeItem'); $proxy = new lmbPersistentObjectProxyCollection($dao, $mapper, new RecipeItem()); $object->setRecipeItems($proxy); } return $object; } function save($object) { if($dish = $object->getDish()) $object->set('dish_id', $dish->getId()); $this->_markAsDeletedRemovedCollectionItems($object->getRecipeItems()); parent :: save($object); } function delete($object) { foreach($object->getRecipeItems() as $recipe_item) $this->uow->delete($recipe); parent :: delete($object); } } Прокомментируем код. class RecipeMapper extends lmbOneTableObjectMapper { [...] } Маппер RecipeMapper наследуется от lmbOneTableObjectMapper. lmbOneTableObjectMapper является маппером, которых позволяет хранить данные по доменному объекту в одной таблице базы данных. lmbOneTableObjectMapper для работы с таблицей базы данных использует lmbTableGateway или один из его потомков. В нашем случае маппер будет работать с таблицей recipe через класс RecipeDbTable. Большинство доменных объектов являются одно-табличными, поэтому мы чаще всего будем наследовать мапперы доменных объектов от lmbOneTableObjectMapper. function _defineClassName() { return 'Recipe'; } Маппер должен знать, доменные объекты какого класса он загружает. Каждый маппер доменного объекта должен перекрывать данный метод. function _doMapRecordToObject($ds, $object, $prefix) { $object = parent :: _doMapRecordToObject($ds, $object, $prefix); [...] RecipeMapper наследуется от lmbOneTableObjectMapper, который в свою очередь наследуется от lmbAbstractDataMapper. lmbAbstractDataMapper имеет базовую реализацию метода mapRecordToObject, в которой производится проверка наличия кеша, регистрация загружаемого объекта в lmbUnitOfWork и некоторые другие операции. Дочерние классы lmbAbstractDataMapper должны в свою очередь перекрывать метод _doMapRecordToObject, что мы и сделали. И еще, **parent :: _doMapRecordToObject** в данном случая будет означать помещение в $object данные из $ds, которые соответсвуют полям таблицы recipe. if(!$this->_isMapped($object->getDish()) && ($dish_id = $object->get('dish_id'))) { $dish = $this->_generateProxyFor('Dish', $dish_id); $object->setDish($dish); } После загрузки в доменный объект Recipe данных из таблицы recipe необходимо загрузить остальные атрибуты. Однако в целях снижения нагрузки на базу данных, маппер не загружает реальный объект класса Dish, а создает proxy-объект класса lmbPersistentObjectProxy. **$this->_generateProxyFor('Dish', $dish_id);** - это метод класса lmbAbstractDataMapper, который позволяет сократить размер кода для создания такого прокси. Обратите внимание на проверку **$this->_isMapped($object->getDish())** - таким образом мы проверяем, не находится ли уже в загружаемом объекте необходимый атрибут. Такое может случиться, когда объекты загружаются целыми группами по результатам одного запроса. if(!$this->_isMapped($object->getRecipeItems())) { $dao = new RecipeItemsDAO($object->getId()); $mapper = $this->_createMapperFor('RecipeItem'); $proxy = new lmbPersistentObjectProxyCollection($dao, $mapper, new RecipeItem()); $object->setRecipeItems($proxy); } То же самое для атрибута $recipe_items. Для создания $proxy-объекта для атрибута $recipe_items создается объект класса **lmbPersistentObjectProxyCollection** и в его конструктор передается **$dao**, который позволит загрузить данные по реальным объектам, **$mapper**, которых из этих данных сможет сделать реальные объекты и пустой объект класса **RecipeItem**, которыми будет наполнена коллекция **$recipe_items**. function save($object) { if($dish = $object->getDish()) $object->set('dish_id', $dish->getId()); $this->_markAsDeletedRemovedCollectionItems($object->getRecipeItems()); parent :: save($object); } При сохранении рецепта маппер проверяет, изменилась ли коллекция $recipe_items и помечает в lmbUnitOfWork удаленные объекты класса RecipeItem. Для того, чтобы у нас хранилась информация о удаленных элементах коллекции $recipe_items, эта коллекция должна быть объектом класса **lmbPersistentObjectCollection**. Обычно об этом не нужно беспокоиться, так как **lmbPersistentObjectProxyCollection** создает именно такую коллекцию, когда $proxy заменяется реальным объектом. function delete($object) { foreach($object->getRecipeItems() as $recipe_item) $this->uow->delete($recipe); parent :: delete($object); } Так как Recipe и Ingredient связаны отношением много-ко-многим при удалении рецепта нам необходимо удалять все RecipeItems связанные с удаляемым рецептом. Для этого мы помечаем все элементы коллекции на удаление в lmbUnitOfWork. ===== Пример маппера, которых хранит данные в cookie ===== Теперь можно рассмотреть небольшой пример маппера, использующего COOKIE для хранения информации. Данный маппер может быть использован, например, для создания корзины товаров электронного магазина. class CartMapper extends lmbAbstractDataMapper { function _defineClassName() { return 'Cart'; } function _doMapRecordToObject($ds, $object, $prefix) { if(!$object->getItems()->isChanged()) { $ids = $this->_getIds(); $dao = new CartItemsDAO($ids); $mapper = $this->_createMapperFor('ShopItem'); $proxy = new lmbPersistentObjectProxyCollection($dao, $mapper, new ShopItem()); $object->setItems($proxy); } return $object; } function save($object) { $ids = array(); foreach($object->getItems() as $shop_item) $ids[] = $shop_item->getId(); $response = Limb :: toolkit()->getResponse(); if(sizeof($ids)) $response->setCookie('CartItems', implode(',', $ids)); else $response->setCookie('CartItems', ''); } function _getIds() { $request = Limb :: toolkit()->getRequest(); $cookie = $request->getCookie('CartItems'); if($cookie == null || $cookie == 'deleted') return array(); $ids = explode(',', $cookie); foreach($ids as $key => $id) $ids[$key] = (int)$id; return $ids; } function findCurrent() { $cart = $this->toolkit->createObject($this->_defineClassName()); $ds = new Dataspace(); $ds->set('id', $cart->getId()); return $this->mapRecordToObject($ds, $cart); } } Что здесь интересного: * Может показаться непонятным, зачем в методе **findCurrent()** нужно было создавать пустой **Dataspace** и помещать в него идентификатор вновь созданного объекта $cart. Дело в том, что lmbAbstractDataMapper требует, чтобы $ds, который приходит в метод **mapRecordToObject** содержал идентификатор доменного объекта. Затем именно по этому идентификатору объект будет зарегистрировать в **lmbUnitOfWork**. Именно поэтому нам пришлось так сделать.