一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

数据源架构模式 表入口 行入口 活动记录 数据映射器

时间:2015-08-04 编辑:简简单单 来源:一聚教程网

数据源架构模式 - 表入口模式

表入口模式充当数据库表访问入口的对象,一个实例处理表中的所有行。

可以理解为对之前分散在各个页面的sql语句进行封装,一张表就是一个对象,该对象处理所有与该表有关的业务逻辑,很好的提高了代码的复用性。

现在想起来,当初刚毕业那会儿,经常使用表入口模式。

具体的实现方式参见代码:

database.php

 '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

instance = Person::getInstance();  
    }  
  
    public function getPersonById($personId){  
        $sql = "select * from $this->table where id=$personId";  
        echo $sql;  
        return $this->instance->query($sql);  
    }  
  
    /**其他的一些增删改查操作方法...**/  
}



index.php

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):充当数据源中单条记录入口的对象,每行一个实例。


二、简单实现行数据入口

为了方便理解,还是先简单实现:

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 ";
        foreach ($data as $field => $value) {
            $sql .= "`" . $field . "` = '" . $value . "',";
        }
        $sql = substr($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, " ";
 
        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();
 
    }
 
}
 
Client::main();
?>


三、运行机制

●行数据入口是单条记录极其相似的对象,在该对象中数据库中的每一列为一个域。

●行数据入口一般能实现从数据源类型到内存中类型的任意转换。

●行数据入口不存在任何领域逻辑,如果存在,则是活动记录。

●在实例可看到,为了从数据库中读取信息,设置独立的OrderFinder类。当然这里也可以选择不新建类,采用静态查找方法,但是它不支持需要为不同数据源提供不同查找方法的多态。因此这里最好单独设置查找方法的对象。

●行数据入口除了可以用于表外还可以用于视图。需要注意的是视图的更新操作。

●在代码中可见“定义元数据映射”,这是一种很好的作法,这样一来,所有的数据库访问代码都可以在自动建立过程中自动生成。

四、使用场景

4.1 事务脚本

可以很好地分离数据库访问代码,并且也很容易被不同的事务脚本重用。不过可能会发现业务逻辑在多处脚本中重复出现,这些逻辑可能在行数据入口中有用。不断移动这些逻辑会使行数据入口演变为活动记录,这样减少了业务逻辑的重复。

4.2 领域模型

如果要改变数据库的结构但不想改变领域逻辑,采用行数据入口是不错的选择。大多数情况,数据映射器更加适合领域模型。

行数据入口能和数据映射器一起配合使用,尽管这样看起来有点多此一举,不过,当行数据入口从元数据自动生成,而数据映射器由手动实现时,这种方法会很有效。



数据源架构模式 - 活动记录

  【活动记录的意图】

  一个对象,它包装数据表或视图中某一行,封装数据库访问,并在这些数据上增加了领域逻辑。

  【活动记录的适用场景】

  适用于不太复杂的领域逻辑,如CRUD操作等。

  【活动记录的运行机制】

  对象既有数据又有行为。其使用最直接的方法,将数据访问逻辑置于领域对象中。

  活动记录的本质是一个领域模型,这个领域模型中的类和基数据库中的记录结构应该完全匹配,类的每个域对应表的每一列。

  一般来说,活动记录包括如下一些方法:

  1、由数据行构造一个活动记录实例;

  2、为将来对表的插入构造一个新的实例;

  3、用静态查找方法来包装常用的SQL查询和返回活动记录;

  4、更新数据库并将活动记录中的数据插入数据库;

  5、获取或设置域;

  6、实现部分业务逻辑。

  【活动记录的优点和缺点】

  优点:

  1、简单,容易创建并且容易理解。

  2、在使用事务脚本时,减少代码复制。

  3、可以在改变数据库结构时不改变领域逻辑。

  4、基于单个活动记录的派生和测试验证会很有效。

  缺点:

  1、没有隐藏关系数据库的存在。

  2、仅当活动记录对象和数据库中表直接对应时,活动记录才会有效。

  3、要求对象的设计和数据库的设计紧耦合,这使得项目中的进一步重构很困难

  【活动记录与其它模式】

  数据源架构模式之行数据入口:活动记录与行数据入口十分类似。二者的主要差别是行数据入口 仅有数据库访问而活动记录既有数据源逻辑又有领域逻辑。

  【活动记录的PHP示例】

  
*/ 
private $_order_id;  
/**  
* 客户ID  
* @var   
*/ 
private $_customer_id;  
/**  
* 定单金额  
* @var   
*/ 
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, " ";  
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);  
}  
}  
Client::main();  
?>


同前面的文章一样,这仅仅是一个活动记录的示例,关于活动记录模式的应用,可以查看Yii框架中的DB类,在其源码中有一个CActiveRecord抽象类,从这里可以看到活动记录模式的应用

另外,如果从事务脚本中创建活动记录,一般是首先将表包装为入口,接着开始行为迁移,使表深化成为活动记录。

对于活动记录中的域的访问和设置可以如yii框架一样,使用魔术方法__set方法和__get方法。


数据源架构模式 - 数据映射器

一:数据映射器

关系型数据库用来存储数据和关系,对象则可以处理业务逻辑,所以,要把数据本身和业务逻辑糅杂到一个对象中,我们要么使用 活动记录,要么把两者分开,通过数据映射器把两者关联起来。

数据映射器是分离内存对象和数据库的中间软件层,下面这个时序图描述了这个中间软件层的概念:

image


在这个时序图中,我们还看到一个概念,映射器需能够获取领域对象(在这个例子中,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
    {
        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(ds.Tables[0]).FirstOrDefault();
                if(user == null)
                {
                    return null;
                }
               
                user = Load(user);
                return user;
            }
           
            return user;
        }
       
        public List FindList(string name)
        {
            // SELECT * FROM USER WHERE NAME LIKE NAME
            List users = null;
            return LoadAll(users);
        }
       
        public void Update(User user)
        {
            // UPDATE USER SET ....
        }
    }
   
    public abstract class AbstractMapper where T : BaseMode
    {
        // 这里的问题是,随着对象消失,loadedMap就被回收
        protected Dictionary loadedMap = new Dictionary();
       
        protected T Load(T t)
        {
            if(loadedMap.ContainsKey(t.Id) )
            {
                return loadedMap[t.Id];
            }
            else
            {
                loadedMap.Add(t.Id, t);
                return t;
            }
        }
       
        protected List LoadAll(List 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];
            }
            else
            {
                return null;
            }
        }
    }
       

上面是一个简单的映射器,它具备了 标识映射 功能。由于有标识映射,所以我们运行这段代码得到的结果是:

http://images.cnitblog.com/blog/123061/201405/201103334967754.png


回归本问实质,问题:什么叫 “数据映射”

其实,这个问题很关键,

UserMap 通过 Find 方法,将数据库记录变成了一个 User 对象,这就叫 “数据映射”,但是,真正起到核心作用的是 user = DataTableHelper.ToList(ds.Tables[0]).FirstOrDefault();  这行代码。更进一步的,DataTableHelper.ToList 这个方法完成了 数据映射 功能。

那么,DataTableHelper.ToList 方法具体干了什么事情,实际上,无非就是根据属性名去获取 DataTable 的字段值。这是一种简便的方法,或者说,在很多业务不复杂的场景下,这也许是个好办法,但是,因为业务往往是复杂的,所以实际情况下,我们使用这个方法的情况并不是很多,大多数情况下,我们需要像这样编码来完成映射:

someone.Name = Convert.ToString(row["Name"])

不要怀疑,上面这行代码,就叫数据映射,任何高大上的概念,实际上就是那条你写了很多遍的代码。

1.1 EntityFramework 中的数据映射

这是一个典型的 EF 的数据映射类,

public class CourseMap : EntityTypeConfiguration
{
    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,简化的数据映射器

其实就是 DataTable To List 了。如果你在用 EF 或者 NHibernate 这样的框架,那么,就用它们提供的映射器好了(严格来说,你不是在使用它们的映射器。因为这些框架本身才是在使用自己的映射器,我们只是在配置映射器所要的数据和关系而已,有时候,这些配置是在配置文件中,有时候是在字段或属性上加 Attribute,有时候则是简单但庞大的单行代码)。我们当然也可以创建自己的 标准的 映射器,Tim McCarthy 在 《领域驱动设计 C# 2008 实现》 中就实现了这样的映射器。但是,EF 和 NHibernate  固然很好,但是很多时候我们还是不得不使用 手写SQL,因为:

1:EF 和 NHibernate 是需要学习成本的,这代表者团队培训成本高,且易出错的;

2:不应放弃 手写SQL 的高效性。

热门栏目