Open nfaugout-lucca opened 6 years ago
Je me suis peut-être mal exprimé; j'avais compris qu'on abandonnait la possibilité de charger dynamiquement des sous-entités, les objets exposés devenant moins dynamiques et plus liés à leur contexte, càd avec seules les propriétés correspondant à leur usages dans leur boundedContext. Par exemple, pour un objet à valider, on pourrait trouver la propriété:
{
...
validator : { id : 3, displayName: "lulu", mail: "mail@lol.com"}
}
avec validator
portant le type "Validator
" ne portant QUE les propriétés ayant du sens dans le contexte dans lequel est utilisé l'objet parent (le boundedContext en fait), et toujours chargé avec l'entité. Cela avait plusieurs avantages:
Finalement, il ne fallait pas voir ces propriétés comme des includes automatiques, mais comme des propriétés directement extraites de la vue SQL, donc rapatriées de manière performante (potentiellement)
Donc pour répondre à ta question, je ne préconise pas de ne jamais charger de sous-objets, je suggères de n'avoir qu'un seul format de retour du SQL des objets; quand on demande les objets en base, ils reviennent toujours complets.
Il y a 2 points :
les includes, qu'ils soient automatiques ou non resteront des Include( ) EF à ne pas oublier dans le Repository. Et comme EF fait sauter les Include( ) si tu les mets en début de req LINQ (voir si c'est toujours le cas avec EFCore), on ne peut même pas dire que ces includes peuvent être noyés dans le Set( ) du Repo. Donc à priori on garderait un ApplyInclude( ) dans le Repository pour gérer de façon non contextuelle les includes. Tu es OK avec ça ? Ou alors tu nous propose une solution plus précise qui ne nécessite pas d'includes EF.
si une entité embarque ses sous entités spécifiques (mais correspondant à des concepts représenté via des API spécifiques, comme les users par ex), on va avoir un pb d'invalidation du cache. Dans l'exemple que tu cites, si ton objet n'est pas modifié (donc pris dans le cache HTTP), mais que le "validator" a changé de mail (via l'API des users), alors ton objet se met à exposer un mail obsolète.
Deux possibilités pour gérer ce dernier point :
soit on accepte le risque d'inconsistance du cache, qu'on règle par différents moyens (ex : le cache de ton objet ne dure de toute façon que 24h max, donc dès le lendemain le changement de mail sera "pris en compte" dans toutes les autres API référençant le user.
soit on part sur une version où ton objet n'expose (et ne peut exposer) que (id, name, url), auquel cas le seul "risque" d'incohérence de cache est au niveau du name de la ressource (=limité), en revanche ça demande au front de faire bcp plus d'appels. Mais on espère compenser ce nb plus grand d'appels par le fait que 99% d'entres eux se solderont par une utilisation du cache, donc au final ça devrait aller plus vite. Et HTTP/2 devrait également solutionner ce pb du nb d'appels.
Perso je suis pour cette dernière option, qui nous permet de faire un POC agressif sur le cache HTTP.
La "bonne" nouvelle, c'est qu'on n'a pas besoin de faire d'évo à RDD pour tester ça.
En effet, on peut tout à fait développer une appli (genre LuccaFaces) en s'interdisant depuis le front de demander autre chose que (id, name, url) sur les sous ressources. Côté back mettre en place la gestion du cache (TTL, ETag & co sur les entités), et valider le POC.
Et si ça marche, on fait une évo à RDD v2.2 pour gérer en natif ce genre d'approche.
Concernant l'approche BC / redéfinition des concepts, comme ce que tu proposes avec Validator, ça n'est pas incompatible avec une approche radicale au niveau cache HTTP. En effet, au niveau Domain, on peut très bien avoir une richesse des objets, avec des sous objets complets, simplement au moment de la sérialisation, on les "coupe" à (id, name, url). C'est pour ça que je voulais gérer ça au niveau Web.
Je pense à une alternative qui ne nécessite aucune adaptation au niveau sérialisation, et qui consiste à utiliser des interfaces, des implémentations explicites, et de jouer sur le internal/public qu'on a déjà pour indiquer à Web les props qu'on veut sérialiser ou non. Voici ce que ça donnerait avec ton exemple :
IEntityWithValidator
qui impose une prop IValidator
qui n'a que (Id, Name, Url), et qui sera public au niveau de ton objet
Un sous objet internal Validator Validator { get; set }
pour l'utilisation dans le Domain.
Ainsi l'objet aurait 2 props :
internal Validator Validator { get; set }
public IValidator IEntityWithValidator.Validator { get { return Validator; } }
Perso je préfère gérer le truc au niveau Web/Sérialization pour éviter d'ajouter du "bruit" au niveau du Domain, mais c'est la même idée.
Attention avec la 2ème solution. Il y a de gros risques de créer des "chatty apis" :
Oui c'est exactement ce qu'on veut faire ici 😄
La différence, c'est qu'on pense que le cache HTTP va régler tous les pb évoqués dans ces 3 posts.
Peut-être qu'on se trompe, et c'est pour ça qu'on va le faire sur un POC, "pour voir", et on reviendra peut-être sur des réponses partielles sans gestion de cache, ou avec une gestion différente.
Mais si on ne teste pas, on ne saura pas ;)
En outre, même si notre back end REST expose des "chatty API", si on voit que c'est lourd, on pourra abattre une dernière carte qui consiste à mettre un back end GrapQL entre le client et le back end REST, qui aura pour job de reconstituer des réponses aggrégées customisées en fonction du front, tout en s'appuyant sur des API REST chatty derrière.
Donc j'ai bon espoir qu'on se mette d'accord pour tester ce nouveau terrain de jeu.
Bah pour moi la solution 2 ne résout pas ce problème d'un point de vue "client". Seulement du point de vue "ressources serveur". Ça veut pas du tout dire que je suis contre le cache http, bien au contraire. Mais la solution "on référence tout par id,name,url" me semble être un peu la solution de facilité pour nous, mais pas trop pour les clients consommateurs 😉
Tu marques un point, car il faut penser aux intégrations et pas seulement à nos applis front.
On a 3 types de consommateurs de nos API :
les end users, qui passent par nos applis front, et pour qui le cache HTTP est fait. Le front peut soit passer en direct (avec le pb de chatty API tel que décrit), soit on crée un intermédiaire GraphQL qui fait que le front consomme les API comme "avant", avec des réponses partielles
nos applis mobiles, pour lesquels on a le mm pb de cache HTTP que pour les applis front, encore plus je dirais d'ailleurs ! Donc même combat
nos clients/partenaires, via des clés d'API. Eux c'est différent car à leur place en effet je préfèrerais largement avoir toujours des données "fraîches" pour éviter justement des pb de cache ou autre, ET je voudrais également des réponses partielles. Sachant cela, on pourrait décider que pour ces "principaux" là, on autorise les réponses partielles, d'autant plus simple à implémenter que le Domain aurait toujours ces sous objets complets, il suffirait donc de mettre en route la découpe de la sérialisation uniquement dans les 2 premiers types de principaux.
Si on veut utiliser le cache HTTP, il faut que nos API renvoie des entités sans enfant, uniquement avec des FK vers les entités liées.
Techniquement, y'a rien à changer dans le Domain, càd qu'il doit continuer à travailler sur des grappes d'entités complètes. C'est dans la couche Web que, même si on a les enfants sous la main, on coupe le lien au moment de la sérialisation en ne renvoyant que les (id, name, url) de ces sous objets.
Du coup, on peut très bien supporter les 2 approches dans la même version de RDD, càd avoir 2 sérialieurs :
Le fait de supporter les 2 et de gérer ça au niveau Web permet de ne pas remettre massivement en cause RDD.Domain, et donc finalement de pouvoir changer d'avis, si jamais un premier POC sur un projet cobaye s'avère catastrophique.
@Poltuu d'après tes remarques dans l'Issue principale, j'ai l'impression que tu préconises de ne jamais charger de sous entités même dans le Domain, tu confirmes ? Pour ma part je pense qu'on ne devrait gérer ça qu'au niveau de la couche Web, car ça nous permet de continuer à travailler sur des grappes d'objets complets au niveau Domain.
Concernant les "includes", ils sont gérés au niveau des Repositories (Infra), et la différence avec Lucca, c'est qu'ils ne sont plus contextuels, càd qu'il ne faut plus faire des includes en fonction de ce que le demandeur à mis dans les Fields, mais bien faire des includes systématiques qui permet de ramener au Domain un objet toujours complet, avec toutes ses dépendances.
On a vu que c'était lourd de faire des includes systématique dans le monolithe car plus on ajoute de responsabilité sur les entités racines, plus on leur rattache une grosse grappe de données. La "bonne" solution à ce pb n'est pas d'arrêter les includes, mais plutôt de découper une application monolithique en sous applications (ou bounded contexts), ce qui permet de n'avoir qu'une partie de la grappe pour un même concpet, selon le BC dans lequel il se trouve.