Top Banner
87

Database Design Patterns

May 24, 2015

Download

Technology

Hugo Hamon

This session introduces most well known design patterns to build PHP classes and objects that need to store and fetch data from a relational databases. The session will describe the difference between of the Active Record, the Table and Row Data Gateway and the Data Mapper pattern. We will also examine some technical advantages and drawbacks of these implementations. This talk will expose some of the best PHP tools, which ease database interactions and are built on top of these patterns.
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: Database Design Patterns
Page 2: Database Design Patterns

Enterprise Database Design Patterns in PHP

Hugo Hamon – OSIDays 2011

Page 3: Database Design Patterns

By Martin Fowler §  Table Module §  Transaction Script §  Row Data Gateway §  Table Data Gateway §  Active Record §  Data Mapper §  Unit of Work §  Identity Map §  Data Transfer Object §  …

Page 4: Database Design Patterns

Table Data Gateway

Page 5: Database Design Patterns

« An object that acts as a Gateway to

a database table. One instance

handles all the rows in the table. » Martin Fowler  

Page 6: Database Design Patterns

Same as Data Access Object

Martin Fowler  

Page 7: Database Design Patterns

CRUD

Page 8: Database Design Patterns

$table = new OrderGateway(new Connection('...'));

$table->insert('XX123456789', 358.80, 'unpaid');

$table->update(42, 'XX123456789', 358.80, 'paid');

$table->delete(42);

Page 9: Database Design Patterns

class OrderGateway

{

private $conn;

public function __construct(Connection $conn)

{

$this->conn = $conn;

}

}

Page 10: Database Design Patterns

class OrderGateway

{

public function insert($reference, $amount, $status)

{

$query = 'INSERT INTO orders (reference, amount, status) VALUES (?, ?, ?)';

$data = array($reference, $amount, $status);

$this->conn->executeQuery($query, $data);

return $this->conn->lastInsertId();

}

}

Page 11: Database Design Patterns

class OrderGateway

{

public function update($pk, $ref, $amount, $status)

{

$query = 'UPDATE orders SET reference = ?, amount = ?, status = ? WHERE id = ?';

$data = array($ref, $amount, $status, $pk);

return $this->conn->executeQuery($query, $data);

}

}

Page 12: Database Design Patterns

class OrderGateway

{

public function delete($pk)

{

return $this->conn->executeQuery(

'DELETE FROM orders WHERE id = ?',

array($pk)

);

}

}

Page 13: Database Design Patterns

Finders

Page 14: Database Design Patterns

$orders = $table->findAll();

$orders = $table->findPaidOrders();

$orders = $table->findUnpaidOrders();

$orders = $table->findBy(array(

'status' => 'paid',

'amount' => 250.00

));

$order = $table->find(42);

$order = $table->findOneBy(array('reference' => '...'));

Page 15: Database Design Patterns

class OrderGateway

{

public function findAll()

{

$query = 'SELECT * FROM orders';

return $this->conn->fetchAll($query);

}

public function find($pk)

{

$rs = $this->conn->findBy(array('id' => $pk));

return 1 === count($rs) ? $rs[0] : false;

}

}

Page 16: Database Design Patterns

public function findBy(array $criteria)

{

$where = array();

foreach ($criteria as $field => $value) {

$where[] = sprintf('%s = ?');

}

$q = sprintf(

'SELECT * FROM orders WHERE %s',

implode(' AND ', $where)

);

return $this->conn->fetchAll($q, array_values($criteria));

}

Page 17: Database Design Patterns

public function findPaidOrders()

{

return $this->findBy(array('status' => 'paid'));

}

public function findUnpaidOrders()

{

return $this->findBy(array('status' => 'unpaid'));

}

Page 18: Database Design Patterns

When to use it?

Page 19: Database Design Patterns

Row Data Gateway

Page 20: Database Design Patterns

« An object that acts as a Gateway to

a single record in a data source. There

is one instance per row. » Martin Fowler  

Page 21: Database Design Patterns

CRUD

Page 22: Database Design Patterns

class Order

{

private $id;

private $reference;

private $amount;

private $vat;

private $total;

private $createdAt;

// Getters and setters for each property

// ...

}

Page 23: Database Design Patterns

$conn = new Connection('...');

$order = new OrderGateway();

$order->setReference('XX12345678');

$order->setAmount(300.00);

$order->setVat(58.80);

$order->setTotal(358.80);

$order->setCreatedAt(new DateTime());

$order->insert($conn);

Page 24: Database Design Patterns

class OrderGateway

{

public function insert(Connection $conn)

{

$query = 'INSERT INTO orders (reference, amount, vat, total, created_at) VALUES (?, ?, ?, ?, ?)';

$data = array(

$this->reference, $this->amount, $this->vat,

$this->total, $this->createdAt->format('Y-m-d H:i:s')

);

$conn->executeQuery($query, $data);

$this->id = $conn->lastInsertId();

}

}

Page 25: Database Design Patterns

Finders

Page 26: Database Design Patterns

OrderFinder::setConnection($conn);

$order = OrderFinder::findByReference('XX12345678');

echo sprintf('%01.2f euros', $order->getTotal());

Page 27: Database Design Patterns

class OrderFinder

{

static public function findByReference($reference)

{

$query = 'SELECT * FROM orders WHERE reference = ?';

$rs = static::getConnection()

->fetchSingle($query, array($reference))

;

return $rs ? OrderGateway::load($rs) : false;

}

}

Page 28: Database Design Patterns

class OrderGateway

{

static public function load(array $rs)

{

$order = new OrderGateway($rs['id']);

$order->setReference($rs['reference']);

$order->setAmount($rs['amount']);

$order->setVat($rs['vat']);

$order->setTotal($rs['total']);

$order->setCreatedAt(new DateTime($rs['created_at']));

return $order;

}

}

Page 29: Database Design Patterns

OrderFinder::setConnection($conn);

$orders = OrderFinder::findMostExpensiveOrders(10);

foreach ($orders as $order) {

echo $order->getReference(), "\n";

echo sprintf('%01.2f euros', $order->getTotal()), "\n";

echo "\n-----\n";

}

Page 30: Database Design Patterns

class OrderFinder

{

static public function findMostExpensiveOrders($limit)

{

$orders = array();

$query = 'SELECT * FROM orders ORDER BY total DESC LIMIT ?';

$rs = static::getConnection()->fetchAll($query, array($limit));

foreach ($rs as $data) {

$orders[] = OrderGateway::load($data);

}

return $orders;

}

}

Page 31: Database Design Patterns

When to use it?

Page 32: Database Design Patterns

Active Record

Page 33: Database Design Patterns

« An object that wraps a row in a

database table or view, encapsulates

the database access, and adds

domain logic on that data. » Martin Fowler  

Page 34: Database Design Patterns

Active Record =

Row Data Gateway + Business Logic

Page 35: Database Design Patterns

Active Record =

Data + Behaviors

Page 36: Database Design Patterns

Active Record =

Properties + Methods

Page 37: Database Design Patterns

class Order

{

private $id;

private $reference;

private $amount;

private $vat;

private $vatRate;

private $total;

private $createdAt;

private $status;

private $isPaid;

// Getters and setters for each property

// ...

}

Page 38: Database Design Patterns

class Order

{

public function __construct($id = null)

{

if (null !== $id) {

$this->id = $id;

}

$this->vatRate = 0.00;

$this->vat = 0.00;

$this->amount = 0.00;

$this->total = 0.00;

$this->isPaid = false;

$this->status = 'processing';

$this->createdAt = new DateTime();

}

}

Page 39: Database Design Patterns

$conn = new Connection('...'); $order = new Order(); $order->setReference('XX12345678'); $order->setAmount(300.00); $order->setVatRate(0.196); $order->applyDiscount(20.00); $order->updateTotal(); $order->save($conn);

Page 40: Database Design Patterns

class Order { public function applyDiscount($discount) { $this->amount -= $discount; } public function updateTotal() { if ($this->vatRate) { $this->vat = $this->amount * $this->vatRate; } $this->total = $this->amount + $this->vat; } }

Page 41: Database Design Patterns

class Order

{

public function isPaid()

{

return $this->isPaid;

}

public function setPaid()

{

$this->isPaid = true;

}

}

Page 42: Database Design Patterns

class Order

{

public function isReadyForShipment()

{

return $this->isPaid() && 'complete' == $this->status;

}

public function ship($address)

{

$this->doShipment($address);

$this->status = 'shipped';

}

}

Page 43: Database Design Patterns

class OrderController

{

public function confirmAction($reference)

{

$conn = $this->getDatabaseConnection();

$order = ...;

$order->setPaid();

$order->save($conn);

if ($order->isReadyForShipment()) {

$order->ship();

return $this->view->render('ship.php', array('order' => $order));

}

return $this->view->render('pending.php', array('order' => $order));

}

}

Page 44: Database Design Patterns

Refactoring

Page 45: Database Design Patterns

abstract class ActiveRecord

{

protected $fields = array();

abstract public function getTableName();

public function save(Connection $conn)

{

// insert or update $fields in the database

}

public function delete(Connection $conn)

{

// delete the object from the database

}

}

Page 46: Database Design Patterns

class Order extends ActiveRecord

{

private $amount;

abstract public function getTableName()

{

return 'tbl_orders';

}

public function setAmount($amount)

{

$this->amount = $amount;

$this->fields['amount'] = $amount;

}

}

Page 47: Database Design Patterns

When to use it?

Page 48: Database Design Patterns

Data Mapper

Page 49: Database Design Patterns

« A layer of Mappers that moves data

between objects and a database

while keeping them independent of

each other and the mapper itself. » Martin Fowler  

Page 50: Database Design Patterns

« Man in the Middle »

Page 51: Database Design Patterns

http://martinfowler.com  

Page 52: Database Design Patterns

class OrderMapper

{

private $conn;

public function __construct(Connection $conn) {

$this->conn = $conn;

}

public function store(Order $order) {

// Execute the query to persist the object to the DB

}

public function remove(Order $order) {

// Executes the query to remove the object to the DB

}

}

Page 53: Database Design Patterns

$order = new Order();

$order->setReference('XX12345678');

$order->setAmount(300.00);

$order->setVatRate(0.196);

$order->updateTotal();

$conn = new Connection('mysql:host=localhost ...');

$mapper = new OrderMapper($conn);

$mapper->store($order);

Page 54: Database Design Patterns

class OrderMapper

{

public function findAll()

{

$objects = array();

$query = 'SELECT id, reference, vat ... FROM orders';

foreach ($this->conn->fetchAll($query) as $data) {

$object = new Order($data['id']);

$object->load($data);

$objects[] = $object;

}

return $objects;

}

}

Page 55: Database Design Patterns

class OrderMapper

{

public function find($pk)

{

$query = 'SELECT id, vat ... FROM orders WHERE id = ?';

$object = false;

if (false !== $data = conn->fetch($query, array($pk))) {

$object = new Order($data['id']);

$object->load($data);

}

return $object;

}

}

Page 56: Database Design Patterns

$conn = new Connection('mysql:host=localhost ...');

$mapper = new OrderMapper($conn);

$order = $mapper->find(42);

$order->setAmount(399.00);

$order->updateTotal();

$mapper->store($order);

Page 57: Database Design Patterns

Unit testing

Page 58: Database Design Patterns

class OrderTest extends PHPUnit_Framework_TestCase

{

public function testUpdateTotal()

{

$order = new Order();

$order->setAmount(299.00);

$order->setVatRate(0.196);

$order->updateTotal();

$this->assertEquals(58.60, $order->getVat());

$this->assertEquals(357.60, $order->getTotal());

}

}

Page 59: Database Design Patterns

When to use it?

Page 60: Database Design Patterns

IdentityMap

Page 61: Database Design Patterns

« Ensures that each object gets

loaded only once by keeping every

loaded object in a map. »

Martin Fowler  

Page 62: Database Design Patterns

$conn = new Connection('mysql:host=localhost ...');

$mapper = new OrderMapper($conn);

$orderA = $mapper->find(42);

$orderB = $mapper->find(42);

$orderC = $mapper->find(42);

// 3 SQL queries for getting the same object

Page 63: Database Design Patterns

The solution

Page 64: Database Design Patterns

class IdentityMap implements IdentityMapInterface

{

private $entities;

public function fetch($class, $pk)

{

$key = $this->getKey($class, $pk);

if (isset($this->entities[$key])) {

return $this->entities[$key];

}

return false;

}

}

Page 65: Database Design Patterns

class IdentityMap implements IdentityMapInterface

{

public function store(ValueObjectInterface $entity)

{

$key = $this->getKey($class, $entity->getId());

$this->entities[$key] = $entity;

}

private function getKey($class, $pk)

{

return $class.'-'.$pk;

}

}

Page 66: Database Design Patterns

class Order implements ValueObjectInterface

{

private $id;

private $reference;

private $amount;

// ...

public function getId()

{

return $this->id;

}

}

Page 67: Database Design Patterns

class OrderMapper extends DatabaseMapper

{

private $map;

public function __construct(IdentityMap $map, ...)

{

parent::__construct($conn);

$this->map = $map;

}

}

Page 68: Database Design Patterns

class OrderMapper extends DatabaseMapper

{

public function store(Order $order)

{

parent::store($order);

$this->map->store('Order', $object);

}

}

Page 69: Database Design Patterns

class OrderMapper extends DatabaseMapper

{

public function find($pk)

{

if (false !== $object = $this->map->fetch($pk)) {

return $object;

}

if (false !== $object = parent::find($pk)) {

$this->map->store('Order', $object);

}

return $object;

}

}

Page 70: Database Design Patterns

$conn = new Connection('mysql:host=localhost ...');

$mapper = new OrderMapper(new IdentityMap(), $conn);

$orderA = $mapper->find(42); // Query

$orderB = $mapper->find(42); // No query

$orderB->setAmount(299.00);

$orderB->setVatRate(0.196);

$orderB->updateTotal();

$mapper->store($orderB);

$orderC = $mapper->find(42); // No query

Page 71: Database Design Patterns

Query Object

Page 72: Database Design Patterns

$query = Query::create()

->select(array('id', 'reference', 'amount', 'status'))

->from('orders')

->where(Criteria::equals('status', 'paid'))

->where(Criteria::greaterThan('amount', 2000))

->where(Criteria::like('reference', 'XX123%'))

->orderBy('amount', 'desc')

->getSql()

;

// SELECT id, reference, amount, status

// WHERE status = ? AND amount > ? AND reference LIKE ?

// ORDER BY amount DESC

Page 73: Database Design Patterns

class Criteria

{

private $field;

private $operator;

private $parameters;

public function __construct($field, $operator, $value)

{

$this->field = $field;

$this->operator = $operator;

$this->parameters[] = $value;

}

}

Page 74: Database Design Patterns

class Criteria

{

static public function equal($field, $value, $vars)

{

return new Criteria($field, '=', $vars);

}

static public function notEqual($field, $value, $vars)

{

return new Criteria($field, '<>', $vars);

}

}

Page 75: Database Design Patterns

Custom Queries

Page 76: Database Design Patterns

class OrderQuery extends Query

{

public function filterByPriority($amount)

{

return $this

->where(Criteria::equal('status', 'paid'))

->where(Criteria::greaterThan('amount', $amount))

;

}

public function filterByReference($like)

{

return $this->where(Criteria::like('reference', $like));

}

}

Page 77: Database Design Patterns

$query = OrderQuery::create()

->filterByPriority(2000)

->filterByReference('%567%')

->orderByAmount('DESC')

->getSql()

;

Page 78: Database Design Patterns

ORM Tools

Page 79: Database Design Patterns

Zend_DB

Doctrine 1.2

Doctrine 2.x

Propel

Pomm

Page 80: Database Design Patterns

Quizz

Can you guess the patterns?

Page 81: Database Design Patterns

$table = new Author(); // New empty row $row = $table->createRow(); // Insert a new row $row->firstName = 'Jules'; $row->lastName = 'Verne'; $row->save();

Page 82: Database Design Patterns

$pax1 = new Passenger('Hugo Hamon', '7B'); $pax2 = new Passenger('John Smith', '3A'); $aircraft = new Plane(); $aircraft->setCapacity(120); $aircraft->addPassenger($pax1); $aircraft->addPassenger($pax2); $aircraft->save(); $pax2->changeSeat('2C'); $pax2->save(); $aircraft->isAvailableSeat('3A') ? 'Yes' : 'No';

Page 83: Database Design Patterns

$post = new BlogPost(); $post->setTitle('My First Blog Post'); $post->setBody('Some content...'); $author = new Author(); $author->setName('Hugo Hamon'); $author->addPost($post); $em->persist($user); $em->persist($post); $em->flush();

Page 84: Database Design Patterns

$data = array( 'first_name' => 'Jules', 'last_name' => 'Vernes', ); $table = new AuthorTable(); $table->insert($data);

Page 85: Database Design Patterns

Conclusion

Page 86: Database Design Patterns

By Martin Fowler §  Table Module §  Transaction Script §  Row Data Gateway §  Table Data Gateway §  Active Record §  Data Mapper §  Unit of Work §  Identity Map §  Data Transfer Object §  …

Page 87: Database Design Patterns

Ques&ons?  

92-98, boulevard Victor Hugo

92 115 Clichy Cedex

[email protected] (+33 (0)1 40 99 82 11)

sensiolabs.com - symfony.com – trainings.sensiolabs.com