Upload
filip-prochazka
View
567
Download
1
Embed Size (px)
Citation preview
Doctrine ORM& model
@ProchazkaFilip
kdyby/doctrine v3.0
Co si povíme
- jak psát entity- práce s db schématem- použití entit- pokládání dotazů- modelové třídy (a jak nepsat repozitáře)- jak cachovat
Jak psát entity
Databáze neexistuje
$articles = [
new Article('abc'),
new Article('def'),
new Article('ghi')
];
Asociace mezi entitami
class Comment {
private $article;
public function __construct(Article $article) {
$this->article = $article;
}
Asociace mezi entitamiclass Article {
private $comments;
public function __construct() {
$this->comments = new ArrayCollection();
}
function addComment(Comment $comment) {
$this->comments[] = $comment;
}
Nahaté kolekce? Never!
class Article {
// ...
function getComments() {
return $this->comments;
}
Magic to the rescue
use Kdyby\Doctrine\Entities\MagicAccessors;
class Article {
use MagicAccessors;
Magic to the rescue
- pouze na protected properties- magické (get|set)tery- magické collection accessory
Magic to the rescue
class Article {
private $comments;
function getComments() {
return $this->comments;
}
Magic to the rescue
class Article {
protected $comments;
Magic to the rescue
$article->comments
->add(new Comment($article)); // vyhodi vyjimku
$article->comments
->filter(function () { .. }); // ok
Magic to the rescue
class Article {
// ...
function getComments();
function addComment(Comment $comment);
function hasComment(Comment $comment);
function removeComment(Comment $comment);
Less is more
class Comment {
private $article;
// ...
function getArticle() {
return $this->article;
}
Less is moreprivate $published;
function isPublished() {
return $this->published;
}
function publish() {
$this->published = TRUE;
}
Tak od teď,databáze už zase existuje
Životní cyklus entity
- create -> persist -> flush -> konec rq- load -> update -> flush -> konec rq- load -> delete -> flush -> konec rq
Existence entity- začíná s create- končí s delete+flush
Aby entita byla entita
- musí mít metadata- její NS s driverem musí být registrovaný
Mapping/** @ORM\Entity() */
class Article {
use MagicAccessors;
use Identifier;
/** @ORM\Column(type="string") */
protected $title;
Mapping
/**
* @ORM\OneToMany(targetEntity="Comment", cascade={"persist"})
* @var Comment[]|ArrayCollection
*/
protected $comments;
Zaregistrujeme
extensions:
console: Kdyby\Console\DI\ConsoleExtension
events: Kdyby\Events\DI\EventsExtension
annotations: Kdyby\Annotations\DI\AnnotationsExtension
doctrine: Kdyby\Doctrine\DI\OrmExtension
Nakonfigurujeme
doctrine:
metadata:
App: %appDir%
Schéma (pomocí sf console)
$ php www/index.php
orm:schema:create
orm:schema:update --dump-sql
$ php www/index.php o:s:u --dump-sql
Migrace
- doctrine/migrations:@dev
- zenify/doctrine-migrations
extensions:
migrations: Zenify\DoctrineMigrations\DI\MigrationsExtension
Používáme
$article = new Article('Abc');
$article->addComment(new Comment($article));
$em->persist($article);
$em->flush();
Používáme
$article = $em->find(Article::class, 1);
$article->title = "lol";
$em->flush();
Praktické použití entit
V presenterechprivate $article;
public function actionDefault() {
$this->article = $this->em->find(Article::class, 42);
}
public function renderDefault() {
$this->template->article = $this->article
}
Ve formulářích: vytvořeníprotected function createComponentForm() {
$form = new UI\Form;
$form->onSuccess[] = function ($form, $values) {
$article = new Article($values->title);
$this->em->persist($article);
$this->em->flush();
}
return $form;
}
Ve formulářích: editaceprotected function createComponentForm() {
$form = new UI\Form;
$form->onSuccess[] = function ($form, $values) {
$this->article->setTitle($values->title);
$this->em->flush();
}
return $form;
}
Ve formulářích: alternativně
- univerzální formuláře pro create/edit- form mappery (Kdyby/DoctrineForms ?)
V komponentáchclass ArticleControl extends UI\Control {
private $article;
function __construct(Article $article) {
$this->article = $article;
}
public function render() {
$this->template->article = $this->article;
}
Netriviální dotazování
EntityRepositoryfunction findBy(array $criteria, array $orderBy, $limit, $offset)
$repo->findBy(['article.title' => $title]);
$repo->findBy([], ['article.title' => 'DESC'])
function countBy(array $criteria)
$repo->countBy(['article.author' => $author]);
function findPairs($criteria, $value, $orderBy, $key)
$repo->findPairs(['currency' => 'USD'], "name")
DQL & Query builder
$qb = $em->createQueryBuilder();
$qb->select('u')
->from(User::class, 'u')
->where('u.id = :id')->setParameter('id', 123)
->orderBy('u.name', 'ASC');
DQL & Query builder
$query = $qb->getQuery();
/** @var User[] $result */
$result = $query->getResult();
Native query
$sql = 'SELECT * FROM users WHERE name = ?';
$rsm = new ResultSetMapping();
// build rsm here
$query = $entityManager->createNativeQuery($sql, $rsm);
$query->setParameter(1, 'romanb');
$users = $query->getResult();
ResultSet: why$paginator = $this['vp']->getPaginator();
$paginator->setItemsCount($articles->countAll());
$this->template->articles = $articles->findAll(
$paginator->getOffset(),
$paginator->getLength()
);
ResultSet: whyfunction findAll($limit, $offset) {
return $this->repository
->createQuery("SELECT a FROM App\Article a")
->setMaxResults($limit)
->setFirstResult($offset);
function countAll() {
return $this->repository
->createQuery("SELECT COUNT(a.id) FROM App\Article a")
->getSingleScalarResult();
ResultSet: howpublic function findAll() {
$query = $this->repository
->createQuery("SELECT a FROM App\Article a")
return new ResultSet($query);
}
// usage
$this->template->articles = $articles->findAll()
->applyPaginator($this['vp']->getPaginator());
Ještě složitější(ale mocnější) dotazování
Query Object
class QuestionsQuery extends Kdyby\Doctrine\QueryObject
{
/**
* @param \Kdyby\Persistence\Queryable $repository
* @return \Doctrine\ORM\QueryBuilder
*/
protected function doCreateQuery(Queryable $repository);
Query Object
function inCategory(Category $cat) {
$this->filter[] = function (QueryBuilder $qb) use ($cat) {
$qb->andWhere('q.category = :categoryId')
->setParameter('categoryId', $cat->getId());
};
return $this;
}
Query Object
public function withLastPost() {
$this->select[] = function (QueryBuilder $qb) {
$qb->addSelect(lp')
->leftJoin('q.lastPost', 'lp');
};
return $this;
}
Query Object
$query = (new QuestionsQuery())
->withLastPost()
->inCategory($category);
$result = $repository->fetch($query);
1+N problem
- optimální množství dat- vysoká komplexita- špatná odezva na síti- performance killer
1+N problem
$qb->select('article, comment')
->from(Article::class, 'article')
->leftJoin('article.comments', 'comment')
M*N problem: násobení řádků
- moc dat na projití- vysoká komplexita- moc práce pro databázi- moc práce pro doctrine- performance killer
Query Object: efektivně
- každá query musí načítat pouze toOne relace
- toMany relace načítat s WHERE IN dalšími queries (postFetch)
- konstatní počet queries
Query Object: postFetchpublic function withComments() {
$this->onPostFetch[] = function ($_, Queryable $repository, \Iterator $iterator) {
$ids = array_keys(iterator_to_array($iterator, TRUE));
$repository->createQueryBuilder()
->select('partial article.{id}', 'comments')
->from(Article::class, 'article')
->leftJoin('article.comments', 'comments')
->andWhere('article.id IN (:ids)')->setParameter('ids', $ids)
->getQuery()->getResult();
}
return $this;
}
Query Object: postFetch
$query = (new ArticlesQuery())
->withComments();
$result = $repository->fetch($query);
Repozitáře, fasády, služby
aneb pětivrstvý model
Repozitáře
- vhodné použití ve facade- vhodné použití v services- ale klidně i v presenteru/komponentě- pouze čtení!
Repozitáře
/**
* @ORM\Entity(repositoryClass="ArticleRepository")
*/
class Article { }
class ArticleRepository
extends Kdyby\Doctrine\EntityRepository { }
services:
- App\Blog\ArticleRepository()
-
class: App\Blog\ArticleRepository()
tags: [doctrine.repositoryEntity: App\Blog\Article]
Repozitáře jako služby
Facade
- brána mezi presenterem/komponentou a modelem
- snadnější sdílení logiky mezi api a frontem- vhodné místo pro flush- není nutné mít 1:1 k entitám
Facade
class BlogFacade {
function findPublished();
function fetch(ArticlesQuery $query);
function saveComment(Comment $comment);
class RedactorFacade {
function createDraft();
function save(Article $article);
Služby
- dělení aplikace na menší logické celky- nějaké konkrétní operace nad db/entitami- klidně i externí api- používají se ve facades
Eventy
Lifecycle eventy
- na entitě- “full blown” listenery- listenery pro typ
Lifecycle eventy na entitě
/** @ORM\Entity @ORM\HasLifecycleCallbacks */
class User {
/** @ORM\PrePersist */
public function doStuffOnPrePersist() {
$this->createdAt = date('Y-m-d H:i:s');
}
Listenery
class MyEventListener implements Kdyby\Events\Subscriber {
function preUpdate(LifecycleEventArgs $args) {
$entityManager = $args->getObjectManager();
$entity = $args->getObject();
if ($entity instanceof User) {
// do something with the User
}
}
Listenery pro typ
class MyEventListener {
function preUpdate(User $user, LifecycleEventArgs $args) {
$entityManager = $args->getObjectManager();
// do something with the User
}
Cache
Cachovací strategie
- result caching- 2nd level caching
Result caching
$query = $em->createQuery('SELECT u FROM App\User u');
$query->useResultCache(
true,
$lifetime,
$resultCacheId
);
2nd level cache (since 2.5)- READ_ONLY (výchozí)- NOSTRICT_READ_WRITE- READ_WRITE (zámky)
2nd level cache
doctrine:
secondLevelCache:
enabled: true
driver: redis(@redis.default_client::getDriver())
2nd level cache/**
* @ORM\Cache(region="product")
*/
class Product {
/**
* @ORM\ManyToMany(...)
* @ORM\Cache(usage="NONSTRICT_READ_WRITE", region="product")
*/
private $tags;
Shrnutí
- jak psát entity- práce s db schématem- použití entit- pokládání dotazů- modelové třídy- cache
Možná někdy příště
- Zápis do db, čtení z Elasticu (to dělá rohlík)- Command Query Responsibility Segregation
Dotazy?
Díky za pozornost!@ProchazkaFilip