Who am I?
Jonathan H. Wage
PHP Developer for over 10 years
Symfony Contributor
Doctrine Contributor
Published Author
Business Owner
Nashville, TN Resident
http://www.twitter.com/jwage
http://www.facebook.com/jwage
I work at
What is OpenSky?
“a social commerce platform”
Based in New York and is a major open
source software advocate
http://www.shopopensky.com
What is Doctrine?
- Open Source PHP Project started in
2006
- Specializes in database functionality
- Database Abstraction Layer (DBAL)
- Database Migrations
- Object Relational Mapper (DBAL)
- MongoDB Object Document Manager (ODM)
- CouchDB Object Document Manager (ODM)
Who is on the team?
• Roman S. Borschel
• Guilherme Blanco
• Benjamin Eberlei
• Bulat Shakirzyanov
• Jonathan H.Wage
Project History
- First commit April 13th 2006
- First stable version finished and Released September
1st 2008
- One of the first ORM implementations for PHP
- 1.0 is First LTS(long term support) release.
Maintained until March 1st 2010
- Integrated with many popular frameworks: Symfony,
Zend Framework, Code Igniter
Database Abstraction Layer
The Doctrine Database Abstraction
Layer (DBAL) is a thin layer on top of
PDO, it offers:
- select, update, delete, transactions
- database schema introspection
- schema management
Download
You can download a standalone
package to get started using the DBAL:
http://www.doctrine-project.org/projects/dbal/download
Autoloader
To use any Doctrine library you must
register an autoloader:
use DoctrineCommonClassLoader;
require '/path/to/doctrine-common/lib/Doctrine/Common/ClassLoader.php';
$classLoader = new ClassLoader('DoctrineDBAL', '/path/to/doctrine-dbal/lib');
$classLoader->register();
Data API
prepare($sql) - Prepare a given sql statement and return the DoctrineDBALDriver
Statement instance.
executeUpdate($sql, array $params) - Executes a prepared statement with the given sql
and parameters and returns the affected rows count.
execute($sql, array $params) - Creates a prepared statement for the given sql and passes
the parameters to the execute method, then returning the statement.
fetchAll($sql, array $params) - Execute the query and fetch all results into an array.
fetchArray($sql, array $params) - Numeric index retrieval of first result row of the given
query.
fetchBoth($sql, array $params) - Both numeric and assoc column name retrieval of the first
result row.
fetchColumn($sql, array $params, $colnum) - Retrieve only the given column of the first
result row.
fetchRow($sql, array $params) - Retrieve assoc row of the first result row.
select($sql, $limit, $offset) - Modify the given query with a limit clause.
delete($tableName, array $identifier) - Delete all rows of a table matching the given
identifier, where keys are column names.
insert($tableName, array $data) - Insert a row into the given table name using the key
value pairs of data.
Very Similar to PDO
$users = $conn->fetchAll('SELECT * FROM users');
Schema Manager
Learn about and modify your database
through the SchemaManager:
$sm = $conn->getSchemaManager();
Try a Method
You can try a method and return true if
the operation was successful:
if ($sm->tryMethod('createTable', 'new_table', $columns, $options)) {
// do something
}
Drop and Create Database
try {
$sm->dropDatabase('test_db');
} catch (Exception $e) {}
$sm->createDatabase('test_db');
Drop and Create Database
A little better! Every drop and create
functionality in the API has a method
that follows the dropAndCreate pattern:
$sm->dropAndCreateDatabase('test_db');
Schema Representation
$platform = $em->getConnection()->getDatabasePlatform();
$schema = new DoctrineDBALSchemaSchema();
$myTable = $schema->createTable("my_table");
$myTable->addColumn("id", "integer", array("unsigned" => true));
$myTable->addColumn("username", "string", array("length" => 32));
$myTable->setPrimaryKey(array("id"));
// get queries to create this schema.
$queries = $schema->toSql($platform);
Array
(
[0] => CREATE TABLE my_table (id INTEGER NOT NULL, username VARCHAR(32)
NOT NULL, PRIMARY KEY("id"))
)
Schema Representation
Array
(
[0] => DROP TABLE my_table
)
Returns the reverse SQL of what toSql() returns
// ......
// get queries to safely delete this schema.
$dropSchema = $schema->toDropSql($platform);
Array
(
[0] => DROP TABLE my_table
)
Comparing Schemas
$fromSchema = new DoctrineDBALSchemaSchema();
$myTable = $fromSchema->createTable("my_table");
$myTable->addColumn("id", "integer", array("unsigned" => true));
$myTable->addColumn("username", "string", array("length" => 32));
$myTable->setPrimaryKey(array("id"));
$toSchema = new DoctrineDBALSchemaSchema();
$myTable = $toSchema->createTable("my_table");
$myTable->addColumn("id", "integer", array("unsigned" => true));
$myTable->addColumn("username", "string", array("length" => 32));
$myTable->addColumn("email", "string", array("length" => 255));
$myTable->setPrimaryKey(array("id"));
$comparator = new DoctrineDBALSchemaComparator();
$schemaDiff = $comparator->compare($fromSchema, $toSchema);
// queries to get from one to another schema.
$queries = $schemaDiff->toSql($platform);
print_r($queries);
ALTER TABLE my_table ADD email VARCHAR(255) NOT NULL
What is ORM?
“Technique for converting data between
incompatible type systems in object-
oriented programming languages.”
http://en.wikipedia.org/wiki/Object-relational_mapping
Architecture
Entities
- Lightweight persistent domain object
- Regular PHP class
- Does not extend any base Doctrine class
- Cannot be final or contain final methods
- Any two entities in a hierarchy of classes must not have
a mapped property with the same name
- Supports inheritance, polymorphic associations and
polymorphic queries.
- Both abstract and concrete classes can be entities
- Entities may extend non-entity classes as well as entity
classes, and non-entity classes may extend entity
classes
Architecture
- No more base class required
- Values stored in object properties
- Persistence is done transparently
namespace Entities;
class User
{
private $id;
private $name;
}
Architecture
The EntityManager
- Central access point to the ORM functionality provided by
Doctrine 2. API is used to manage the persistence of your
objects and to query for persistent objects.
- Employes transactional write behind strategy that delays the
execution of SQL statements in order to execute them in the
most efficient way
- Execute at end of transaction so that all write locks are
quickly releases
- Internally an EntityManager uses a UnitOfWork to keep track
of your objects
Create EntityManager
Create a new EntityManager instance:
$config = new DoctrineORMConfiguration();
$config->setMetadataCacheImpl(new DoctrineCommonCacheArrayCache);
$driverImpl = $config->newDefaultAnnotationDriver(array(__DIR__."/Entities"));
$config->setMetadataDriverImpl($driverImpl);
$config->setProxyDir(__DIR__ . '/Proxies');
$config->setProxyNamespace('Proxies');
$em = DoctrineORMEntityManager::create($conn, $config);
Map entities to RDBMS tables
Entities are just regular PHP objects
namespace Entities;
class User
{
private $id;
private $name;
}
Map entities to RDBMS tables
Entities are just regular PHP objects
Mapped By:
- Annotations
namespace Entities;
/**
* @Entity @Table(name="users")
*/
class User
{
/** @Id @Column(type="integer") @GeneratedValue */
private $id;
/** @Column(length=50) */
private $name;
}
Map entities to RDBMS tables
Entities are just regular PHP objects:
Mapped By:
- Annotations
- YAML
EntitiesUser:
type: entity
table: users
id:
id:
type: integer
generator:
strategy: AUTO
fields:
name:
type: string
length: 255
Mapping Performance
- Only parsed once
- Cached using configured cache driver
- Subsequent requests pull mapping
information from configured cache
driver
Working with Objects
Use the $em to manage the persistence
of your entities:
$user = new User;
$user->setName('Jonathan H. Wage');
$em->persist($user);
$em->flush();
Working with Objects
Updating an object:
$user = $em->getRepository('User')
->find(array('name' => 'jwage'));
// modify the already managed object
$user->setPassword('changed');
$em->flush(); // issues update
Working with Objects
Removing an object:
$user = $em->getRepository('User')
->find(array('name' => 'jwage'));
// schedule for deletion
$em->remove($user);
$em->flush(); // issues delete
Transactions
Explicit:
// $em instanceof EntityManager
$em->getConnection()->beginTransaction(); // suspend auto-commit
try {
//... do some work
$user = new User;
$user->setName('George');
$em->persist($user);
$em->flush();
$em->getConnection()->commit();
} catch (Exception $e) {
$em->getConnection()->rollback();
$em->close();
throw $e;
}
Transactions
A more convenient explicit transaction:
// $em instanceof EntityManager
$em->transactional(function($em) {
//... do some work
$user = new User;
$user->setName('George');
$em->persist($user);
});
Transactions and Performance
for ($i = 0; $i < 20; ++$i) {
$user = new User;
$user->name = 'Jonathan H. Wage';
$em->persist($user);
}
$s = microtime(true);
$em->flush();
$e = microtime(true);
echo $e - $s;
Transactions and Performance
How you use transactions can greatly
affect performance. Here is the same
thing using raw PHP code:
$s = microtime(true);
for ($i = 0; $i < 20; ++$i) {
mysql_query("INSERT INTO users (name) VALUES
('Jonathan H. Wage')", $link);
}
$e = microtime(true);
echo $e - $s;
Which is faster?
- The one using no ORM, and no
abstraction at all?
- Or the one using the Doctrine ORM?
Which is faster?
- The one using no ORM, and no
abstraction at all?
- Or the one using the Doctrine ORM?
- Doctrine2 wins! How?
Doctrine2 0.0094 seconds
mysql_query 0.0165 seconds
Not Faster
Doctrine just automatically performed
the inserts inside one transaction. Here
is the code updated to use transactions:
$s = microtime(true);
mysql_query('START TRANSACTION', $link);
for ($i = 0; $i < 20; ++$i) {
mysql_query("INSERT INTO users (name) VALUES ('Jonathan H. Wage')",
$link);
}
mysql_query('COMMIT', $link);
$e = microtime(true);
echo $e - $s;
Much Faster
Transactions matter and can affect
performance greater than any code
optimization!
Doctrine2 0.0094 seconds
mysql_query 0.0165 seconds
0.0028
Locking Support
Verify version when finding:
use DoctrineDBALLockMode;
use DoctrineORMOptimisticLockException;
$theEntityId = 1;
$expectedVersion = 184;
try {
$entity = $em->find('User', $theEntityId, LockMode::OPTIMISTIC, $expectedVersion);
// do the work
$em->flush();
} catch(OptimisticLockException $e) {
echo "Sorry, but someone else has already changed this entity. Please apply the changes again!";
}
DQL
- DQL stands for Doctrine Query Language and is an
Object Query Language derivate that is very similar to
the Hibernate Query Language (HQL) or the Java
Persistence Query Language (JPQL).
- DQL provides powerful querying capabilities over your
object model. Imagine all your objects lying around in
some storage (like an object database). When writing
DQL queries, think about querying that storage to find
a certain subset of your objects.
DQL Parser
- Parser completely re-written from
scratch
- Parsed by top down recursive descent
lexer parser that constructs an
AST(Abstract Syntax Tree)
- Platform specific SQL is generated
from AST
Query Builder
Same query built using the QueryBuilder
$qb = $em->createQueryBuilder()
->select('u')
->from('User', 'u');
$q = $qb->getQuery();
$users = $q->execute();
More Examples
$query = $em->createQuery(
'SELECT u, g, FROM User u ' .
'LEFT JOIN u.Groups g ' .
'ORDER BY u.name ASC, g.name ASC'
);
$users = $query->execute();
$qb = $em->createQueryBuilder()
->select('u, g')
->from('User', 'u')
->leftJoin('u.Groups', 'g')
->orderBy('u.name', 'ASC')
->addOrderBy('g.name', 'ASC');
$query = $qb->getQuery();
Executing Queries
Executing and getting results
$users = $query->execute();
foreach ($users as $user) {
// ...
foreach ($user->getGroups() as $group) {
// ...
}
}
Executing Queries
Execute query and iterate over results
keeping memory usage low:
foreach ($query->iterate() as $user) {
// ...
foreach ($user->getGroups() as $group) {
// ...
}
}
Result Cache
Optionally cache the results of your
queries in your driver of choice:
$cacheDriver = new DoctrineCommonCacheApcCache();
$config->setResultCacheImpl($cacheDriver);
$query = $em->createQuery('select u from EntitiesUser u');
$query->useResultCache(true, 3600, 'my_query_name');
$users = $query->execute();
$users = $query->execute(); // 2nd time pulls from cache
Inheritance
Doctrine supports mapping entities that
use inheritance with the following
strategies:
- Mapped Superclass
- Single Table Inheritance
- Class Table Inheritance
Single Table Inheritance
/**
* @Entity
* @InheritanceType("SINGLE_TABLE")
* @DiscriminatorColumn(name="discr", type="string")
* @DiscriminatorMap({"person" = "Person", "employee" = "Employee"})
*/
class Person
{
// ...
}
/**
* @Entity
*/
class Employee extends Person
{
// ...
}
Single Table Inheritance
- All entities share one table.
- To distinguish which row represents
which type in the hierarchy a so-
called discriminator column is used.
Class Table Inheritance
/**
* @Entity
* @InheritanceType("JOINED")
* @DiscriminatorColumn(name="discr", type="string")
* @DiscriminatorMap({"person" = "Person", "employee" = "Employee"})
*/
class Person
{
// ...
}
/** @Entity */
class Employee extends Person
{
// ...
}
Class Table Inheritance
- Each class in a hierarchy is mapped to several
tables: its own table and the tables of all
parent classes.
- The table of a child class is linked to the table
of a parent class through a foreign key
constraint.
- A discriminator column is used in the topmost
table of the hierarchy because this is the
easiest way to achieve polymorphic queries.
Bulk Inserts with Domain
Insert 10000 objects batches of 20:
$batchSize = 20;
for ($i = 1; $i <= 10000; ++$i) {
$user = new User;
$user->setStatus('user');
$user->setUsername('user' . $i);
$user->setName('Mr.Smith-' . $i);
$em->persist($user);
if ($i % $batchSize == 0) {
$em->flush();
$em->clear(); // Detaches all objects from Doctrine!
}
}
Bulk Update with DQL
$q = $em->createQuery('update Manager m set m.salary = m.salary * 0.9');
$numUpdated = $q->execute();
Bulk Update with Domain
Update objects in batches of 20:
$batchSize = 20;
$i = 0;
$q = $em->createQuery('select u from User u');
$iterableResult = $q->iterate();
foreach($iterableResult AS $row) {
$user = $row[0];
$user->increaseCredit();
$user->calculateNewBonuses();
if (($i % $batchSize) == 0) {
$em->flush(); // Executes all updates.
$em->clear(); // Detaches all objects from Doctrine!
}
++$i;
}
Bulk Delete with DQL
$q = $em->createQuery('delete from Manager m where m.salary > 100000');
$numDeleted = $q->execute();
Bulk Delete with Domain
$batchSize = 20;
$i = 0;
$q = $em->createQuery('select u from User u');
$iterableResult = $q->iterate();
while (($row = $iterableResult->next()) !== false) {
$em->remove($row[0]);
if (($i % $batchSize) == 0) {
$em->flush(); // Executes all deletions.
$em->clear(); // Detaches all objects from Doctrine!
}
++$i;
}
Events
Doctrine triggers events throughout the
lifecycle of objects it manages:
- preRemove
- postRemove
- prePersist
- postPersist
- preUpdate
- postUpdate
- preLoad
- postLoad
Example
/**
* @Entity
* @HasLifecycleCallbacks
*/
class BlogPost
{
// ...
/** @PreUpdate */
public function prePersist()
{
$this->createdAt = new DateTime();
}
/** @PreUpdate */
public function preUpdate()
{
$this->updatedAt = new DateTime();
}
}
Using Raw SQL
- Write a raw SQL string
- Map the result set of the SQL query
using a ResultSetMapping instance
Using Raw SQL
$sql = 'SELECT id, name FROM users WHERE username = ?';
$rsm = new ResultSetMapping;
$rsm->addEntityResult('User', 'u');
$rsm->addFieldResult('u', 'id', 'id');
$rsm->addFieldResult('u', 'name', 'name');
$query = $this->_em->createNativeQuery($sql, $rsm);
$query->setParameter(1, 'jwage');
$users = $query->getResult();