orm 系列 之 常用设计模式

orm

本文是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 or collections;
  • 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.

http://www.php.net/manual/en/intro.pdo.php


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-One

Code Snippet

!php
$profile = $banana->getProfile();

One-To-Many (1-N)

Code Snippet

!php
$bananas = $bananaTree->getBananas();

Many-To-Many (N-N)

Many-To-Many

Code 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 Pattern

Repository协调了领域对象和数据映射层的关系,扮演着内存中领域对象集合( 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 Pattern

Specification 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的第一篇,你的鼓励是我继续写下去的动力,期待我们共同进步。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • PLEASE READ THE FOLLOWING APPLE DEVELOPER PROGRAM LICENSE...
    念念不忘的阅读 13,304评论 5 6
  • 1、英语跟读,公主日记45分钟,代沟的存在仿佛是必然的,而一个人习惯了一种生活方式就会遗忘掉还有其他种方式,甚至自...
    长海1994阅读 96评论 0 0
  • 写给星星的你 ——致小星星 你眼里的光一闪一闪 穿过黑夜连成线 聚成的模样璀璨 照亮前路悠长漫漫 ...
    山谷里的余声阅读 857评论 0 3
  • 行走在流言蜚语的世界中,我心脏早已停止跳动,我只是拖着一副尸体行走着,等待着腐朽,喧闹声天真的想要唤醒我心跳,...
    冬日暮熊阅读 289评论 0 0