前面分别介紹了資料源架構模式之表資料入口、資料源架構模式之行和資料入口資料源架構模式之活動記錄,相較于這三種資料源架構模式,資料映射器顯得更加“高大上”。
一、概念
資料映射器(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對象在幕後運作,可以應用于各種對象關系映射。而與之帶來的是需要建立大量具體的映射器類。不過現在架構都可以通過程式自動生成了。
四、使用時機
使用資料庫映射器的主要是資料庫方案和對象模型需要彼此獨立演變的時候。最常見的當然是和領域模式一起使用。資料映射器無論是在設計階段、開發階段,還是測試階段,在領域模型上操作時可以不考慮資料庫。領域對象對資料庫的結構一無所知,因為所有這些對應關系都由資料映射器完成。
當然,資料映射器引入了新的層次,是以使用這些模式的前提條件是業務邏輯的複雜性,如果很簡單,那就沒必要了。
如果沒有領域模型,我不會選用資料映射器。但是沒有資料映射器時,能使用領域模型嗎?如果領域模型簡單,且資料庫受領域模型開發者的控制,則領域對象用活動記錄直接通路資料庫也是合理的。
不必建立完全意義上的資料庫映射層。建立這樣的資料映射器很複雜。大多數情況下,建議使用開源的資料庫映射層而不是自己動手建立