數據源架構模式 - 表入口模式
表入口模式充當數據庫表訪問入口的對象,一個實例處理表中的所有行。
可以理解為對之前分散在各個頁面的sql語句進行封裝,一張表就是一個對象,該對象處理所有與該表有關的業務邏輯,很好的提高了代碼的復用性。
現在想起來,當初剛畢業那會兒,經常使用表入口模式。
具體的實現方式參見代碼:
database.php
- <?php
- class Database{
- //只是為了演示,通常情況下數據庫的配置是會單獨寫在配置文件中的
- private static $_dbConfig = array(
- 'host' => '127.0.0.1',
- 'username' => 'root',
- 'pwd' => '',
- 'dbname' => 'bussiness'
- );
- private static $_instance;
- public static function getInstance(){
- if(is_null(self::$_instance)){
- self::$_instance = new mysqli(self::$_dbConfig['host'], self::$_dbConfig['username'], self::$_dbConfig['pwd'], self::$_dbConfig['dbname']);
- if(self::$_instance->connect_errno){
- throw new Exception(self::$_instance->connect_error);
- }
- }
- return self::$_instance;
- }
- }
person.php
- <?php
- require_once 'database.php';
- class Person extends Database{
- public $instance;
- public $table = 'person';
- public function __construct(){
- $this->instance = Person::getInstance();
- }
- public function getPersonById($personId){
- $sql = "<a href="/"/tags.php/select//"" target="/"_blank/"">select</a> * from $this->table where id=$personId";
- echo $sql;
- return $this->instance->query($sql);
- }
- /**其他的一些增刪改查操作方法...**/
- }
index.php
- require_once 'person.php';
- $person = new Person();
- var_dump($person->getPersonById(1)->fetch_assoc());
- die();
運行結果:
- select * from person where id=1
- array (size=2)
- 'id' => string '1' (length=1)
- 'name' => string 'ben' (length=3)
數據源架構模式 - 行入口模式
一、概念
行數據入口(Row Data Gateway):充當數據源中單條記錄入口的對象,每行一個實例。
二、簡單實現行數據入口
為了方便理解,還是先簡單實現:
- <?php
- /**
- * 企業應用架構 數據源架構模式之行數據入口 2010-09-27 sz
- * @author phppan.p#gmail.com http://www.phppan.com
- * 哥學社成員(http://www.blog-brother.com/)
- * @package architecture
- */
- class PersonGateway {
- private $_name;
- private $_id;
- private $_birthday;
- public function __construct($id, $name, $birthday) {
- $this->setId($id);
- $this->setName($name);
- $this->setBirthday($birthday);
- }
- public function getName() {
- return $this->_name;
- }
- public function setName($name) {
- $this->_name = $name;
- }
- public function getId() {
- return $this->_id;
- }
- public function setId($id) {
- $this->_id = $id;
- }
- public function getBirthday() {
- return $this->_birthday;
- }
- public function setBirthday($birthday) {
- $this->_birthday = $birthday;
- }
- /**
- * 入口類自身擁有更新操作
- */
- public function update() {
- $data = array('id' => $this->_id, 'name' => $this->_name, 'birthday' => $this->_birthday);
- $sql = "UPDATE person SET ";
- <a href="/"/tags.php/foreach//"" target="/"_blank/"">foreach</a> ($data as $field => $value) {
- $sql .= "`" . $field . "` = '" . $value . "',";
- }
- $sql = <a href="/"/tags.php/substr//"" target="/"_blank/"">substr</a>($sql, 0, -1);
- $sql .= " WHERE id = " . $this->_id;
- return DB::query($sql);
- }
- /**
- * 入口類自身擁有插入操作
- */
- public function insert() {
- $data = array('name' => $this->_name, 'birthday' => $this->_birthday);
- $sql = "INSERT INTO person ";
- $sql .= "(`" . implode("`,`", array_keys($data)) . "`)";
- $sql .= " VALUES('" . implode("','", array_values($data)) . "')";
- return DB::query($sql);
- }
- public static function load($rs) {
- /* 此處可加上緩存 */
- return new PersonGateway($rs['id'] ? $rs['id'] : NULL, $rs['name'], $rs['birthday']);
- }
- }
- /**
- * 人員查找類
- */
- class PersonFinder {
- public function find($id) {
- $sql = "SELECT * FROM person WHERE id = " . $id;
- $rs = DB::query($sql);
- return PersonGateway::load($rs);
- }
- public function findAll() {
- $sql = "SELECT * FROM person";
- $rs = DB::query($sql);
- $result = array();
- if (is_array($rs)) {
- foreach ($rs as $row) {
- $result[] = PersonGateway::load($row);
- }
- }
- return $result;
- }
- }
- class DB {
- /**
- * 這只是一個執行SQL的演示方法
- * @param string $sql 需要執行的SQL
- */
- public static function query($sql) {
- echo "執行SQL: ", $sql, " <br />";
- if (strpos($sql, 'SELECT') !== FALSE) { // 示例,對于select查詢返回查詢結果
- return array('id' => 1, 'name' => 'Martin', 'birthday' => '2010-09-15');
- }
- }
- }
- /**
- * 客戶端調用
- */
- class Client {
- /**
- * Main program.
- */
- public static function main() {
- header("Content-type:text/html; charset=utf-8");
- /* 寫入示例 */
- $data = array('name' => 'Martin', 'birthday' => '2010-09-15');
- $person = PersonGateway::load($data);
- $person->insert();
- /* 更新示例 */
- $data = array('id' => 1, 'name' => 'Martin', 'birthday' => '2010-09-15');
- $person = PersonGateway::load($data);
- $person->setName('Phppan');
- $person->update();
- /* 查詢示例 */
- $finder = new PersonFinder();
- $person = $finder->find(1);
- echo $person->getName();
- //Vevb.com
- }
- }
- Client::main();
- ?>
三、運行機制
●行數據入口是單條記錄極其相似的對象,在該對象中數據庫中的每一列為一個域。
●行數據入口一般能實現從數據源類型到內存中類型的任意轉換。
●行數據入口不存在任何領域邏輯,如果存在,則是活動記錄。
●在實例可看到,為了從數據庫中讀取信息,設置獨立的OrderFinder類。當然這里也可以選擇不新建類,采用靜態查找方法,但是它不支持需要為不同數據源提供不同查找方法的多態。因此這里最好單獨設置查找方法的對象。
●行數據入口除了可以用于表外還可以用于視圖。需要注意的是視圖的更新操作。
●在代碼中可見“定義元數據映射”,這是一種很好的作法,這樣一來,所有的數據庫訪問代碼都可以在自動建立過程中自動生成。
四、使用場景
4.1 事務腳本
可以很好地分離數據庫訪問代碼,并且也很容易被不同的事務腳本重用。不過可能會發現業務邏輯在多處腳本中重復出現,這些邏輯可能在行數據入口中有用。不斷移動這些邏輯會使行數據入口演變為活動記錄,這樣減少了業務邏輯的重復。
4.2 領域模型
如果要改變數據庫的結構但不想改變領域邏輯,采用行數據入口是不錯的選擇。大多數情況,數據映射器更加適合領域模型。
行數據入口能和數據映射器一起配合使用,盡管這樣看起來有點多此一舉,不過,當行數據入口從元數據自動生成,而數據映射器由手動實現時,這種方法會很有效。
數據源架構模式 - 活動記錄
【活動記錄的意圖】
一個對象,它包裝數據表或視圖中某一行,封裝數據庫訪問,并在這些數據上增加了領域邏輯。
【活動記錄的適用場景】
適用于不太復雜的領域邏輯,如CRUD操作等。
【活動記錄的運行機制】
對象既有數據又有行為。其使用最直接的方法,將數據訪問邏輯置于領域對象中。
活動記錄的本質是一個領域模型,這個領域模型中的類和基數據庫中的記錄結構應該完全匹配,類的每個域對應表的每一列。
一般來說,活動記錄包括如下一些方法:
1、由數據行構造一個活動記錄實例;
2、為將來對表的插入構造一個新的實例;
3、用靜態查找方法來包裝常用的SQL查詢和返回活動記錄;
4、更新數據庫并將活動記錄中的數據插入數據庫;
5、獲取或設置域;
6、實現部分業務邏輯。
【活動記錄的優點和缺點】
優點:
1、簡單,容易創建并且容易理解。
2、在使用事務腳本時,減少代碼復制。
3、可以在改變數據庫結構時不改變領域邏輯。
4、基于單個活動記錄的派生和測試驗證會很有效。
缺點:
1、沒有隱藏關系數據庫的存在。
2、僅當活動記錄對象和數據庫中表直接對應時,活動記錄才會有效。
3、要求對象的設計和數據庫的設計緊耦合,這使得項目中的進一步重構很困難
【活動記錄與其它模式】
數據源架構模式之行數據入口:活動記錄與行數據入口十分類似。二者的主要差別是行數據入口 僅有數據庫訪問而活動記錄既有數據源邏輯又有領域邏輯。
【活動記錄的PHP示例】
- <?php
- /**
- * 企業應用架構 數據源架構模式之活動記錄 2010-10-17 sz
- * @author phppan.p#gmail.com http://www.phppan.com
- * 哥學社成員(http://www.blog-brother.com/)
- * @package architecture
- */
- /**
- * 定單類
- */
- class Order {
- /**
- * 定單ID
- * @var <type>
- */
- private $_order_id;
- /**
- * 客戶ID
- * @var <type>
- */
- private $_customer_id;
- /**
- * 定單金額
- * @var <type>
- */
- private $_amount;
- public function __construct($order_id, $customer_id, $amount) {
- $this->_order_id = $order_id;
- $this->_customer_id = $customer_id;
- $this->_amount = $amount;
- }
- /**
- * 實例的刪除操作
- */
- public function delete() {
- $sql = "DELETE FROM Order SET WHERE order_id = " . $this->_order_id . " AND customer_id = " . $this->_customer_id;
- return DB::query($sql);
- }
- /**
- * 實例的更新操作
- */
- public function update() {
- }
- /**
- * 插入操作
- */
- public function insert() {
- }
- public static function load($rs) {
- return new Order($rs['order_id'] ? $rs['order_id'] : NULL, $rs['customer_id'], $rs['amount'] ? $rs['amount'] : 0);
- }
- }
- class Customer {
- private $_name;
- private $_customer_id;
- public function __construct($customer_id, $name) {
- $this->_customer_id = $customer_id;
- $this->_name = $name;
- }
- /**
- * 用戶刪除定單操作 此實例方法包含了業務邏輯
- * 通過調用定單實例實現
- * 假設此處是對應的刪除操作(實際中可能是一種以某字段來標記的假刪除操作)
- */
- public function deleteOrder($order_id) {
- $order = Order::load(array('order_id' => $order_id, 'customer_id' => $this->_customer_id));
- return $order->delete();
- }
- /**
- * 實例的更新操作
- */
- public function update() {
- }
- /**
- * 入口類自身擁有插入操作
- */
- public function insert() {
- }
- public static function load($rs) {
- /* 此處可加上緩存 */
- return new Customer($rs['customer_id'] ? $rs['customer_id'] : NULL, $rs['name']);
- }
- /**
- * 根據客戶ID 查找
- * @param integer $id 客戶ID
- * @return Customer 客戶對象
- */
- public static function find($id) {
- return CustomerFinder::find($id);
- }
- }
- /**
- * 人員查找類
- */
- class CustomerFinder {
- public static function find($id) {
- $sql = "SELECT * FROM person WHERE customer_id = " . $id;
- $rs = DB::query($sql);
- return Customer::load($rs);
- }
- }
- class DB {
- /**
- * 這只是一個執行SQL的演示方法
- * @param string $sql 需要執行的SQL
- */
- public static function query($sql) {
- echo "執行SQL: ", $sql, " <br />";
- if (strpos($sql, 'SELECT') !== FALSE) { // 示例,對于select查詢返回查詢結果
- return array('customer_id' => 1, 'name' => 'Martin');
- }
- }
- }
- /**
- * 客戶端調用
- */
- class Client {
- /**
- * Main program.
- */
- public static function main() {
- header("Content-type:text/html; charset=utf-8");
- /* 加載客戶ID為1的客戶信息 */
- $customer = Customer::find(1);
- /* 假設用戶擁有的定單id為 9527*/
- $customer->deleteOrder(9527);
- } //Vevb.com
- }
- Client::main();
- ?>
同前面的文章一樣,這僅僅是一個活動記錄的示例,關于活動記錄模式的應用,可以查看Yii框架中的DB類,在其源碼中有一個CActiveRecord抽象類,從這里可以看到活動記錄模式的應用
另外,如果從事務腳本中創建活動記錄,一般是首先將表包裝為入口,接著開始行為遷移,使表深化成為活動記錄。
對于活動記錄中的域的訪問和設置可以如yii框架一樣,使用魔術方法__set方法和__get方法。
數據源架構模式 - 數據映射器
一:數據映射器
關系型數據庫用來存儲數據和關系,對象則可以處理業務邏輯,所以,要把數據本身和業務邏輯糅雜到一個對象中,我們要么使用 活動記錄,要么把兩者分開,通過數據映射器把兩者關聯起來。
數據映射器是分離內存對象和數據庫的中間軟件層,下面這個時序圖描述了這個中間軟件層的概念:

在這個時序圖中,我們還看到一個概念,映射器需能夠獲取領域對象(在這個例子中,a Person 就是一個領域對象)。而對于數據的變化(或者說領域對象的變化),映射器還必須要知道這些變化,在這個時候,我們就需要 工作單元 模式(后議)。
從上圖中,我們仿佛看到 數據映射器 還蠻簡單的,復雜的部分是:我們需要處理聯表查詢,領域對象的繼承等。領域對象的字段則可能來自于數據庫中的多個表,這種時候,我們就必須要讓數據映射器做更多的事情。是的,以上我們說到了,數據映射器要能做到兩個復雜的部分:
1:感知變化;
2:通過聯表查詢的結果,為領域對象賦值;
為了感知變化以及與數據庫對象保持一致,則需要 標識映射(架構模式對象與關系結構模式之:標識域(Identity Field)),這通常需要有 標識映射的注冊表,或者為每個查找方法持有一個 標識映射,下面的代碼是后者:
- void Main()
- {
- SqlHelper.ConnectionString = "Data Source=xxx;Initial Catalog=xxx;Integrated Security=False;User ID=sa;Password=xxx;Connect Timeout=15;Encrypt=False;TrustServerCertificate=False";
- var user1 = User.FindUser("6f7ff44435f3412cada61898bcf0df6c");
- var user2 = User.FindUser("6f7ff44435f3412cada61898bcf0df6c");
- (user1 == user2).Dump();
- "END".Dump();
- }
- public abstract class BaseMode
- {
- public string Id {get; set;}
- public string Name {get; set;}
- }
- public class User : BaseMode
- {
- static UserMap map = new UserMap();
- public static User FindUser(string id)
- {
- var user = map.Find(id);
- return user;
- }
- }
- public class UserMap : AbstractMapper<User>
- {
- public User Find(string id)
- {
- return (User)AbstractFind(id);
- }
- protected override User AbstractFind(string id)
- {
- var user = base.AbstractFind(id);
- if( user == null )
- {
- "is Null".Dump();
- string sql = "SELECT * FROM [EL_Organization].[User] WHERE ID=@Id";
- var pms = new SqlParameter[]
- {
- new SqlParameter("@Id", id)
- };
- var ds = SqlHelper.ExecuteDataset(CommandType.Text, sql, pms);
- user = DataTableHelper.ToList<User>(ds.Tables[0]).FirstOrDefault();
- if(user == null)
- {
- return null;
- }
- user = Load(user);
- return user;
- }
- return user;
- }
- public List<User> FindList(string name)
- {
- // SELECT * FROM USER WHERE NAME LIKE NAME
- List<User> users = null;
- return LoadAll(users);
- }
- public void Update(User user)
- {
- // UPDATE USER SET ....
- }
- }
- public abstract class AbstractMapper<T> where T : BaseMode
- {
- // 這里的問題是,隨著對象消失,loadedMap就被回收
- protected Dictionary<string, T> loadedMap = new Dictionary<string, T>();
- protected T Load(T t)
- {
- if(loadedMap.ContainsKey(t.Id) )
- {
- return loadedMap[t.Id];
- }
- else
- {
- loadedMap.Add(t.Id, t);
- return t;
- }
- }
- protected List<T> LoadAll(List<T> ts)
- {
- for(int i=0; i < ts.Count; i++)
- {
- ts[i] = Load(ts[i]);
- }
- return ts;
- }
- protected virtual T AbstractFind(string id)
- {
- if(loadedMap.ContainsKey(id))
- {
- return loadedMap[id];
- } //Vevb.com
- else
- {
- return null;
- }
- }
- }
上面是一個簡單的映射器,它具備了 標識映射 功能。由于有標識映射,所以我們運行這段代碼得到的結果是:

回歸本問實質,問題:什么叫 “數據映射”
其實,這個問題很關鍵,UserMap 通過 Find 方法,將數據庫記錄變成了一個 User 對象,這就叫 “數據映射”,但是,真正起到核心作用的是 user = DataTableHelper.ToList<User>(ds.Tables[0]).FirstOrDefault(); 這行代碼。更進一步的,DataTableHelper.ToList<T> 這個方法完成了 數據映射 功能。
那么,DataTableHelper.ToList<T> 方法具體干了什么事情,實際上,無非就是根據屬性名去獲取 DataTable 的字段值。這是一種簡便的方法,或者說,在很多業務不復雜的場景下,這也許是個好辦法,但是,因為業務往往是復雜的,所以實際情況下,我們使用這個方法的情況并不是很多,大多數情況下,我們需要像這樣編碼來完成映射:
someone.Name = Convert.ToString(row["Name"])
不要懷疑,上面這行代碼,就叫數據映射,任何高大上的概念,實際上就是那條你寫了很多遍的代碼。
1.1 EntityFramework 中的數據映射
這是一個典型的 EF 的數據映射類,
- public class CourseMap : EntityTypeConfiguration<Course>
- {
- public CourseMap()
- {
- // Primary Key
- this.HasKey(t => t.CourseID);
- // Properties
- this.Property(t => t.CourseID)
- .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
- this.Property(t => t.Title)
- .IsRequired()
- .HasMaxLength(100);
- // Table & Column Mappings
- this.ToTable("Course");
- this.Property(t => t.CourseID).HasColumnName("CourseID");
- this.Property(t => t.Title).HasColumnName("Title");
- this.Property(t => t.Credits).HasColumnName("Credits");
- this.Property(t => t.DepartmentID).HasColumnName("DepartmentID");
- // Relationships
- this.HasMany(t => t.People)
- .WithMany(t => t.Courses)
- .Map(m =>
- {
- m.ToTable("CourseInstructor");
- m.MapLeftKey("CourseID");
- m.MapRightKey("PersonID");
- });
- this.HasRequired(t => t.Department)
- .WithMany(t => t.Courses)
- .HasForeignKey(d => d.DepartmentID);
- }
- }
我們可以看到,EF 的數據映射,那算是真正的數據映射。最基本的,其在內部無非是干了一件這樣的事情:
數據庫是哪個字段,對應的內存對象的屬性是哪個屬性。
最終,它都是通過一個對象工廠把領域模型生成出來,其原理大致如下:
- internal static Course BuildCourse(IDataReader reader)
- {
- Course course = new Course(reader[FieldNames.CourseId]);
- contract.Title = reader[FieldNames.Title].ToString();
- …
- return contract;
- }
二:倉儲庫
UserMap 關于 數據映射器 的概念是不是覺得太重了?因為它干了 映射 和 持久化 的事情,它甚至還得持有 工作單元。那么,如果我們能不能像 EF 一樣,映射器 只干映射的事情,而把其余事情分出去呢?可以,分離出去的這部分就叫做 倉儲庫。
三:再多說一點 DataTableHelper.ToList<T>,簡化的數據映射器
其實就是 DataTable To List 了。如果你在用 EF 或者 NHibernate 這樣的框架,那么,就用它們提供的映射器好了(嚴格來說,你不是在使用它們的映射器。因為這些框架本身才是在使用自己的映射器,我們只是在配置映射器所要的數據和關系而已,有時候,這些配置是在配置文件中,有時候是在字段或屬性上加 Attribute,有時候則是簡單但龐大的單行代碼)。我們當然也可以創建自己的 標準的 映射器,Tim McCarthy 在 《領域驅動設計 C# 2008 實現》 中就實現了這樣的映射器。但是,EF 和 NHibernate 固然很好,但是很多時候我們還是不得不使用 手寫SQL,因為:
1:EF 和 NHibernate 是需要學習成本的,這代表者團隊培訓成本高,且易出錯的;
2:不應放棄 手寫SQL 的高效性。
新聞熱點
疑難解答