前面分別介紹了資料來源架構模式之表資料入口、資料來源架構模式之行和資料入口資料來源架構模式之活動記錄,相較於這三種資料來源架構模式,資料映射器顯得更加「高大上」。
一、概念
資料映射器(Data Mapper):在保持物件和資料庫(以及映射器本身)彼此獨立的情況下,在二者之間移動資料的一個映射器層。概念永遠是抽象的,簡單的說,資料映射器就是負責將資料映射到物件的類別資料。
二、為什麼要使用資料映射器?
資料映射器實作起來比前三種模式都要複雜,那為什麼還要使用它呢?
物件間的組織關係和關係資料庫中的表是不同的。資料庫表格可以看成是由行與列組成的格子,表中的一行可以透過外鍵和另一個表(甚至同一個表)中的一行關聯,而物件的組織關係更為複雜:一個物件可能包含其他物件;不同的資料結構可能透過不同的方式組織相同的物件。
物件和關聯式資料庫之間的這種分歧被稱為「物件關係阻抗不匹配」或「阻抗不匹配」。
資料映射器可以很好地解決這個問題,由它來負責物件和關聯式資料庫兩者資料的轉換,從而有效地在領域模型中隱藏資料庫操作並管理資料庫轉換中不可以避免的衝突。
三、簡單實作資料映射器
Php程式碼
<?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物件在幕後運作,可以應用於各種物件關係映射。而與之帶來的是需要創建大量具體的映射器類別。不過現在框架都可以透過程式自動產生了。
四、使用時機
使用資料庫映射器的主要是資料庫方案和物件模型需要彼此獨立演進的時候。最常見的當然是和領域模式一起使用。資料映射器無論是在設計階段、開發階段,或是測試階段,在領域模型上操作時可以不考慮資料庫。領域物件對資料庫的結構一無所知,因為所有這些對應關係都由資料映射器完成。
當然,資料映射器引入了新的層次,因此使用這些模式的前提條件是業務邏輯的複雜性,如果很簡單,那就沒必要了。
如果沒有領域模型,我不會選用資料映射器。但是沒有資料映射器時,能使用領域模型嗎?如果領域模型簡單,且資料庫受領域模型開發者的控制,則領域物件用活動記錄直接存取資料庫也是合理的。
不必建立完全意義上的資料庫映射層。創建這樣的資料映射器很複雜。大多數情況下,建議使用開源的資料庫映射層而不是自己動手創建