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

Doctrine, objet typé, et colonne JSON

Doctrine, objet typé, et colonne JSON

Le dépot git pour la démo : https://github.com/lyrixx/symfony-doctrine-inlined-properties

Les bases de données savent gérer des colonnes JSON depuis des années déjà, et
ces colonnes permettent d'accélérer le développement en simplifiant le code, les
migrations, et la maintenance.

Cependant, manipuler un array PHP n'est pas pratique : les analyseurs statiques
de code sont perdus (a moins de spécifier énormément de chose via de la PHPDoc),
PHP ne peut pas controller le type au runtime, mais surtout la lisibilité du
code est réduite. En effet, à moins de lire tout le code, il est difficile de
savoir quelles sont les clés obligatoires, lesquelles sont optionnelles, et
enfin comment est typée la donnée.

À travers cette présentation, nous allons voir comment étendre Doctrine pour
avoir le meilleur des deux mondes : des colonnes en JSON, et des objets PHP
fortement typés.

Grégoire Pineau

April 07, 2022
Tweet

More Decks by Grégoire Pineau

Other Decks in Technology

Transcript

  1. Doctrine, objet typé,
    et colonne JSON
    AFUP Day Lille - 2023
    Grégoire Pineau
    @lyrixx
    Dév @JoliCode
    Core Team @Symfony
    1

    View Slide

  2. Problématique
    2

    View Slide

  3. 3

    View Slide

  4. Un CMS flexible
    ● Plusieurs types de blocs (Image, Text, TopTravel, …). Plus d’une vingtaine de blocs disponibles
    ● Chaque bloc a sa configuration :
    ○ Des propriétés (alt, src, responsive, content, destination, …)
    ○ Des valeurs par défaut (responsive=true, …)
    ○ Son propre système de rendu
    ● On peut composer les blocs
    ○ Text + Image
    ○ Text + Vidéo
    4

    View Slide

  5. La configuration d’un bloc
    5

    View Slide

  6. L’objectif
    ● Eviter la duplication de code
    ● Pouvoir stocker les blocs en base de données
    ● Avoir au maximum des objets PHP
    ● Et si possible un objet par type de bloc
    ● Avoir un système performant : éviter les requêtes SQL inutiles
    (autant lors d’un SELECT que lors d’un UPDATE)
    6

    View Slide

  7. L’entité Block
    7

    View Slide

  8. Comment faire ?
    8

    View Slide

  9. Quelles sont les solutions ?
    1. Utiliser l'héritage doctrine
    ○ MappedSuperClass
    ○ Single table
    ○ Class table
    2. Stocker du PHP sérialisé
    3. Stocker du PHP sérialisé en JSON
    4. Existe-t-il d’autres solutions ? le mystère reste entier…
    9

    View Slide

  10. MappedSuperClass
    10

    View Slide

  11. MappedSuperClass
    Permet de définir des propriétés communes à différentes classes
    Ou pour définir les propriétés par défaut d’une classe située dans un bundle tiers
    L'application pourra ensuite ajouter d'autres propriétés
    Cependant, on ne peut pas mettre de relation vers la MappedSuperClass
    Ce n’est pas une solution adaptée à notre besoin
    11

    View Slide

  12. Single Table
    12

    View Slide

  13. Single Table
    ● Une seule table, mais remplie de vide
    ● Impossible d'avoir deux classes avec une propriété ayant le même nom mais un type différent
    ● Impossible d'avoir des champs non null dans une classe fille
    ● Problème de performance avec une relation car doctrine ne peut pas créer de proxy
    Avec beaucoup de blocs différents, on perd beaucoup d’espace disque. Et le typage est faible !
    13
    object 1 object 2
    id col_1 col_2 col_3 col_4
    1 foo bar
    2 hello world

    View Slide

  14. Class Table
    14

    View Slide

  15. Class Table
    ● Une table par type de bloc
    ● Beaucoup de join ➡ performances réduites
    ● Problème de performance avec une relation car doctrine ne peut pas créer de proxy
    Avec une table par type de bloc, la base de données va comporter beaucoup de tables ce qui peut être
    gênant. De plus, les performances vont être trop impactées
    15
    object 2
    id col_3 col_4
    2 hello world
    object 1
    id col_1 col_2
    1 foo bar

    View Slide

  16. Du PHP sérialisé
    16

    View Slide

  17. ● Doctrine va sérialiser la configuration via serialize() / unserialize()
    ● Avoir du PHP sérialisé dans la DB est un NO GO
    ○ Non interopérable
    ○ Peut comporter un \0
    ○ Si on déplace ou renomme ou modifie une classe, la donnée ne sera plus déserialisable
    Il ne faut jamais stocker du PHP sérialisé dans une base de données
    Du PHP sérialisé
    17

    View Slide

  18. Du PHP sérialisé en JSON
    18

    View Slide

  19. Du PHP sérialisé en JSON
    ● Doctrine va sérialiser la configuration via json_encode() / json_decode()
    ● La configuration est sous forme de tableau PHP. So 2013 󰘮
    On perd l’objectif initial d’avoir une classe par type de block
    19

    View Slide

  20. Existe-il une autre solution ?
    ⚠ expérimentale ⚠
    20

    View Slide

  21. Type doctrine
    21

    View Slide

  22. Les types customs Doctrine
    22

    View Slide

  23. Les types customs Doctrine
    23

    View Slide

  24. Les types customs Doctrine
    24

    View Slide

  25. Configuration Interface
    25

    View Slide

  26. Implémentation du Type
    26

    View Slide

  27. Implémentation du Type
    27

    View Slide

  28. Attention
    Si vous êtes sensible ou psychorigide, la slide suivante va vous choquer !
    Vous pouvez checker twitter pendant les 60 prochaines secondes
    A tout de suite
    28

    View Slide

  29. Implémentation du Type 3 (😨)
    Fixé dans doctrine 4.0
    https://github.com/doctrine/dbal/pull/5036
    29

    View Slide

  30. Utilisation
    30

    View Slide

  31. 31

    View Slide

  32. 32
    Contenu de la base de données

    View Slide

  33. 🎉🎉 🎉🎉
    Ça marche
    … enfin presque
    33

    View Slide

  34. “Ça marche pas” ©
    34

    View Slide

  35. Requête SQL effectuée
    35

    View Slide

  36. L’Unit Of Work (UOW)
    36

    View Slide

  37. L’unit of work
    37
    ● Pièce centrale de Doctrine
    ● Probablement la plus complexe
    ● 3685 lignes de code
    ● Vous ne voudriez sûrement pas avoir à la debugger 🤗
    ● https://github.com/doctrine/orm/blob/2.11.x/lib/Doctrine/ORM/UnitOfWork.php

    View Slide

  38. L’unit of work (2)
    38
    ● Encapsulée par l’entity manager
    ● Contient un cache de tous les objets récupérés de la DB 💡
    ● Gère les écritures en base de données
    ● Gère les transactions
    ● S’occupe de savoir quel objet doit être INSERT / UPDATE / DELETE
    ● Protips: pas besoin de persist() un objet qui vient de la DB
    ● S’occupe de gérer les dépendances entre les objets
    ● Fait des snapshots des objets qu’il rencontre pour faire un diff au moment du flush()

    View Slide

  39. Pourquoi ça ne fonctionne pas
    ● Comme Doctrine fait un snapshot du block avec sa
    configuration
    ● Et comme la configuration est un object
    ● Et comme les objets sont passés par référence
    ➡ Doctrine ne voit pas la différence sur notre object
    ↪ Doctrine ne sauvegarde pas notre objet
    39

    View Slide

  40. Listener
    40

    View Slide

  41. Les events Doctrine
    41
    ● Doctrine émet beaucoup d'événements durant son cycle de vie :
    ○ preRemove + postRemove
    ○ prePersist + postPersist
    ○ preUpdate + postUpdate
    ○ postLoad
    ○ loadClassMetadata + onClassMetadataNotFound
    ○ preFlush + onFlush + postFlush
    ○ onClear
    ● Comme avec Symfony, il est possible d’ajouter de nouvelles fonctionnalités en écoutant ces
    événements
    ● Les extensions Doctrine se basent sur ces événements

    View Slide

  42. Les events Doctrine - Protips
    ● Ne pas mettre votre logique métier dans un listener.
    À n’utiliser que pour les traitements génériques.
    ● Ne pas flusher dans un listener.
    42

    View Slide

  43. Un listener Doctrine
    43
    ● postLoad & postPersist : pour sauvegarder les données initiales
    ● preFlush : on calcule un diff, et on dit à Doctrine de sauvegarder si besoin
    ● onFlush : on effectue les changements en base de données
    ● onClear : on supprime les données sauvegardées
    Environ 140 lignes de code
    Le code est disponible sur GitHub 🔍

    View Slide

  44. Un listener Doctrine
    44

    View Slide

  45. Un listener Doctrine
    45

    View Slide

  46. Un listener Doctrine
    46

    View Slide

  47. Un listener Doctrine
    47

    View Slide

  48. Un listener Doctrine
    48

    View Slide

  49. 🎉🎉 🎉🎉
    Ça marche
    … vraiment
    49

    View Slide

  50. Limitations
    ● Que faire si on supprime une classe ?
    ● Migrations de données
    ● Performance de la sérialisation
    50

    View Slide

  51. Conclusion
    ✓ Avoir des objects PHP fortementements typé
    ○ Une classe par type de bloc
    ✓ Avoir une colonne JSON
    ○ Qu’on peut requêter
    ✓ Performant
    ✓ Scalable
    ✓ Maintenable
    ✓ Facile d’utilisation - Transparent
    51

    View Slide

  52. D’autres cas d’utilisation
    52

    View Slide

  53. D’autres cas d’utilisation
    53
    ● Stocker des données marketing dans la classe User
    ● Stocker les préférences mail d’un utilisateur
    ● Stocker la configuration des notifiers que des utilisateurs pourraient ajouter
    ● Stocker la configuration d’un produit
    ● Dénormaliser de la donnée, comme une souscription Stripe

    View Slide

  54. Peut être que…
    https://github.com/dunglas/doctrine-json-odm
    Ou peut être que cette fonctionnalité sera un jour native à Doctrine
    Ou peut être le mettre dans le DoctrineBridge
    À nous de faire du lobbying 󰙣
    54

    View Slide

  55. Merci
    55
    https:/
    /github.com/lyrixx/symfony-doctrine-inlined-properties

    View Slide

  56. Des questions ?
    Symfony Live Paris - 2022
    Grégoire Pineau
    @lyrixx
    Dév @JoliCode
    Core Team @Symfony
    https:/
    /github.com/lyrixx/symfony-doctrine-inlined-properties
    56

    View Slide