数据源架构模式 - 表入口模式
表入口模式充当数据库表访问入口的对象,一个实例处理表中的所有行。
可以理解为对之前分散在各个页面的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 = "select * from $this->table where id=$personId"; echo $sql; return $this->instance->query($sql); } /**其他的一些增删改查操作方法...**/ }
index.php
<?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 "; 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, " <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(); } } 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); } } 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];
}
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 的高效性。
以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索string
, 数据
, 对象
, 代码
, this
模式
excel xml映射数据源、软件架构师报考入口、数据源、odbc数据源配置、jndi数据源配置,以便于您获取更多的相关知识。