orm 系列 之 常用設(shè)計(jì)模式

orm

本文是orm系列的第一篇,內(nèi)容來(lái)自github上的一個(gè)Markdown,清晰的講述了一些數(shù)據(jù)庫(kù)設(shè)計(jì)上常用的設(shè)計(jì)模式,并且闡述了orm是什么?

數(shù)據(jù)庫(kù)


主要分一下幾部分:

  • 數(shù)據(jù)庫(kù)設(shè)計(jì)模式
  • 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.

數(shù)據(jù)庫(kù)設(shè)計(jì)模式

  • Row Data Gateway
  • Table Data Gateway
  • Active Record
  • Data Mapper
  • Identity Map
  • etc.

定義和插圖來(lái)自 Catalog of Patterns of Enterprise Application Architecture

作者Martin Fowler.

Don't forget his name! Read his books!


Row Data Gateway

Row Data Gateway

一個(gè)對(duì)象扮演的角色就像是數(shù)據(jù)庫(kù)中單行記錄的網(wǎng)關(guān)(Gateway)

每個(gè)對(duì)象對(duì)應(yīng)一行

!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);

底層實(shí)現(xiàn)

!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


扮演著數(shù)據(jù)庫(kù)表的網(wǎng)關(guān)角色(Gateway

一個(gè)對(duì)象處理了表中所有的行記錄

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實(shí)現(xiàn)了CURD操作

讀操作會(huì)比較負(fù)責(zé),是一系列Finders


實(shí)現(xiàn)

!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 %');

使用魔術(shù)方法 __call() 來(lái)實(shí)現(xiàn)這些魔術(shù)般的finders
http://www.php.net/manual/en/language.oop5.overloading.php#object.call.


Active Record


封裝了表中的單行記錄,除此之外加上了領(lǐng)域邏輯

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

將內(nèi)存中的數(shù)據(jù)映射到數(shù)據(jù)庫(kù)中,同時(shí)保持著彼此之間的解耦

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


保證每個(gè)對(duì)象只會(huì)從數(shù)據(jù)庫(kù)中加載一次,一旦加載進(jìn)來(lái),將其保存到一個(gè)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) 是標(biāo)準(zhǔn)的操作數(shù)據(jù)的api,不管你使用哪個(gè)數(shù)據(jù)庫(kù),都是一樣的

Data Source Name (DSN)則是區(qū)分到底在使用哪種數(shù)據(jù)庫(kù)

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


對(duì)象之間的關(guān)系

介紹3種關(guān)系

  • One-To-One;
  • One-To-Many;
  • Many-To-Many.

ORM一般認(rèn)為是實(shí)現(xiàn)上面各種設(shè)計(jì)模式的一個(gè)工具,并且能很方便的處理對(duì)象之間的關(guān)系


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

可以通過(guò)id進(jìn)行區(qū)分的對(duì)象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

直接通過(guò)值來(lái)分區(qū),無(wú)id的對(duì)象,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協(xié)調(diào)了領(lǐng)域?qū)ο蠛蛿?shù)據(jù)映射層的關(guān)系,扮演著內(nèi)存中領(lǐng)域?qū)ο蠹希?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

客戶端通過(guò)構(gòu)造聲明式的query specifications去Repository進(jìn)行查詢。

對(duì)象可以被添加進(jìn)Repository,同樣的也能從Repository中移除,從這個(gè)角度講,Repository有點(diǎn)類似于集合的概念,其內(nèi)部封裝了對(duì)象和數(shù)據(jù)庫(kù)記錄之間的映射關(guān)系,Repository提供了persistence的一個(gè)更面向?qū)ο蟮囊暯恰?/p>

Repository同時(shí)很好的解決了領(lǐng)域?qū)ο蠛蛿?shù)據(jù)映射層之間的耦合關(guān)系,充分的分離的關(guān)注點(diǎn),領(lǐng)域?qū)ο蠛蛿?shù)據(jù)映射層可以獨(dú)自的開發(fā),演化。


The Specification Pattern

Specification Pattern

Specification pattern可以將業(yè)務(wù)規(guī)則建模為獨(dú)立的對(duì)象,其主要思想是關(guān)注一個(gè)對(duì)象的問(wèn)題,可以通過(guò)isSatisfiedBy()來(lái)回答:

!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

在業(yè)務(wù)層復(fù)用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的第一篇,你的鼓勵(lì)是我繼續(xù)寫下去的動(dòng)力,期待我們共同進(jìn)步。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評(píng)論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,489評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,510評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,866評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評(píng)論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,036評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,585評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,331評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,536評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,754評(píng)論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,273評(píng)論 3 399
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,505評(píng)論 2 379

推薦閱讀更多精彩內(nèi)容

  • PLEASE READ THE FOLLOWING APPLE DEVELOPER PROGRAM LICENSE...
    念念不忘的閱讀 13,519評(píng)論 5 6
  • 1、英語(yǔ)跟讀,公主日記45分鐘,代溝的存在仿佛是必然的,而一個(gè)人習(xí)慣了一種生活方式就會(huì)遺忘掉還有其他種方式,甚至自...
    長(zhǎng)海1994閱讀 126評(píng)論 0 0
  • 寫給星星的你 ——致小星星 你眼里的光一閃一閃 穿過(guò)黑夜連成線 聚成的模樣璀璨 照亮前路悠長(zhǎng)漫漫 ...
    山谷里的余聲閱讀 906評(píng)論 0 3
  • 行走在流言蜚語(yǔ)的世界中,我心臟早已停止跳動(dòng),我只是拖著一副尸體行走著,等待著腐朽,喧鬧聲天真的想要喚醒我心跳,...
    冬日暮熊閱讀 316評(píng)論 0 0