本文是orm系列的第一篇,内容来自github上的一个Markdown,清晰的讲述了一些数据库设计上常用的设计模式,并且阐述了orm是什么?
数据库
主要分一下几部分:
- 数据库设计模式
- DAL(Data Access Layer)
- ORM(Object Relational Mapping)
- 存在的组件
- A Note About Domain-Driven Design
Quick note
In our context, a database is seen as a server hosting:
- a set of
records
; - organised through
tables
orcollections
; - grouped by
databases
.
数据库设计模式
- Row Data Gateway
- Table Data Gateway
- Active Record
- Data Mapper
- Identity Map
- etc.
定义和插图来自 Catalog of Patterns of Enterprise Application Architecture
作者Martin Fowler.
Don't forget his name! Read his books!
Row Data Gateway
Row Data Gateway一个对象扮演的角色就像是数据库中单行记录的网关(Gateway)
每个对象对应一行
!php
// This is the implementation of `BananaGateway`
class Banana
{
private $id;
private $name;
public function getId()
{
return $this->id;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
}
使用
!php
$con = new Connection('...');
$banana = new Banana();
$banana->setName('Super Banana');
// Save the banana
$banana->insert($con);
// Update it
$banana->setName('New name for my banana');
$banana->update($con);
// Delete it
$banana->delete($con);
底层实现
!php
public function insert(Connection $con)
{
// Prepared statement
$stmt = $this->con->prepare('INSERT INTO bananas VALUES (:name)');
$stmt->bindValue(':name', $name);
$stmt->execute();
// Set the id for this banana
//
// It becomes easy to know whether the banana is new or not,
// you just need to check if id is defined.
$this->id = $this->con->lastInsertId();
}
Table Data Gateway
扮演着数据库表的网关角色(Gateway)
一个对象处理了表中所有的行记录
It's a Data Access Object.
!php
$table = new BananaGateway(new Connection('...'));
// Insert a new record
$id = $table->insert('My favorite banana');
// Update it
$table->update($id, 'THE banana');
// Delete it
$table->delete($id);
CRUD
DAO实现了CURD操作
读操作会比较负责,是一系列Finders
实现
!php
class BananaGateway
{
private $con;
public function __construct(Connection $con)
{
$this->con = $con;
}
public function insert($name) {}
public function update($id, $name) {}
public function delete($id);
}
insert操作
!php
/**
* @param string $name The name of the banana you want to create
*
* @return int The id of the banana
*/
public function insert($name)
{
// Prepared statement
$stmt = $this->con->prepare('INSERT INTO bananas VALUES (:name)');
$stmt->bindValue(':name', $name);
$stmt->execute();
return $this->con->lastInsertId();
}
update操作
!php
/**
* @param int $id The id of the banana to update
* @param string $name The new name of the banana
*
* @return bool Returns `true` on success, `false` otherwise
*/
public function update($id, $name)
{
$stmt = $this->con->prepare(<<<SQL
UPDATE bananas
SET name = :name
WHERE id = :id
SQL
);
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
return $stmt->execute();
}
delete操作
!php
/**
* @param int $id The id of the banana to delete
*
* @return bool Returns `true` on success, `false` otherwise
*/
public function delete($id)
{
$stmt = $this->con->prepare('DELETE FROM bananas WHERE id = :id');
$stmt->bindValue(':id', $id);
return $stmt->execute();
}
Finder方法
!php
// Retrieve all bananas
$bananas = $table->findAll();
// Find bananas by name matching 'THE %'
$bananas = $table->findByName('THE %');
// Retrieve a given banana using its id
$banana = $table->find(123);
// Find one banana by name matching 'THE %'
$banana = $table->findOneByName('THE %');
使用魔术方法
__call()
来实现这些魔术般的finders
http://www.php.net/manual/en/language.oop5.overloading.php#object.call.
Active Record
封装了表中的单行记录,除此之外加上了领域逻辑
Active Record = Row Data Gateway + Domain Logic
!php
$con = new Connection('...');
$banana = new Banana();
$banana->setName('Another banana');
$banana->save($con);
// Call a method that is part of the domain logic
// What can a banana do anyway?
$banana->grow();
// Smart `save()` method
// use `isNew()` under the hood
$banana->save($con);
!php
class Banana
{
private $height = 1;
public function grow()
{
$this->height++;
}
public function save(Connection $con)
{
if ($this->isNew()) {
// issue an INSERT query
} else {
// issue an UPDATE query
}
}
public function isNew()
{
// Yoda style
return null === $this->id;
}
}
Data Mapper
Data Mapper将内存中的数据映射到数据库中,同时保持着彼此之间的解耦
Sort of "Man in the Middle".
!php
class BananaMapper
{
private $con;
public function __construct(Connection $con)
{
$this->con = $con;
}
public function persist(Banana $banana)
{
// code to save the banana
}
public function remove(Banana $banana)
{
// code to delete the banana
}
}
使用
!php
$banana = new Banana();
$banana->setName('Fantastic Banana');
$con = new Connection('...');
$mapper = new BananaMapper($con);
Persist = Save or Update
!php
$mapper->persist($banana);
Remove
!php
$mapper->remove($banana);
Identity Map
保证每个对象只会从数据库中加载一次,一旦加载进来,将其保存到一个map中
!php
class Finder
{
private $identityMap = [];
public function find($id)
{
if (!isset($this->identityMap[$id])) {
// fetch the object for the given id
$this->identityMap[$id] = ...;
}
return $this->identityMap[$id];
}
}
Data Access Layer
Data Access Layer / Data Source Name
D**ata Access Layer (DAL) 是标准的操作数据的api,不管你使用哪个数据库,都是一样的
Data Source Name (DSN)则是区分到底在使用哪种数据库
PHP Data Object (PDO)
A DSN in PHP looks like: <database>:host=<host>;dbname=<dbname>
where:
-
<database>
can be:mysql
,sqlite
,pgsql
, etc; -
<host>
is the IP address of the database server (e.g.localhost
); -
<dbname>
is your database name.
PDO usage
!php
$dsn = 'mysql:host=localhost;dbname=test';
$con = new PDO($dsn, $user, $password);
// Prepared statement
$stmt = $con->prepare($query);
$stmt->execute();
Looks like the Connection
class you used before, right?
!php
class Connection extends PDO
{
}
Usage
!php
$con = new Connection($dsn, $user, $password);
Refactoring
!php
class Connection extends PDO
{
/**
* @param string $query
* @param array $parameters
*
* @return bool Returns `true` on success, `false` otherwise
*/
public function executeQuery($query, array $parameters = [])
{
$stmt = $this->prepare($query);
foreach ($parameters as $name => $value) {
$stmt->bindValue(':' . $name, $value);
}
return $stmt->execute();
}
}
Usage
!php
/**
* @param int $id The id of the banana to update
* @param string $name The new name of the banana
*
* @return bool Returns `true` on success, `false` otherwise
*/
public function update($id, $name)
{
$query = 'UPDATE bananas SET name = :name WHERE id = :id';
return $this->con->executeQuery($query, [
'id' => $id,
'name' => $name,
]);
}
Object Relational Mapping
对象之间的关系
介绍3种关系
- One-To-One;
- One-To-Many;
- Many-To-Many.
ORM一般认为是实现上面各种设计模式的一个工具,并且能很方便的处理对象之间的关系
One-To-One (1-1)
One-To-OneCode Snippet
!php
$profile = $banana->getProfile();
One-To-Many (1-N)
Code Snippet
!php
$bananas = $bananaTree->getBananas();
Many-To-Many (N-N)
Many-To-ManyCode Snippet
!php
$roles = [];
foreach ($banana->getBananaRoles() as $bananaRole) {
$roles[] = $bananaRole->getRole();
}
// Or, better:
$roles = $banana->getRoles();
存在的组件
Propel ORM
An ORM that implements the Table Data Gateway and Row Data Gateway
patterns, often seen as an Active Record approach.
Documentation: www.propelorm.org.
Doctrine2 ORM
An ORM that implements the Data Mapper pattern.
Documentation: www.doctrine-project.org.
A Note About Domain-Driven Design
Entities
可以通过id进行区分的对象entity:
!php
class Customer
{
private $id;
private $name;
public function __construct($id, Name $name)
{
$this->id = $id;
$this->name = $name;
}
public function getId()
{
return $this->id;
}
public function getName()
{
return $this->name;
}
}
Value Objects
直接通过值来分区,无id的对象,Value Object:
!php
class Name
{
private $firstName;
private $lastName;
public function __construct($firstName, $lastName)
{
$this->firstName = $firstName;
$this->lastName = $lastName;
}
public function getFirstName()
{
return $this->firstName;
}
public function getLastName()
{
return $this->lastName;
}
}
The Repository Pattern
The Repository PatternRepository协调了领域对象和数据映射层的关系,扮演着内存中领域对象集合( in-memory domain object collection)的角色。
!php
interface CustomerRepository
{
/**
* @return Customer
*/
public function find($customerId);
/**
* @return Customer[]
*/
public function findAll();
public function add(Customer $user);
public function remove(Customer $user);
}
The Repository Pattern
客户端通过构造声明式的query specifications去Repository进行查询。
对象可以被添加进Repository,同样的也能从Repository中移除,从这个角度讲,Repository有点类似于集合的概念,其内部封装了对象和数据库记录之间的映射关系,Repository提供了persistence的一个更面向对象的视角。
Repository同时很好的解决了领域对象和数据映射层之间的耦合关系,充分的分离的关注点,领域对象和数据映射层可以独自的开发,演化。
The Specification Pattern
Specification PatternSpecification pattern可以将业务规则建模为独立的对象,其主要思想是关注一个对象的问题,可以通过isSatisfiedBy()
来回答:
!php
interface CustomerSpecification
{
/**
* @return boolean
*/
public function isSatisfiedBy(Customer $customer);
}
!php
class CustomerIsPremium implements CustomerSpecification
{
/**
* {@inheritDoc}
*/
public function isSatisfiedBy(Customer $customer)
{
// figure out if the customer is indeed premium,
// and return true or false.
}
}
Repository ♥ Specification
A findSatisfying()
method can be added to the CustomerRepository
:
!php
interface CustomerRepository
{
...
/**
* @return Customer[]
*/
public function findSatisfying(CustomerSpecification $specification);
}
Usage
!php
$specification = new CustomerIsPremium();
$customers = $repository->findSatisfying($specification);
Combine Them!
!php
class OrSpecification implements CustomerSpecification
{
public function __construct(
CustomerSpecification $s1,
CustomerSpecification $s2
) {
$this->s1 = $s1;
$this->s2 = $s2;
}
public function isSatisfiedBy(Customer $c)
{
return $this->s1->isSatisfiedBy($c) || $this->s2->isSatisfiedBy($c);
}
}
!php
class AndSpecification implements CustomerSpecification
{
...
public function isSatisfiedBy(Customer $c)
{
return $this->s1->isSatisfiedBy($c) && $this->s2->isSatisfiedBy($c);
}
}
!php
class NotSpecification implements CustomerSpecification
{
public function __construct(CustomerSpecification $s)
{
$this->s = $s;
}
public function isSatisfiedBy(Customer $c)
{
return !$this->s->isSatisfiedBy($c);
}
}
Usage
!php
// Find customers who have ordered exactly three times,
// but who are not premium customers (yet?)
$specification = new AndSpecification(
new CustomerHasOrderedThreeTimes(),
new NotSpecification(
new CustomerIsPremium()
)
);
$customers = $repository->findSatisfying($specification);
Specification For Business Rules
在业务层复用specifications
!php
class AwesomeOfferSender
{
private $specification;
public function __construct(CustomerIsPremium $specification)
{
$this->specification = $specification;
}
public function sendOffersTo(Customer $customer)
{
if ($this->specification->isSatisfiedBy($customer)) {
// send offers
}
}
}
原文地址是:
https://github.com/willdurand-edu/php-slides/blob/master/src/common/09_databases.md
这是orm的第一篇,你的鼓励是我继续写下去的动力,期待我们共同进步。
网友评论