Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Doctrine effizient verwenden

Denis Brumann
September 25, 2019

Doctrine effizient verwenden

Doctrine ORM ist ein elementarer Bestandteil in vielen Symfony-Anwendungen, egal
ob Full-Stack, API oder Command Line-Tool. Seit dem Release von Version 2.6 Ende
Dezember 2017 wird an ORM Next (3.0) gearbeitet und der Branch enthält bereits
einige Änderungen, die man in aktuellen Projekten berücksichtigen kann, um ein
späteres Upgrade einfacher zu gestalten.

In diesem Workshop besprechen wir aktuelle Best Practices beim Einsatz von Doctrine in einer modernen Symfony-Anwendung mit Fokus auf die Vermeidung von Speicher- und Performanceproblemen und einem Blick auf zukünftige Änderungen und ihre Auswirkungen auf Projekte, die Doctrine ORM einsetzen. Der Workshop richtet sich an Entwickler*innen, die bereits Erfahrung mit Doctrine haben, aber ihr Verständnis vertiefen möchten.

Die Praxisbeispiele bauen auf einer aktuellen Symfony 4-Anwendung auf und Features, die zum Einsatz kommen, wie das Autowiring, werden als bekannt vorausgesetzt.

Denis Brumann

September 25, 2019
Tweet

More Decks by Denis Brumann

Other Decks in Programming

Transcript

  1. Architektur / DBAL The Doctrine database abstraction & access layer

    (DBAL) offers a lightweight and thin runtime layer around a PDO-like API and a lot of additional, horizontal features like database schema introspection and manipulation through an OO API.
  2. Architektur / DBAL •Database Driver pdo_mysql, pdo_pgsql, pdo_sqlsrv, … •Platform

    MySQL 5.7, MySQL 8.0, MariaDB10 •Connection (carries driver & executes queries) •Statement (represents a request, executed or being prepared)
  3. Architektur / DBAL / Konfiguration # config/packages/doctrine.yaml doctrine: dbal: server_version:

    5.7 url: '%env(resolve:DATABASE_URL)%' # .env ###> doctrine/doctrine-bundle ### #... DATABASE_URL=mysql://db_user:[email protected]:3306/db_name ###< doctrine/doctrine-bundle ###
  4. Architektur / DBAL / Connection function query($sql); function prepare($sql); function

    beginTransaction(); function commit(); function rollback(); function lastInsertId();
  5. Architektur / DBAL / Connection function insert($expression, $data); function update($expression,

    $data, $id); function delete($expression, $id); function transactional(Closure $func);
  6. Architektur / ORM •Object Manager main public interface •Object Repository

    retrieve instances of your mapped objects •ClassMetadata
  7. Architektur / ORM / Entities Klasse darf nicht final sein

    Klasse darf keine finalen Methoden enthalten persistente Properties sollten private oder protected sein __clone darf nicht implementiert werden* __wakeup darf nicht implementiert werden* func_get_args() statt definierter Argumente, darf nicht verwendet werden
  8. Architektur / ORM / Unit Of Work Enthält eine Liste

    der (gemanagten) Entities Daten werden im Arbeitsspeicher vorgehalten Kontrolliert: • entity state • commit order • what to write and how to write (update/insert)
  9. Setup / Challenge version: '3' services: database: image: mysql:latest ports:

    - '3306:3306' environment: MYSQL_ALLOW_EMPTY_PASSWORD: '1' MYSQL_DATABASE: workshop_app MYSQL_USER: root volumes: - 'app_data:/var/lib/mysql' command: - 'mysqld' - '--character-set-server=utf8mb4' - '--collation-server=utf8mb4_unicode_ci' - '--default-authentication-plugin=mysql_native_password' volumes: app_data: ~
  10. Mapping / Field Types • array • simple_array • json_array

    • json • bigint • boolean • datetimetz • date • time • decimal • integer • object • smallint • string • text • binary • blob • float • guid • dateinterval • datetime
  11. Mapping / Embeddable • Embeddables sind keine Entities und dürfen

    keine Assoziationen enthalten • Felder werden wie bei Entities mit @Column annotiert • Nullable Columns können nicht in der Entity überschrieben werden
  12. Mapping / Embeddable /** @ORM\Embeddable */ class Address { /**

    @ORM\Column */ private $street; /** @ORM\Column */ private $postalCode; /** @ORM\Column */ private $city; /** @ORM\Column */ private $country; }
  13. Mapping / Embeddable /** @ORM\Entity */ class Order { /**

    @ORM\Embedded(class="Address") */ private $shippingAddress; /** @ORM\Embedded(class="Address") */ private $billingAddress; }
  14. Mapping / Embeddable /** @ORM\Entity */ class Profile { /**

    @ORM\Embedded(class="Address", columnPrefix=false) */ private $address; }
  15. Mapping / MappedSuperclass / Restrictions • MappedSuperclass ist keine eigene

    Entity • nur uni-direktionale Assoziationen mit owning side möglich • Keine Queries über MappedSuperclass möglich • Overrides für Assoziationen und Felder sind möglich
  16. Mapping / Inheritance / STI /** * @Entity * @InheritanceType("SINGLE_TABLE")

    * @DiscriminatorColumn(name="discr", type="string") * @DiscriminatorMap({"person" = "Person", "employee" = "Employee"}) */ class Person { // ... } /** * @Entity */ class Employee extends Person { // ... }
  17. Mapping / STI • DiscriminatorMap kann automatisch generiert werden •

    nicht verwenden! • Daten befinden sich in einer Tabelle • keine Joins notwendig • Subtypen können einfach mit WHERE identifiziert werden
  18. Mapping / Inheritance / CTI /** * @Entity * @InheritanceType("JOINED")

    * @DiscriminatorColumn(name="discr", type="string") * @DiscriminatorMap({"person" = "Person", "employee" = "Employee"}) */ class Person { // ... } /** * @Entity */ class Employee extends Person { // ... }
  19. Mapping / CTI • DiscriminatorMap kann automatisch generiert werden •

    nicht verwenden! • Basis-Tabelle mit Join-Tabelle für Subtypen • Subtypen werden per Join angehangen • Abfrage über alle Suchtypen ist entsprechend ineffizient
  20. Querying / DQL / SELECT $dql = <<<DQL SELECT task

    FROM App\Entity\TaskItem task WHERE task.list = :list_id AND task.done = true DQL; return $em->createQuery($dql) ->setParameter('list_id', 1) ->getResult();
  21. Querying / DQL / UPDATE $dql = <<<DQL UPDATE App\Entity\TaskItem

    task SET done = true WHERE task.list = :list_id DQL;
  22. Querying / DQL / Expressions •DQL Functions CONCAT(), SIZE(), LENGTH(),

    … •Arithmetic Operators +, /, * •Aggregate Functions AVG(), COUNT(), MIN(), MAX(), SUM() •Other Expressions BETWEEN, IN, LIKE
  23. Querying / DQL / Custom Expressions •Create custom FunctionNodes Doctrine\ORM\Query\AST\Functions\FunctionNode

    •Libraries Benjamin Eberlei DoctrineExtensions – https://github.com/beberlei/DoctrineExtensions
  24. Querying / NativeQuery / SQL $dql = <<<SQL SELECT task

    FROM app_task_item AS task WHERE task.list_id = :list_id AND task.done = 1 SQL; return $em->createNativeQuery($dql, $rsm) ->setParameter('list_id', 1) ->getSingleScalarResult();
  25. Querying / DQL / PARTIAL RESULTS $dql = <<<DQL SELECT

    partial task.{name,done} FROM App\Entity\TaskItem task WHERE task.list = :list_id AND task.done = true DQL; return $em->createQuery($dql) ->setParameter('list_id', 1) ->getResult();
  26. Querying / Lazy Associations /** * @Entity */ class CmsGroup

    { /** * @ManyToMany( * targetEntity="CmsUser", * mappedBy="groups", * fetch="EXTRA_LAZY" * ) */ public $users; }
  27. Querying / Hydration /** * Hydrates an object graph. This

    is the default behavior. */ const HYDRATE_OBJECT = 1; /** * Hydrates an array graph. */ const HYDRATE_ARRAY = 2; /** * Hydrates a flat, rectangular result set with scalar values. */ const HYDRATE_SCALAR = 3; /** * Hydrates a single scalar value. */ const HYDRATE_SINGLE_SCALAR = 4; /** * Very simple object hydrator (optimized for performance). */ const HYDRATE_SIMPLEOBJECT = 5;
  28. Querying / Hydration • ObjectHydrator • ArrayHydrator • ScalarHydrator •

    SimpleObjectHydrator • SingleScalarHydrator • GeneratedHydrator – https://github.com/Ocramius/GeneratedHydrator
  29. Querying / Challenge Custom Object Hydration 1. Objekt für TaskListSummary

    erstellen alle relevanten Argumente im Constructor und nur Getter 2. Neue Methode im TaskListRepository SELECT NEW TaskListSummary(…) FROM TaskList::class …
  30. Locking / Optimistic Locking class User { // ... /**

    * @Version * @Column(type="integer") */ private $version; // ... } alternativ: datetime
  31. Locking / Optimistic Locking use Doctrine\DBAL\LockMode; use Doctrine\ORM\OptimisticLockException; $id =

    1; $version = 184; try { $entity = $em->find(User::class, $id, LockMode::OPTIMISTIC, $version); // do the work $em->flush(); } catch(OptimisticLockException $e) { echo "Change detected! Someone already wrote a newer version"; }
  32. Locking / Pessimistic Locking Locking erfolgt auf Datanbenk-Ebene keine Entity-Anpassungen

    notwendig, aber Anpassungen an DB-Interaktion disable Auto-Commit mode Transaktionen PESSIMISTIC_WRITE, PESSIMISTIC_READ
  33. Locking / Optimistic Locking Using EntityManager:: find($className, $id, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE) or

    find($className, $id, \Doctrine\DBAL\LockMode::PESSIMISTIC_READ) Using EntityManager:: lock($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE) or lock($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_READ) Using Query:: setLockMode(\Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE) or setLockMode(\Doctrine\DBAL\LockMode::PESSIMISTIC_READ)
  34. Locking / Challenge TaskList - neues Attribut (+ Getter) für

    Version hinzufügen - Neue Aktion im Controller, um Liste zu archivieren - show-Template anpassen (form action hinzufügen)
  35. Transaktionen $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(); throw $e; }
  36. Transaktionen $em->transactional(function($em) { //... do some work $user = new

    User(); $user->setName('George'); $em->persist($user); });
  37. Lifecycle Events / Callbacks /** * @ORM\Entity * @ORM\HasLifecycleCallbacks */

    class BlogPost { // ... /** * @ORM\PreUpdate */ public function setUpdatedAt() { $this->updatedAt = new \DateTimeImmutable('now'); } }
  38. Lifecycle Events / Event Listeners services: # ... App\EventListener\SearchIndexer: tags:

    - name: 'doctrine.event_listener' event: 'postPersist' priority: 500 # connection: 'default'
  39. Lifecycle Events / Entity Listeners services: # ... App\EventListener\UserChangedNotifier: tags:

    - name: 'doctrine.orm.entity_listener' event: 'postUpdate' entity: 'App\Entity\User' lazy: true entity_manager: 'custom' method: 'checkUserChanges'
  40. Change Tracking Policies use Doctrine\Common\NotifyPropertyChanged; use Doctrine\Common\PropertyChangedListener; /** * @Entity

    * @ChangeTrackingPolicy("NOTIFY") */ class MyEntity implements NotifyPropertyChanged { // ... private $_listeners = []; public function addPropertyChangedListener(PropertyChangedListener $listener) { $this->_listeners[] = $listener; } } protected function _onPropertyChanged($propName, $oldValue, $newValue) { if ($this->_listeners) { foreach ($this->_listeners as $listener) { $listener->propertyChanged( $this, $propName, $oldValue, $newValue ); } } }
  41. Caching / Metadata Cache Annotation, XML bzw. YAML-Mapping muss in

    ClassMetadata umgewandelt werden à Cache verhindert Parsing bei jedem Request.
  42. Caching / Query Cache DQL muss in SQL transformiert werden

    à Cache verhindert SQL-Parsing, wenn DQL-Query nicht geändert wurde
  43. Caching / Result Cache Ergebnis der SQL-Abfrage kann unter Umständen

    gecacht werden, um weniger oft auf die Datenbank zugreifen zu müssen. Trade Off zwischen Kosten für DB-Verbindung vs. Cache- Zugriff. à Ergebnis einer Query und der resultierenden Hydration wird gespeichert
  44. Caching / Using Result Cache $query = $em->createQuery( 'select u

    from \Entities\User u' ); $query ->useResultCache(true) ->setResultCacheLifetime(3600) ->setResultCacheId('my_id') // ->useResultCache(true, 3600, 'my_id') ;
  45. Second Level Cache Neuer Cache Layer überhalb der bestehenden Caches

    Cache schreibt in verschieden Regions. Jede Region hat einen eigenen Namespace und Lifetime. Caching kann in CacheFactory angepasst werden.
  46. Second Level Cache / Cache Mode READ_ONLY Can do reads,

    inserts and deletes, but no updates or locking NONSTRICT_READ_WRITE Does not employ locks, but can do reads, inserts, updates and deletes READ_WRITE Employs locks before update/delete
  47. Second Level Cache / Query Cache $query = $em->createQuery( 'select

    u from \Entities\User u' ); $query->setCacheable(true);
  48. Second Level Cache / Query Cache / Cache Mode Cache::MODE_GET

    May read items from the cache, but will not add items. Cache::MODE_PUT Will never read items from the cache, but will add items to the cache as it reads them from the database. Cache::MODE_NORMAL May read items from the cache, and add items to the cache. Cache::MODE_REFRESH The query will never read items from the cache, but will refresh items to the cache as it reads them from the database.
  49. Second Level Cache / Cache Eviction $this->_em->createQuery( 'UPDATE Entity\Country u

    SET u.name = 'unknown' WHERE u.id = 1' ) ->setHint(Query::HINT_CACHE_EVICT, true) ->execute(); // $em->getCache()->evictEntityRegion('Entity\Country');
  50. Indizes /** * @Entity * @Table( * name="ecommerce_products", * indexes={@Index(

    * name="search_idx", * columns={"name", "email"} * )} * ) */ class Product { }
  51. Batch-Processing / Bulk Insert $batchSize = 20; for ($i =

    1; $i <= 10000; ++$i) { $user = new CmsUser(); $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! } } //Persist objects that did not make up an entire batch $em->flush(); $em->clear();
  52. Batch-Processing / Iterating Results $batchSize = 20; $i = 0;

    $q = $em->createQuery('select u from MyProject\Model\User u'); $iterableResult = $q->iterate(); while (($row = $iterableResult->next()) !== false) { $em->remove($row[0]); if (($i % $batchSize) === 0) { $em->flush(); $em->clear(); } ++$i; } $em->flush();
  53. Migrations use Doctrine\DBAL\Schema\Schema; use Doctrine\Migrations\AbstractMigration; /** * Auto-generated Migration: Please

    modify to your needs! */ final class Version20180601193057 extends AbstractMigration { public function getDescription() : string { return ''; } public function up(Schema $schema) : void { … } public function down(Schema $schema) : void { … } }
  54. Migrations / Additional Behavior public function isTransactional() : bool {

    return false; } public function preUp(Schema $schema) : void { } public function postUp(Schema $schema) : void { } public function preDown(Schema $schema) : void { } public function postDown(Schema $schema) : void { }
  55. Migrations / Special Conditions public function up(Schema $schema) : void

    { // ... $this->warnIf(true, 'Something might be going wrong'); $this->abortIf(true, 'Something went wrong. Aborting.'); $this->skipIf(true, 'Skipping this migration.'); // ... }
  56. Fixtures namespace App\DataFixtures; use App\Entity\Product; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Common\Persistence\ObjectManager; class

    AppFixtures extends Fixture { public function load(ObjectManager $manager) { $product = new Product(); //… $manager->persist($product); $manager->flush(); } }
  57. Testing use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; class TaskListRepositoryTest extends TestCase { public function

    testSomething() { $config = DoctrineTestHelper::createTestConfiguration(); $entityManager = DoctrineTestHelper::createTestEntityManager($config); } }
  58. Testing <!-- phpunit.xml.dist --> <phpunit> <!-- ... --> <!-- Add

    this in PHPUnit 8 or higher --> <extensions> <extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/> </extensions> <!-- Add this in PHPUnit 7 or lower --> <listeners> <listener class="\DAMA\DoctrineTestBundle\PHPUnit\PHPUnitListener"/> </listeners> </phpunit>