====== 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**. Именно поэтому нам пришлось так сделать.