数据源架构模式 表入口 行入口 活动记录 数据映射器
发布:smiling 来源: PHP粉丝网 添加日期:2018-10-28 20:04:06 浏览: 评论:0
数据源架构模式 - 表入口模式
表入口模式充当数据库表访问入口的对象,一个实例处理表中的所有行。
可以理解为对之前分散在各个页面的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();
- //phpfensi.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);
- } //phpfensi.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];
- } //phpfensi.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 的高效性。
Tags: 数据源架构 表入口
推荐文章
热门文章
最新评论文章
- 写给考虑创业的年轻程序员(10)
- PHP新手上路(一)(7)
- 惹恼程序员的十件事(5)
- PHP邮件发送例子,已测试成功(5)
- 致初学者:PHP比ASP优秀的七个理由(4)
- PHP会被淘汰吗?(4)
- PHP新手上路(四)(4)
- 如何去学习PHP?(2)
- 简单入门级php分页代码(2)
- php中邮箱email 电话等格式的验证(2)