doctrine: co dělat, když entity nestačí
TRANSCRIPT
![Page 1: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/1.jpg)
Doctrine: Co dělat,když entity nestačí?
@ProchazkaFilip
#makeCodeNotWar
![Page 2: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/2.jpg)
Co si povíme?● minimum využití databáze každého Doctrinisty● batch operace● rozšiřování DQL● metody hydratace● native queries
#makeCodeNotWar
![Page 3: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/3.jpg)
Minimum využití databázekaždého Doctrinisty
![Page 4: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/4.jpg)
Minimum využití databáze každého Doctrinisty● Co za vás dělá Doctrine
○ indexy pro vazby a primární klíče○ vzdálené klíče
● Co si musíte pohlídat sami○ unique indexy○ vlastní typy
Pokud se bavíme o nedělání hloupého úložiště z databáze, tak logicky chceme využívat funkce co databáze nabízí. Takový úplný základ jsou indexy a vzdálené klíče. O trochu pokročilejší jsou pak unikátní klíče a vlastní datové typy.
![Page 5: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/5.jpg)
Indexy a vzdálené klíče
class User {
/**
* @ORM\Id()
* @ORM\Column(type="uuid")
*/
private $id;
class Order {
/**
* @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=false)
*/
private $user;
IDčko musíme entitě dát vždy, jinak ji Doctrine nepřijme - tím získáme primární klíče. Když pak děláme vazby pomocí *ToMany nebo *ToOne vazeb, generuje nám Doctrine i vzdálené klíče, k nim indexy a klidně i many to many tabulky.
![Page 6: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/6.jpg)
Unique indexy
class User {
/**
* @ORM\Column(type="string", unique=true)
*/
private $email;
Nejjednodušší způsob, jak získat unique klíče, je použít unique vlastnost annotace Column. Doctrine nám pak vygeneruje adektávní schéma.
![Page 7: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/7.jpg)
Unique indexy/**
* @ORM\Entity()
* @ORM\Table(name="order_item_rating",
* uniqueConstraints={
* @ORM\UniqueConstraint(name="order_item_rating_x_unique", columns={
* "order_item_id", "user_id"
* })
* }
* )
*/
class OrderItemRating
Malinko složitější je pak udělat unikátní klíče nad vícero sloupci.
![Page 8: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/8.jpg)
Unique indexy: vkládání a race conditions● Opravdu to potřebujete řešit?
○ >90% aplikacím stačí check přes repository před flushem○ >90% aplikací nikdy nenaroste natolik, aby to byl skutečný problém
● Pokud to opravdu opravdu potřebujete řešit○ kdyby/doctrine ... NonLockingUniqueInserter ○ ^ brzy i samostatně, jako kdyby/doctrine-nonlocking-unique-inserter○ ^ Symfony friendly ❤○ ^ sledujte issue kdyby/doctrine#238
S unikátními klíči souvisí řešení race conditions. EntityManager se při flushnutí entity, která poruší unique index, kompletně zamkne a není možné s ním dál pracovat. Pro 90% aplikací stačí prostý check repozitářem, že takový záznam v databázi neexistuje. Pokud to však opravu potřebujete řešit, koukněte na issue https://github.com/Kdyby/Doctrine/issues/238, kde se řeší rozdělení Kdyby/Doctrine na framework-agnostic balíčky. Brzy totiž vznikne například kdyby/doctrine-nonlocking-unique-inserter, který tento problém řeší.
![Page 9: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/9.jpg)
Vlastní datový typclass UuidType extends Type {
const NAME = 'uuid';
function getName() { return self::NAME; }
function getSQLDeclaration(
array $fieldDeclaration, AbstractPlatform $platform);
function convertToPHPValue(
$value, AbstractPlatform $platform);
function convertToDatabaseValue(
$value, AbstractPlatform $platform);
Zdroj: ramsey/uuid-doctrine
Pěkný příklad vlastního typu je UuidType z balíčku ramsey/uuid-doctrine. Viz http://docs.doctrine-project.org/en/latest/cookbook/custom-mapping-types.html
![Page 10: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/10.jpg)
Vlastní datový typ: komentáře ve schématu
class SomethingBasedOnStringType extends StringType {
function requiresSQLCommentHint(
AbstractPlatform $platform) : bool
// …
$platform->markDoctrineTypeCommented(Type::getType($type));
U vlastních typů je nutné řešit, aby byly správně oanotovány v databázi, protože jinak Doctrine neumí správně diffnout co se změnilo. Pokud však typ označíme, jakože se má komentovat, Doctrine si do komentáře sloupečku tabulky uloží jak se ten typ jmenuje a bude schéma diffovat správně.
![Page 11: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/11.jpg)
Vlastní datový typ: konverze na úrovni DB
class GeometryType extends Type {
function canRequireSQLConversion() : bool;
function convertToDatabaseValueSQL(
$sqlExpr, AbstractPlatform $platform);
function convertToPHPValueSQL(
$sqlExpr, $platform);
Pokud budete potřebovat pokročilou konverzi binárních dat už na úrovni databáze, slouží k tomu tyto metody.
![Page 12: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/12.jpg)
Vlastní datový typ: registrace
doctrine:
dbal:
types:
uuid:
class: Ramsey\Uuid\Doctrine\UuidType
Zdroj: ramsey/uuid-doctrine
Ukázka registrace vlastního typu v Symfony.
![Page 13: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/13.jpg)
Vlastní datový typ: použití
/**
* @ORM\Id()
* @ORM\Column(type="uuid")
*/
private $id;
CREATE TABLE "user" (
id UUID NOT NULL,
email VARCHAR(255) DEFAULT NULL
-- atd
);
COMMENT ON COLUMN "user".id
IS '(DC2Type:uuid)';
Při použití uuid typu na sloupci id se vygeneruje podobné schéma, jako je v pravo.
![Page 14: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/14.jpg)
Logika v databázivs
Logika v modelu
Premature optimization is the root of all evil ~ Donald Knuth
O tom, jestli patří logika do databáze nebo do modelu můžeme klidně dál vést svaté války, ale v momentě kdy si jednu z cest zvolíte, měli byste ji co nejstriktněji dodržovat. Já volím logiku v modelu a to znamená, že logice v databázi se budu vyhýbat dokud to bude možné.
![Page 15: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/15.jpg)
Batch operace
![Page 16: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/16.jpg)
Batch operace s DQL
“UPDATE/DELETE statements are ported directly into a Database statement and therefore bypass any locking scheme, events and do not increment the version column. Entities that are already loaded into the persistence context will NOT be synced with the updated database state. It is recommended to call EntityManager#clear() and retrieve new instances of any affected entity.”
~ Dokumentace
Dávejte si pozor, pokud píšete SQL, ale i DQL update a delete dotazy, protože Doctrine nemá jak kontrolovat zamykání a nemá jak poznat, které entity obnovit v paměti. Pokud tedy něco takového pustíte, je doporučováno následně vyčistit entity z paměti a načíst si je znovu.
![Page 17: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/17.jpg)
Batch operace s DQL● Zkuste nejprve chytřejší způsoby iterace nad výsledkem
○ ORM\Query::iterate();
○ Stránkování
● Raději DQL update, než SQL update
● Neumí JOINy :(
Předtím než se uchýlíme k DQL updatu, tak je vhodné nejprve zkusit, jestli by nestačilo udělat nějakou chytřejší iteraci nad daty. Buď tedy nějaké custom stránkování výsledků, nebo použití metody iterate(), která umí hydratovat entity postupně a snižujeme tím šanci, že nám dojde paměť, ikdyž potřebujeme zpracovat miliony entit. Jedna z nevýhod ale je, že UPDATE ani DELETE neumí JOIN.
![Page 18: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/18.jpg)
Batch operace s DQL
$select = $em->createQueryBuilder()
->addSelect('orderItem.price')
->from(OrderItem::class, 'orderItem')
->andWhere('orderItem.order = theOrder.id');
$update = $em->createQueryBuilder()
->update(Order::class, 'theOrder')
->set('theOrder.totalPrice', sprintf('(%s)', $select))
->getQuery();
Ovšem to že neumí JOINy se dá celkem snadno řešit pomocí subselectů.
![Page 19: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/19.jpg)
Batch operace s DQL
UPDATE "order"
SET total_price = (
SELECT SUM(o0_.price) AS dctrn__1
FROM order_item o0_
WHERE o0_.order_id = order.id
)
Výraz na minulém slidu by měl vygenerovat SQL podobné tomuto.
![Page 20: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/20.jpg)
Batch operace s DBAL● $connection->prepare('UPDATE ...');
● $connection->createQueryBuilder();
Občas se nejde vyhnout použití SQL na batch operace a pokud to opravdu dává smysl, tak není důvod kvůli tomu skřípat zuby. Práci by vám mohl usnadnit chudší bratříček DibiFluent, tedy QueryBuilder v DBAL.
![Page 21: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/21.jpg)
Rozšiřování DQL
![Page 22: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/22.jpg)
Rozšiřování DQL● TreeWalker - může modifikovat AST● output SqlWalker - generuje samotný SQL dotaz● vlastní funkce
TreeWalker umí modifikovat načtenou strukturu DQL dotazu do stromu objektů, které ho reprezentují (AST) a modifikovat jej. Output SqlWalker umí převést AST na SQL a když si tento výchozí podědíte, můžete si změnit způsob jakým se výsledné SQL z AST DQLka skládá.
![Page 23: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/23.jpg)
Rozšiřování DQL: limitace● Gramatika je jasně definovaná, není možné to nijak ohackovat● Tree Walker může modifikovat výsledné AST, ale opět nezmění gramatiku● SqlWalker to nemá jak zachránit
![Page 24: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/24.jpg)
Rozšiřování DQL: co jdeKam nemůže DQL
SELECT order.finishedTime IS NULL AS something
FROM ...;
Tam musí funkce
SELECT IS_NULL(order.finishedTime) AS something
FROM ...;
Funkce je ovšem možné doplnit téměř kamkoliv a chybějící syntax tedy ve výsledku není problém, protože identickou funkcionalitu snadno doplníme.
![Page 25: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/25.jpg)
Rozšiřování DQL: vlastní funkceclass IsNull extends FunctionNode {
private $expression;
public function getSql(SqlWalker $sqlWalker) {
return $sqlWalker->walkArithmeticPrimary($this->expression) . ' IS NULL';
}
public function parse(Parser $parser) {
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->expression = $parser->ArithmeticPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}Vlastní funkce do DQL musí mít minimálně metodu parse(), která tokeny v DQL převede na node v AST a funkci getSql() která zpracovaný node převede na SQL.
![Page 26: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/26.jpg)
SELECT employee,
AVG(salary) OVER (PARTITION BY employee.department) AS avgSalary
FROM My\Employee employee;
SELECT employee,
WINDOW_OVER(AVG(salary), employee.department) AS avgSalary
FROM My\Employee employee;
SELECT employee,
WINDOW(AVG(salary) OVER (PARTITION BY employee.department)) AS avgSalary
FROM My\Employee employee;
Rozšiřování DQL: komplexnější syntaxe
První ukázka AFAIK není možná docílit, protože mění syntaxi DQL, druhá a třetí ale je, protože naše speciální syntax je obalená do custom funkce.
![Page 27: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/27.jpg)
Rozšiřování DQL: existující knihovny● oro/doctrine-extensions (MySQL, PostgreSQL)● opsway/doctrine-dbal-postgresql (PostgreSQL)● beberlei/DoctrineExtensions (MySQL, Oracle)● syslogic/doctrine-json-functions (MySQL)● další na: https://packagist.org/search/?q=dql
Pár zajímavých balíčků, kde jsou již naimplementovány užitečné funkce do DQL.
![Page 28: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/28.jpg)
Ukázka, jak jedna z knihoven řeší speciální operátory v PostgreSQL.
![Page 29: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/29.jpg)
Hydratace
![Page 30: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/30.jpg)
Druhy hydratace● entity
○ ORM\Query::getResult();
● scalar + entity (mixed results)○ ORM\Query::getResult();
● array - vytvoří strukturu jako pro entity a tu vrátí○ ORM\Query::getArrayResult();
● scalar - přejmenuje sloupce na fieldy a přetypuje○ ORM\Query::getScalarResult();
Né vždy je potřeba hydratovat entity, když bych z nich zase mapoval hned pole, protože je chci jen číst. Dávejte si ale pozor na vracení výsledků z databáze rovnou jako struktury z API, změnou entit a nebo dotazu si tak snadno rozbijete kompatibilitu API.
![Page 31: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/31.jpg)
Native queries
$rsm = new ResultSetMappingBuilder($em);
$rsm->addRootEntityFromClassMetadata(OrderItem::class, 'order_item')
$connection->createQueryBuilder()
->addSelect($rsm->generateSelectClause())
->from(...)
$em->createNativeQuery($sql, $rsm)
Pokud potřebujete opravdu složité filtrační dotazy, které ale chcete zpracovávat jako entity, můžete použít Native Queries, které umožňují napsat libovolné SQL a převést výsledek na entity, stejně jako by to dělala Doctrine.
![Page 32: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/32.jpg)
Shrnutí: co si z toho odnést?● Logika v modelu dokud to jenom trochu jde● Nebát se využívat hojně vlastní typy● Rozšiřování DQL je snadné● Doctrine není určená na batch operace
Kam dál?● Youtube kanál “Nette Framework” > search “Doctrine”
![Page 33: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/33.jpg)
Dotazy?
![Page 34: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/34.jpg)
Díky za pozornost!@ProchazkaFilip
#makeCodeNotWar
![Page 35: Doctrine: co dělat, když entity nestačí](https://reader033.vdocuments.mx/reader033/viewer/2022042723/58cffcc11a28abfc0a8b5ba5/html5/thumbnails/35.jpg)
Díky za pozornost!@ProchazkaFilip
#makeCodeNotWar