前面分别介绍了数据源架构模式之表数据入口、数据源架构模式之行和数据入口数据源架构模式之活动记录,相较于这三种数据源架构模式,数据映射器显得更加“高大上”。
一、概念
数据映射器(Data Mapper):在保持对象和数据库(以及映射器本身)彼此独立的情况下,在二者之间移动数据的一个映射器层。概念永远都是抽象的,简单的说,数据映射器就是一个负责将数据映射到对象的类数据。
二、为什么要使用数据映射器?
数据映射器实现起来比前三种模式都要复杂,那为什么还要使用它呢?
对象间的组织关系和关系数据库中的表是不同的。数据库表可以看成是由行与列组成的格子,表中的一行可以通过外键和另一个表(甚至同一个表)中的一行关联,而对象的组织关系更为复杂:一个对象可能包含其他对象;不同的数据结构可能通过不同的方式组织相同的对象。
对象和关系数据库之间的这种分歧被称为“对象关系阻抗不匹配”或“阻抗不匹配”。
数据映射器可以很好地解决这个问题,由它来负责对象和关系数据库两者数据的转换,从而有效地在领域模型中隐藏数据库操作并管理数据库转换中不可以避免的冲突。
三、简单实现数据映射器
<?php //领域抽象类 abstract class DomainObject { private $id = -1; function __construct( $id=null ) { if ( is_null( $id ) ) { $this->markNew(); } else { $this->id = $id; } } function getId( ) { return $this->id; } static function getCollection( $type ) { //这里通过一个工广生成此对象对应的数组数据对象 return HelperFactory::getCollection( $type ); } function collection() { return self::getCollection( get_class( $this ) ); } function finder() { return self::getFinder( get_class( $this ) ); } static function getFinder( $type ) { //这里通过一个工厂生成此对象对应的map对象 return HelperFactory::getFinder( $type ); } function setId( $id ) { $this->id = $id; } function __clone() { $this->id = -1; } } //场所类 class Venue extends DomainObject { private $name; private $spaces; function __construct( $id=null, $name=null ) { $this->name = $name; parent::__construct( $id ); } function setSpaces( SpaceCollection $spaces ) { $this->spaces = $spaces; } function getSpaces() { if ( ! isset( $this->spaces ) ) { //创建对应的SpaceMapper对象 $finder = self::getFinder( 'Space' ); $this->spaces = $finder->findByVenue( $this->getId() ); //$this->spaces = self::getCollection("Space"); } return $this->spaces; } function addSpace( Space $space ) { $this->getSpaces()->add( $space ); $space->setVenue( $this ); } function setName( $name_s ) { $this->name = $name_s; } function getName( ) { return $this->name; } static function findAll() { $finder = self::getFinder( __CLASS__ ); return $finder->findAll(); } static function find( $id ) { $finder = self::getFinder( __CLASS__ ); return $finder->find( $id ); } } abstract class Mapper{ protected static $PDO; function __construct() { if ( ! isset(self::$PDO) ) { //此处可加缓存 self::$PDO = new PDO( $dsn ); self::$PDO->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } } private function getFromMap( $id ) { //从内存取出此$id的DomainObject对象 } private function addToMap( DomainObject $obj ) { //将此DomainObject对象加入到内存 } function find( $id ) { $old = $this->getFromMap( $id ); if ( $old ) { return $old; } $this->selectstmt()->execute( array( $id ) ); $array = $this->selectstmt()->fetch( ); $this->selectstmt()->closeCursor( ); if ( ! is_array( $array ) ) { return null; } if ( ! isset( $array['id'] ) ) { return null; } $object = $this->createObject( $array ); return $object; } function findAll( ) { $this->selectAllStmt()->execute( array() ); return $this->getCollection( $this->selectAllStmt()->fetchAll( PDO::FETCH_ASSOC ) ); } function createObject( $array ) { $old = $this->getFromMap( $array['id']); if ( $old ) { return $old; } $obj = $this->doCreateObject( $array ); $this->addToMap( $obj ); return $obj; } function insert( DomainObject $obj ) { $this->doInsert( $obj ); $this->addToMap( $obj ); } protected abstract function getCollection( array $raw ); protected abstract function doCreateObject( array $array ); protected abstract function doInsert( DomainObject $object ); protected abstract function targetClass(); protected abstract function selectStmt( ); protected abstract function selectAllStmt( ); } class VenueMapper extends Mapper { function __construct() { parent::__construct(); $this->selectAllStmt = self::$PDO->prepare( "SELECT * FROM venue"); $this->selectStmt = self::$PDO->prepare( "SELECT * FROM venue WHERE id=?"); $this->updateStmt = self::$PDO->prepare( "UPDATE venue SET name=?, id=? WHERE id=?"); $this->insertStmt = self::$PDO->prepare( "INSERT into venue ( name ) values( ? )"); } function getCollection( array $raw ) { //这里简单起见用个对象数组 $ret = array(); foreach ($raw as $value) { $ret[] = $this->createObject($value); } return $ret; } protected function doCreateObject( array $array ) { $obj = new Venue( $array['id'] ); $obj->setname( $array['name'] ); //$space_mapper = new SpaceMapper(); //$space_collection = $space_mapper->findByVenue( $array['id'] ); //$obj->setSpaces( $space_collection ); return $obj; } protected function targetClass() { return "Venue"; } protected function doInsert( DomainObject $object ) { $values = array( $object->getname() ); $this->insertStmt->execute( $values ); $id = self::$PDO->lastInsertId(); $object->setId( $id ); } function update( DomainObject $object ) { $values = array( $object->getname(), $object->getid(), $object->getId() ); $this->updateStmt->execute( $values ); } function selectStmt() { return $this->selectStmt; } function selectAllStmt() { return $this->selectAllStmt; } } //client代码 $venue = new venue(); $venue->setName("XXXXXXX"); //插入一条数据 $mapper = new VenueMapper(); $mapper->insert($venue); //获取刚插入的数据 $venueInfo = $mapper->find($venue->getId()); //修改数据 $venue->setName('OOOOOOOOOOO'); $mapper->update($venue); ?>
代码省略了一些辅助类,保留最主要的领域对象和数据映射器。数据映射器模式最强大的地方在于消除了领域层和数据库操作之间的耦合。Mapper对象在幕后运作,可以应用于各种对象关系映射。而与之带来的是需要创建大量具体的映射器类。不过现在框架都可以通过程序自动生成了。
四、使用时机
使用数据库映射器的主要是数据库方案和对象模型需要彼此独立演变的时候。最常见的当然是和领域模式一起使用。数据映射器无论是在设计阶段、开发阶段,还是测试阶段,在领域模型上操作时可以不考虑数据库。领域对象对数据库的结构一无所知,因为所有这些对应关系都由数据映射器完成。
当然,数据映射器引入了新的层次,因此使用这些模式的前提条件是业务逻辑的复杂性,如果很简单,那就没必要了。
如果没有领域模型,我不会选用数据映射器。但是没有数据映射器时,能使用领域模型吗?如果领域模型简单,且数据库受领域模型开发者的控制,则领域对象用活动记录直接访问数据库也是合理的。
不必创建完全意义上的数据库映射层。创建这样的数据映射器很复杂。大多数情况下,建议使用开源的数据库映射层而不是自己动手创建