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

orm

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

數(shù)據(jù)庫


主要分一下幾部分:

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

  • 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

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

每個對象對應(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);

底層實現(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ù)庫表的網(wǎng)關(guān)角色(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實現(xiàn)了CURD操作

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


實現(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() 來實現(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ù)庫中,同時保持著彼此之間的解耦

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


保證每個對象只會從數(shù)據(jù)庫中加載一次,一旦加載進(jìn)來,將其保存到一個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,不管你使用哪個數(shù)據(jù)庫,都是一樣的

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

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


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

介紹3種關(guān)系

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

ORM一般認(rèn)為是實現(xiàn)上面各種設(shè)計模式的一個工具,并且能很方便的處理對象之間的關(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

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

直接通過值來分區(qū),無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協(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

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

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

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


The Specification Pattern

Specification Pattern

Specification pattern可以將業(yè)務(wù)規(guī)則建模為獨立的對象,其主要思想是關(guān)注一個對象的問題,可以通過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

在業(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的第一篇,你的鼓勵是我繼續(xù)寫下去的動力,期待我們共同進(jìn)步。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容