Содержание

DataMappers - Мапперы

Краткое описание

Мапперы используются для записи данных из объектов в базу данных (persistence layer) и, наоборот, из persistence layer в объекты. Основное назначение мапперов - избавить объекты от знания информации о том, как и где они хранятся.

Мапперы реализуют простейший интерфейс маппера:

Начиная с версии 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, который на диаграмме не показан.

}

Комментарии и краткое описание классов на диаграмме

МапперОписание
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 показан на странице документации по доменным объектам.

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 для хранения информации. Данный маппер может быть использован, например, для создания корзины товаров электронного магазина.

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);
  }
} 

Что здесь интересного: