Шаблон DataMapper

Данная статья является переводом части книги PHP|ARCHITECT'S GUIDE TO PHP DESIGN PATTERNS by Jason E. Sweat. Книгу вы можете прибрести здесь.

Две предыдущие части, шаблон Active Record и шаблон Table Data Gateway, показали способы абстрагировать запись в таблице и отдельную таблицу соотвественно. Хотя оба шаблона полезны, каждая реализация слишком сильно связана со структурой используемой базы данных, так что решения, использующие эти шаблоны, могут быть хрупкими. Например, если ваш код использует названия полей как ключи строк массива или как атрибуты в полях объектов, ваше приложение связано со структурой базы данных и вам, вероятно, придется сделать обширные изменения в коде на каждое (соответствующее) небольшое изменение в таблице. Из-за того что код и базы данных часто меняются в процессе разработки и развиваются после внедрения, есть достаточно много возможных преимуществ от максимального разделения кода доменных объектов и используемых баз данных, изолирующих друг друга от взаимозависимостей. К тому же такое разделение облегчает понимание кода для дальнейшего внесения изменений.

Проблема

Как вы можете уменьшить взаимосвязь между классами вашего приложения и базой данных? Например, как вы можете уменьшить переписывание кода, если одно или несколько полей в таблице сменили название?

Решение

Шаблон Data Mapper разделяет атрибуты объектов от полей таблиц, которые им соответствуют. Суть шаблона Data Mapper – это класс, который траслирует атрибуты и/или методы доменного объекта в поля таблицы базы данных и наоборот. Работа шаблона Data Mapper заключается в том, чтобы понимать оба представления информации и передавать её в обе стороны, создавая новые доменные объекты, основанные на информации из базы данных, и обновление или удаление информации в базе данных, используя информацию, полученную от доменных объектов. Связь между объектно-ориентированным кодом и таблицами базы данных может быть отображена в разных формах. Одна из возможностей - ручное кодирование связей в класе Data Mapper. Другой способ – массив, расположенный внутри класса. Класс также может получать информацию из других внешних источников, таких как ini-файлы или xml-файлы. Схема ниже показывает диаграмму класса шаблона Data Mapper применительно к задаче доменного объекта – сохранении закладок URL – использованной в предыдущих двух главах. На схеме объект Bookmark – доменный объект и BookmarkMapper – реализация шаблона Data Mapper. Bookmark должен содержать в себе бизнес-логику, такую как валидация URL. BookmarkMapper выступает как полноценный обменник между get- и set-методами объекта Вookmark и структурой таблицы ссылок. :ooad:dm_01.gif

Оба класса тесно связаны - BookmarkMapper действует как Фабрика для экземпляров Bookmark и принимает экземпляр Bookmark как параметр для многих операций BookmarkMapper.

Пример

Используя диаграмму UML как направление для действия, давайте разработаем два класса Bookmark и BookmarkMapper. Сначала, как упомянуто выше, необходимая некоторая конфигурация для обработки трансляции между столбцами таблицы и методами объекта. Давайте используем к качестве конфигурации файл в формате XML. Цель этой конфигурации перечислить поля таблицы bookmark и определить какие методы размещают и получают соответствующую информацию из объекта Bookmark. Достаточно простого формата, состоящего из кореневого элемента <bookmark> и ряда элементов <field>:

<field>
  <name>url</name>
  <accessor>getUrl</accessor>
  <mutator>setUrl</mutator>
</field>

Элемент <name> содержит текущее физическое название поля в таблице БД. Элемент <accessor> определяет метод для получения атрибута и не обязательный, как и некоторые поля, вроде timestamp, у которых нет необходимости быть транслируемыми. Элемент <mutator> содержит название метода Bookmark, для загрузки данных. (Также в этой конфигурации может быть определены и другие зависимости. Например, вы можете опеределить тип и размер каждого поля и использовать эту информацию для динамического формирования SQL, необходимого для создания таблиц. Это может быть интересно, если ваше приложение имеет некоторый установочный скрипт, написанный на PHP, и вы можете использовать эту конфигурацию для создания структуры таблиц. Вы также можете автоматически преобразовывать типы данных, когда устанавливаете атрибуты PHP-объекта.) Завершенный XML-файл должен выглядеть так:

<bookmark>
  <field>
    <name>id</name>
    <accessor>getId</accessor>
    <mutator>setId</mutator>
  </field>
  <field>
    <name>url</name>
    <accessor>getUrl</accessor>
    <mutator>setUrl</mutator>
  </field>
  <field>
    <name>name</name>
    <accessor>getName</accessor>
    <mutator>setName</mutator>
  </field>
  <field>
    <name>description</name>
    <accessor>getDesc</accessor>
    <mutator>setDesc</mutator>
  </field>
  <field>
    <name>tag</name>
    <accessor>getGroup</accessor>
    <mutator>setGroup</mutator>
  </field>
  <field>
    <name>created</name>
    <mutator>setCrtTime</mutator>
  </field>
  <field>
    <name>updated</name>
    <mutator>setModTime</mutator>
  </field>
</bookmark>

Мы можем использовать соответствующие возможности SimpleXML, поставляемого вместе с PHP5, для разбора этого файла. Все что вам нужно, это вызвать simplexml_load_file ('bookmark.xml') и вы имеете готовый композитный объект SimpleXMLElement со всей информацией. Результат выглядит так:

object(SimpleXMLElement)#21 (1) {
["field"]=>
array(7) {
[0]=>
  object(SimpleXMLElement)#15 (3) {
    ["name"]=>
      string(2) "id"
    ["accessor"]=>
      string(5) "getId"
    ["mutator"]=>
      string(5) "setId"
  }
[1]=>
  object(SimpleXMLElement)#19 (3) {
    ["name"]=>
      string(3) "url"
    ["accessor"]=>
      string(6) "getUrl"
    ["mutator"]=>
      string(6) "setUrl"
  }
  //...<snip>...
[4]=>
  object(SimpleXMLElement)#23 (3) {
    ["name"]=>
      string(3) "tag"
    ["accessor"]=>
      string(8) "getGroup"
    ["mutator"]=>
      string(8) "setGroup"
  }
  //...<snip>...
}

Так как XML-файл отражает взаимоотношение доменного объекта и базы данных, BookmarkMapper будет считывать этот файл во время создания объекта. Перед погружением в BookmarkMapper, давайте немного оглянемся в класс Bookmark. Учитывая то, что Bookmark используется в работающем проекте, будет лучше, если он изменится как можно меньше. Более того, Bookmark не должен измениться из-за внедрения BookmarkMapper. Этот шаблон предназначен для того, как можно меньше влиять на код. Доменный объект остается цельным, не обращая внимания на существование BookmarkMapper. Это вносит еще одно требование к реализации шаблона Data Mapper: т.к. доменный объект не знает о существовании Data Mapper, все исходные доменные объекты должны предоставлять публичный доступ ко всем требующимся атрибутам, для того чтобы Data Mapper мог инициализировать доменный объект во время создания и чтение атрибутов во время сохранения. У класса Bookmark все атрибуты имеют статус protected, но он предоставляет набор get- и set-методов, что удовлетворяет этому условию. Давайте начнем с написания кода для установления и получения атрибута «url» класса Bookmark:

class Bookmark {
  protected $url;
  // ...
  public function getUrl() {
    return $this->url;
  }
  public function setUrl($url) {
    $this->url = $url;
  }
}

Вы можете избежать монотонности написания большого количества get- и set-методов, используя рефлексию. Объект как-бы заглядывает в себя, и определяет может ли свойство иметь get- и set-методы или нет, и какие имена должны иметь эти методы. Начнем с тестов:

class BookmarkTestCase extends BaseTestCase {
  //...
  function testAccessorsAndMutators() {
    $bookmark = new Bookmark(false);
    $props = array('Url', 'Name', 'Desc',
      'Group', 'CrtTime', 'ModTime');
    foreach($props as $prop) {
      $getprop = "get$prop";
      $setprop = "set$prop";
      $this->assertNull($bookmark->$getprop());
 
      $val1 = 'some_val';
      $bookmark->$setprop($val1);
      $this->assertEqual($val1,
        $bookmark->$getprop());
 
      $val2 = 'other_val';
      $bookmark->$setprop($val2);
      $this->assertNotEqual($val1,
        $bookmark->$getprop());
      $this->assertEqual($val2,
        $bookmark->$getprop());
    }
  }
}

Для каждого атрибута Bookmark тест устанавливает значение используя изменяющий метод и проверяет метод доступа на то, что устанавливаемые и возвращаемы значения совпадают. Затем занчение изменяется еще раз, и снова проверяется. Этот код основан на соглашении, а не некоторой явной реализации. Методы установки и доступа к значению начинаются с set и get соответственно и имени атрибута за ними. Например, название метода доступа для атрибута «url» будет getUrl(); метод установки – setUrl() соответственно. Ниже код, реализующий динамические метода установки и доступа к атрибутам.

class Bookmark {
  protected $url;
  protected $name;
  protected $desc;
  protected $group;
  protected $crttime;
  protected $modtime;
  //...
  public function __call($name, $args) {
    if (preg_match('/^(get|set)(\w+)/', strtolower($name), $match)
    && $attribute = $this->validateAttribute($match[2])) {
      if ('get' == $match[1]) {
        return $this->$attribute;
      } else {
        $this->$attribute = $args[0];
      }
    }
  }
  protected function validateAttribute($name) {
    if (in_array(strtolower($name),
    array_keys(get_class_vars(get_class($this))))) {
      return strtolower($name);
    }
  }
}

Этот код основан на «магическом» методе __call() PHP5, который вызывается если происходит обращение к неопределенному методу класса. Метод __call() является Имя неопределенного метода передается первым параметром, а аргументы - вторым в ввиде массива.

Для вызова динамически-созданных get- и set-методов, имя вызванного метода проверяется на то, что оно начинается на get или set и содержит корректное название атрибута. Если проверка пропроходит успешно, атрибут изменяется или возвращается соотвественно. Этот подход освобождает от ручного кодирования методов getUrl() и setUrl(), таким образом что они могут быть убраны из кода. Есть один побочный эффект: этот код не выдает никаких сообщений об ошибке, если вызван неверный или несуществующий метод. Для предотвращения этого, можно давать сообщение об исключительной ситуации:

class Bookmark {
  //...
  public function __call($name, $args) {
    if (preg_match('/^(get|set)(\w+)/', strtolower($name), $match)
    && $attribute = $this->validateAttribute($match[2])) {
      if ('get' == $match[1]) {
        return $this->$attribute;
      } else {
        $this->$attribute = $args[0];
      }
    } else {
      throw new Exception(
      'Call to undefined method Bookmark::'.$name.'()');
    }
  }
}

Вы можете протестировать эту ситуацию:

class BookmarkTestCase extends BaseTestCase {
  //...
  function testBadGetSetExceptions() {
    $mapper = new BookmarkMapper($this->conn);
    $this->addSeveralBookmarks($mapper);
    $bookmark = $mapper->findById(1);
    try {
      $this->assertNull($bookmark->getFoo());
      $this->fail('no exception thrown');
    }
    catch (Exception $e) {
      $this->assertWantedPattern('/undefined.*getfoo/i',
      $e->getMessage());
    }
    try {
      $this->assertNull($bookmark->setFoo('bar'));
      $this->fail('no exception thrown');
    }
    catch (Exception $e) {
      $this->assertWantedPattern('/undefined.*setfoo/i',
      $e->getMessage());
    }
  }
}

Есть еще одно ограничение – атрибут $id не должен когда-либо быть изменным. Напишем тест для неизменяемого ID. setId() может быть вызвано однажды для установки ID, а все последующие вызовы метода getId() должны возвращать одно и тоже значение, без учета последующих вызовов setId().

class BookmarkTestCase extends BaseTestCase {
  //...
  function testUnsetIdIsNull() {
    $bookmark = new Bookmark;
    $this->assertNull($bookmark->getId());
  }
  function testIdOnlySetOnce() {
    $bookmark = new Bookmark;
    $id = 10; //just a random value we picked
    $bookmark->setId($id);
    $this->assertEqual($id, $bookmark->getId());
    $another_id = 20; // another random value, != $id
    //state the obvious
    $this->assertNotEqual($id, $another_id);
    $bookmark->setId($another_id);
    // still the old id
    $this->assertEqual($id, $bookmark->getId());
  }
}

Важно помнить, что методы, явно определенные в классе, всегда отменяют вызов __call(). Вы можете определить специфичное, отличное от обычного поведение для любого метода путем добавления метода в класс:

class Bookmark {
  protected $id;
  //...
  public function setId($id) {
    if (!$this->id) {
      $this->id = $id;
    }
  }
}

Пока всё что мы имеем, это простой объект с данными, давайте добавим некоторую доменную логику - в конце концов, один из поводов для реализации шаблона DataMapper – это отделение доменной логики от уровня хранения доменных данных. Следуя принципу «говори, а не спрашивай», добавим метод fetch() для того чтобы получить текущее (HTML) содержание страницы, на которую указывает закладка. Тест для этой функциональности:

class BookmarkTestCase extends BaseTestCase {
  //...
  function testFetch() {
    $bookmark = new Bookmark;
    $bookmark->setUrl('http://www.google.com/');
    $page = $bookmark->fetch();
    $this->assertWantedPattern(
    '~<input[^>]*name=q[^>]*>~im', $page);
  }
}

И примерная реализация:

class Bookmark {
  //...
  public function fetch() {
    return file_get_contents($this->url);
  }
}

Теперь класс целиком выглядит так:

class Bookmark {
  protected $id;
  protected $url;
  protected $name;
  protected $desc;
  protected $group;
  protected $crttime;
  protected $modtime;
  public function setId($id) {
    if (!$this->id) {
      $this->id = $id;
    }
  }
  public function __call($name, $args) {
    if (preg_match('/^(get|set)(\w+)/', strtolower($name), $match)
    && $attribute = $this->validateAttribute($match[2])) {
      if ('get' == $match[1]) {
        return $this->$attribute;
      } else {
        $this->$attribute = $args[0];
      }
    } else {
      throw new Exception(
      'Call to undefined method Bookmark::'.$name.'()');
    }
  }
  protected function validateAttribute($name) {
    if (in_array(strtolower($name),
    array_keys(get_class_vars(get_class($this))))) {
      return strtolower($name);
    }
  }
  public function fetch() {
    return file_get_contents($this->url);
  }
}

С имеющейся реализацией класса Bookmark, вернемся к классу BookmarkMapper. Основная работа BookmarkMapper заключается в получении данных из базы данных и создании объектов Bookmark. Первая задача – это создание с помощью BookmarkMapper новых записей в таблице БД. В шаблоне DataMapper, доменных объект не знает о существовании DataMapper, но содержит в себе всю бизнес-логику, включая возможные правила, относящиеся к созданию объекта. Логический путь создания записи, это создать новый экземпляр класса Bookmark, установить атрибуты и затем попросить BookmarkMapper сохранить созданный экземпляр. Давайте попробуем реализовать этот интерфейс. BookmarkMapper должен взаимодействовать с БД. Как и в предыдущих двух главах, давайте используем ADOdb как абтрактный уровень доступа к БД. Кроме того, давайте передавать соединение ADOdb в конструктор BookmarkMapper.

class BookmarkMapper {
  protected $conn;
  public function __construct($conn) {
    $this->conn = $conn;
  }
}

Также, BookmarkMapper должен считывать XML-файл, описанный ранее. Для того, чтобы сделать использование XML более удобным, сохраним привязки как хеш name ⇒ элемент simplexml для каждого поля в файле. Добавим код в конструктор:

class BookmarkMapper {
  protected $map = array();
  protected $conn;
  public function __construct($conn) {
    $this->conn = $conn;
    foreach(simplexml_load_file('bookmark.xml') as $field) {
      $this->map[(string)$field->name] = $field;
    }
  }
}

Теперь мы готовы для создания теста на метод save():

class BookmarkMapperTestCase extends BaseTestCase {
  function testSave() {
    $bookmark = new Bookmark;
    $bookmark->setUrl('http://phparch.com/');
    $bookmark->setName('php|architect');
    $bookmark->setDesc('php|arch magazine homepage');
    $bookmark->setGroup('php');
    $this->assertNull($bookmark->getId());
    $mapper = new BookmarkMapper($this->conn);
    $mapper->save($bookmark);
    $this->assertEqual(1, $bookmark->getId());
    // a row was added to the database table
    $this->assertEqual(1, $this->conn->getOne(
    'select count(1) from bookmark'));
  }
}

Этот тест создает новый экземпляр класса Bookmark, устанавливает соотвествующие значения, и просит экземпляр BookmarkMapper сохранить Bookmark. Также, в течении теста проверяется то, что объект устанавливает ID и вставляет запись в таблицу. Напишем код для реализации этого:

class BookmarkMapper {
  //...
  const INSERT_SQL = "
  insert into bookmark (url, name, description,
  tag, created, updated)
  values (?, ?, ?, ?, now(), now())
  ";
  public function save($bookmark) {
    $rs = $this->conn->execute(
    self::INSERT_SQL
    ,array(
    $bookmark->getUrl()
    ,$bookmark->getName()
    ,$bookmark->getDesc()
    ,$bookmark->getGroup()));
  }
}

Константа класса хранит выражение для осществления вставки, и код «вручную» производит соотношение get-методов класса Bookmark к верным позициям в SQL-выражении. Все этого хорошо, но требуется еще две вещи: код для определения ошибок БД и установки и изменение трибутов $bookmark, которые инициализируются или изменяются базой данных соотвественно.

class BookmarkMapper {
  //...
  public function save($bookmark) {
  	$insert = new ArrayObject(
 	     $bookmark->getUrl()
	    ,$bookmark->getName()
	    ,$bookmark->getDesc()
	    ,$bookmark->getGroup());
    if ($this->conn->execute(self::INSERT_SQL, $insert)) throw new Exception('DB Error: '.$this->conn->errorMsg());
 
    $inserted = $this->findById($this->conn->Insert_ID());
    //clean up database related fields in parameter instance
    $bookmark->setId($inserted->getId());
    $bookmark->setCrtTime($inserted->getCrtTime());
    $bookmark->setModTime($inserted->getModTime());
  }
}

Метод findById() находит и возвращает Bookmark, подходящий данному ID. Факитчески, BookmarkMapper вставляет новый Bookmark, получается запись из БД, и устанавливает необходимые атрибуты, основываясь на новых верных значениях. Возвращать ничего не требуется, поскольку экземпляр Bookmark был передан как параметр и он уже обновлен для верных значений. Взглянем детально на метод findById(). Вы можете использоваться такой-же BaseTestCase из предыдущей части Table Data Gateway:

class BookmarkMapperTestCase extends BaseTestCase {
  // ...
  function testFindById() {
    $mapper = new BookmarkMapper($this->conn);
    $this->addSeveralBookmarks($mapper);
    $this->assertIsA(
    $bookmark = $mapper->findById(1)
    , 'Bookmark');
    $this->assertEqual(1, $bookmark->getId());
  }
}

Технически, addSeveralBookmarks() не будет работать до тех пор, пока не работает findById(), поэтому вернемся к нему через минуту.

class BookmarkMapper {
  // ...
  public function findById($id) {
    $row = $this->conn->getRow(
    'select * from bookmark where id = ?'
    ,array((int)$id)
    );
    if ($row) {
      $bookmark = new Bookmark($this);
      foreach($this->map as $field) {
        $setprop = (string)$field->mutator;
        $value = $row[(string)$field->name];
        if ($setprop && $value) {
          call_user_func(array($bookmark, $setprop), $value);
        }
      }
      return $bookmark;
    } else {
      return false;
    }
  }
}

Так как каждый поисковый метод в BookmarkMapper должен трансформировать строку в таблице БД в экземпляр Bookmark, имеет смысл вынести этувозможность в отдельный метод createBookmarkFromRow().

class BookmarkMapper {
  // ...
  protected function createBookmarkFromRow($row) {
    $bookmark = new Bookmark($this);
    foreach($this->map as $field) {
      $setprop = (string)$field->mutator;
      $value = $row[(string)$field->name];
      if ($setprop && $value) {
        call_user_func(array($bookmark, $setprop), $value);
      }
    }
    return $bookmark;
  }
}

C этой функцией вы можете уменьшить findById() до:

class BookmarkMapper {
  // ...
  public function findById($id) {
    $row = $this->conn->getRow(
    'select * from bookmark where id = ?'
    ,array((int)$id)
    );
    if ($row) {
      return $this->createBookmarkFromRow($row);
    } else {
      return false;
    }
  }
}

Всё это было несколько усложнено, диаграмма UML может быть полезна для понимания.

:ooad:dm_02.gif

Во-первых, данные забираются из базы данных, затем создается экземпляр Bookmark. Затем для каждого поля код ищет подходящий set-метод и передает значение из строки этому методу. Объект Bookmark, теперь содержащий данные из БД, возвращается функцией findById(). Давайте взглянем на метод BookmarkMapper::add(), использованный в BaseTestCase::addSeveralBookmarks(). Используя тесторый случай, проверим что создается запись в таблице и возвращается экземпляр класса Bookmark с корректными данными.

class BookmarkMapperTestCase extends BaseTestCase {
  // ...
  function testAdd() {
    $mapper = new BookmarkMapper($this->conn);
    $bookmark =
    $mapper->add(
    'http://phparch.com',
    'php|arch',
    'php|architect magazine homepage',
    'php');
    $this->assertEqual(1,
    $this->conn->getOne('select count(1) from bookmark'));
    $this->assertEqual('http://phparch.com', $bookmark->getUrl());
    $this->assertEqual('php|arch', $bookmark->getName());
    $this->assertEqual('php|architect magazine homepage',
    $bookmark->getDesc());
    $this->assertEqual('php', $bookmark->getGroup());
  }
}

Ниже соотвествующий код BookmarkMapper:

class BookmarkMapper {
  // ...
  public function add($url, $name, $description, $group) {
    $bookmark = new Bookmark;
    $bookmark->setUrl($url);
    $bookmark->setName($name);
    $bookmark->setDesc($description);
    $bookmark->setGroup($group);
    $this->save($bookmark);
    return $bookmark;
  }
}

Это похоже на метод ActiveRecordTestCase::add(), с тем отличием, что он был добавлен к DataMapper, а не в тествый случай, сделав его доступным внутри проектного кода. Сейчас мы можем перейти к реализации дополнительных поисковых методом, включая методы, которые возвращают коллекции экземпляров Bookmark.

class BookmarkMapperTestCase extends BaseTestCase {
  // ...
  function testFindByGroup() {
    $mapper = new BookmarkMapper($this->conn);
    $this->addSeveralBookmarks($mapper);
    $this->assertIsA(
    $php_links = $mapper->findByGroup('php')
    ,'array');
    $this->assertEqual(3, count($php_links));
    foreach($php_links as $link) {
      $this->assertIsA($link, 'Bookmark');
    }
  }
}

Нахождение всех экземпляров в определенной группе может быть реализовано так:

class BookmarkMapper {
  // ...
  public function findByGroup($group) {
    $rs = $this->conn->execute(
    'select * from bookmark where tag like ?'
    ,array($group.'%'));
    if ($rs) {
      $ret = array();
      foreach($rs->getArray() as $row) {
        $ret[] = $this->createBookmarkFromRow($row);
      }
      return $ret;
    }
  }
}

Метод ADOConnection::execute() возвращает объект ADOResultSet. Этот список имеет метод getArray(), который возвращает массив ассоциативных массивов (поле ⇒ значение) для каждой строки. Эти строки в цикле передаются методу createBookmarkFromRow() для создания экземпляров класса Bookmark. А что по поводу обновления в DataMapper? Обновление это также процесс взаимодействия между Bookmark и BookmarkMapper. Проверить в том, что закладки действительно обновлены можно лучше всего в BookmarkTestCase. Проверка обращения данных к БД и обратно происходит в тестах на BookmarkMapper.

class BookmarkTestCase extends BaseTestCase {
  // ...
  function testSaveUpdatesDatabase() {
    $mapper = new BookmarkMapper($this->conn);
    $this->addSeveralBookmarks($mapper);
    $bookmark = $mapper->findById(1);
    $this->assertEqual(
    'http://blog.casey-sweat.us/'
    ,$bookmark->getUrl());
    $bookmark->setUrl(
    'http://blog.casey-sweat.us/wp-rss2.php');
    $mapper->save($bookmark);
    $bookmark2 = $mapper->findById(1);
    $this->assertEqual(
    'http://blog.casey-sweat.us/wp-rss2.php'
    ,$bookmark2->getUrl());
  }
}

Как и сейчас, метод save() вставляет закладки в БД используя INSERT. Однако, как преведено в тесте, метод save() должен определять Bookmark – это новая запись, или уже существующая в базе данных. Для новой записи необходим INSERT, для существующей – UPDATE. Изходя из теста, давайте сделаем рефакторинг для кода, исполняющего выражение INSERT, которое было в методе save() и перенесем его в защищенный метод insert().

class BookmarkMapper {
  //...
  protected function insert($bookmark) {
    $rs = $this->conn->execute(
    self::INSERT_SQL
    ,array(
    $bookmark->getUrl()
    ,$bookmark->getName()
    ,$bookmark->getDesc()
    ,$bookmark->getGroup()));
    if ($rs) {
      $inserted = $this->findById($this->conn->Insert_ID());
      // clean up database related fields in parameter instance
      if (method_exists($inserted,'setId')) {
        $bookmark->setId($inserted->getId());
        $bookmark->setCrtTime($inserted->getCrtTime());
        $bookmark->setModTime($inserted->getModTime());
      }
    } else {
      throw new Exception('DB Error: '.$this->conn->errorMsg());
    }
  }
}

Переименовав существующий метод save() в insert(), необходимо реализовать метод save(), который бы с помощью getId() определял определен ли атрибут $id.

class BookmarkMapper {
  //...
  public function save($bookmark) {
    if ($bookmark->getId()) {
      $this->update($bookmark);
    } else {
      $this->insert($bookmark);
    }
  }
}

Теперь необходим метод update(), который будет похож на insert(). Как вы помните, метод insert() имеет жетские зависимости между атрибутами и полями таблицы. Давайте используем для метода update() динамический подход, используя информацию из файла bookmark.xml.

class BookmarkMapper {
  //...
  const UPDATE_SQL = "
  update bookmark set
  url = ?,
  name = ?,
  description = ?,
  tag = ?,
  updated = now()
  where id = ?
  ";
  protected function update($bookmark) {
    $binds = array();
    foreach(array('url','name',
    'description','tag','id') as $fieldname) {
      $field = $this->map[$fieldname];
      $getprop = (string)$field->accessor;
      $binds[] = $bookmark->$getprop();
    }
    $this->conn->execute(
    self::UPDATE_SQL
    ,$binds);
  }
}

Заметьте что порядок элементов в массиве соотвествует порядку полей в SQL-выражении. Метод update() иллистрирует суть шаблона DataMapper: он осуществляет взаимоотношения между атрибутами класса и полями таблицы в БД. Наконец, взглянем на реализацию удаления из набора CRUD. Напишем метод для класса BookmarkMapper, который принимает параметром ъкземпляр объекта Bookmark и удаляет его из БД. Во-первых, тест:

class BookmarkMapperTestCase extends BaseTestCase {
  // ...
  function testDelete() {
    $mapper = new BookmarkMapper($this->conn);
    $this->addSeveralBookmarks($mapper);
    $this->assertEqual(5, $this->countBookmarks());
    $delete_me = $mapper->findById(3);
    $mapper->delete($delete_me);
    $this->assertEqual(4, $this->countBookmarks());
  }
  function countBookmarks() {
    return $this->conn->getOne(
    'select count(1) from bookmark');
  }
}

И код:

class BookmarkMapper {
  // ...
  public function delete($bookmark) {
    $this->conn->execute(
    'delete from bookmark where id = ?'
    ,array((int)$bookmark->getId()));
  }
}

И вот, мы реализовали шаблон DataMapper для таблиы bookmark со всеми возможностями CRUD. Если создание вашего объекта достаточно ресурсоемко, можно реализовать метод BookmarkMapper::deleteById(), который не требует загрузки доменного объекта перед удалением.

Выводы

Видно, что добавление уровня между схемой БД и доменными объектами вносит некоторую сложность. Однако эта сложность дает вам огромную гибкость в вашем коде - вы свободны в развитии вашего класса, не завися от структуры таблиц в БД. Вы также должны понимать, что это достаточно простой трансляционный механизм. Если вы хотите развить этот механизм отношений между таблицами и их соотвествующими отношениями в доменной модели, вам необходимо направить свой взгляд «к святой чаше Грааля» ORM – Object Relational Mapping, который не может быть так легко описан.

 
ooad/dp/data_mapper.txt · Последние изменения: 2006/10/10 00:49 necromant2005
 
Recent changes RSS feed Creative Commons License Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki