Skip to content

Migrer vers le Event Sourcing, partie 2: Etre CQRS en utilisant des DTOs pour les requêtes

by Julien on juillet 15th, 2010

An english version of this post is available here.

Vue d’ensemble

Pour cette première étape de refactorisation, nous voulons utiliser CQRS (séparation des commandes et des requête en français). CQRS veut simplement dire que le composant que l’on utilise pour aller chercher de l’information à afficher (une requête) est différent du composant qui permet d’effectuer une action (une commande). C’est d’ailleurs une bonne pratique à toutes les échelles. Vos classes seront plus facile à maintenir si vos méthodes sont séparées en 2 groupes disctincts comme le suggère Bertrand Meyer:

  • Celles qui retourne void: elles effectuent une action qui change l’état interne de la classe. Elles renvoient des exceptions en cas d’erreur.
  • Celles qui retournent d’autre chose que void: elles renvoient une information, et ne sont pas autorisées à modifier l’état de l’objet

En d’autre termes, poser une question ne devrait pas modifier la réponse. Comme Greg Young le précise, c’est très important dans le cas où vous utilisez des contrats pour vos méthodes: si vous utilisez une méthode qui retourne une information pour vérifier une pré condition, et que votre méthode modifie l’état de l’objet, vous allez vous retrouver avec des situations intéressantes à gérer.

A l’échelle de l’architecture de l’application, cela signifie que nous n’allons plus utiliser NHibernate pour accéder à nos données d’affichage:

Au lieu d’afficher les entités, nous allons afficher des DTOs (Objets de Transfert de Données en français). Un DTO est un simple objet avec des propriétés publiques et aucun comportement associé. Contrairement aux entités qui sont designée pour optimiser l’implémentation des règles d’affaires, les DTOs sont spécialement créés pour optimiser l’affichage. En fait, on peut même dire que les DTOs sont une modélisation logique des données affichées à l’écran. De plus, un DTO contient toutes les données d’un écran, chargées avec une seule requête à la base de données. Dans notre cas, les DTOs sont extraits de la base relationnelle transactionnelle, les DTOs seront donc chargés à partir de requêtes SQL.

Quoi, une requête SQL ? Mais les MOR n’étaient pas sensé nous protéger d’une telle antiquité? En fait, oui, mais cela a un prix. Un prix que nous ne voulons plus payer.

Quel problème y a t-il à utiliser HQL ou Linq pour faire une requête? Tout d’abord, comme dit plus haut, les entités sont optimisées pour traiter les commandes de l’usager. Elles évitent donc la duplication de données, ce qui se traduit en base de données par un schéma de forme normée 3 la plupart du temps. Or les écrans affichent des données qui souvent necessitent plusieurs agrégats. Par example, si vous afficher un étudiant, vous voudrez certainement afficher le nom des classes auxquelles il est inscrit. Cela signifie que votre MOR favoris a besoin de charger les associations, ce qui se fait de façon différé (lazy load en anglais). Si vous ne faites pas attention, vous allez donc faire beaucoup de requêtes en base de données pour chaque écran. De même, vous allez probablement charger un certain nombre de propriétés dont vous n’avez pas besoin. Bien sûr, vous pouvez utiliser des stratégies de chargement (fetching strategies en anglais). Ces stratégies vous permettent pour chaque requête de préciser quelles sont les associations que vous voulez charger immédiatement. Mais cela requiert un niveau de complexité supplémentaire dans votre infrastructure. Finalement, vous aurez certainement besoin de transformer les données de vos entités pour les adapter aux besoin de chaque écran. Mais le plus gros problème est que vos entités ont besoin de supporter l’affichage. Au lieu de se concentrer uniquement sur l’exécution des règles d’affaires. Or les 2 fonctions ont des contraintes très différentes. Idéalement, vous voulez afficher un écran de votre application avec le moins de requêtes possible à votre source de données. Idéalement, les données que vous allez chercher devraient être dénormalisées, afin d’éviter au maximum les jointures. Par contre, du coté transactionnel, vous voulez probablement avoir un modèle de données de forme normée 3.

Séparer le coté transactionnel du coté requêtage permet d’optimiser les 2 cotés indépendamment. Assez parlé, voyons comment faire ce pas important. Regardons une action qui affiche les détails d’un agrégat:

public ViewResult Details(Guid studentId)
{
    return View(studentQueries.ById(studentId));
}

A première vue, peu de chose ont changé. Excepté que l’on utilise plus le repository de l’agrégat pour faire la requête. L’objet retourné n’est plus une entité, mais un DTO.

Avantages

Simplicité

Il n’y a plus besoin de stratégies de chargement, ni de mapping entre les entités et les modèles de vue. Vous n’avez plus besoin que d’une simple requête SQL pour chaque vue.

Plus de getters dans votre domaine!

Vous venez juste de libérer votre domaine des contraintes liées à l’affichage. Dans l’immédiat, cela vous permet de supprimer vos getters dans vos entités. Nous verrons plus tard que cela ouvre d’autres opportunités.

Implémentation

Ce qui change

Il y a un nouveau projet dans la solution pour le modèle de vue:

Ce projet contiens les DTOs ainsi que leur requêtes.

Les repository ont disparu:

En parlant des repository, la méthode IPaginable<T> All() a elle aussi disparue de l’interface.

L’implémentation NHibernate du IPaginable<T> a été remplacée par une implémentation SQL pure (attention, la pagination n’est supportée qu’à partir de SQL Server 2005).

De plus, n’étant pas un fan du SQL dans le code source, un petit composant permet de les écrire dans un fichier XML simple (plus sur le sujet plus tard).

Finalement, un nouveau composant fait son apparition, le mappeur de DTO. Ce composant mappe par convention les résultats d’un IDataReader dans des objets DTOs

Créer un DTO

Dans ce mini framework, un DTO est une simple classe avec un constructeur vide et des propriétés publiques. Par exemple:

public class ClassDTO
{
    public Guid Id { get; set; }
    [Required]
    [StringLength(255, MinimumLength = 1)]
    public string Name { get; set; }
    [Required]
    [Range(3, 6)]
    public int Credits { get; set; }
}

Vous pouvez noter que puisque j’utilise mes DTOs comme modèle de vue également, ils sont l’endroit idéalement pour placer un peu de validation.

Créer une requête

Voici une interface de requête simple:

public interface IClassDTOQueries
{
    IPaginable<ClassDTO> All();
}

Et voici l’implémentation:

public class SQLClassDTOQueries : DTOQueries, IClassDTOQueries
{
    public SQLClassDTOQueries(IPersistenceManager pm)
    : base(pm)
    {
    }

    public IPaginable<ClassDTO> All()
    {
        return ByNamedQuery<ClassDTO>("All", null);
    }
}

La méthode ByNamedQuery() prends 3 arguments:

  • Le nom de la requête (à aller chercher dans le fichier XML)
  • Les paramètres de la requête
  • (Optionnel) La liste des collections du DTO à remplir

Vous pouvez regarder dans l’application exemple pour des requêtes qui utilisent toutes ces fonctions.

Finalement, vous pouvez définir la requête elle même dans un fichier XML qui porte le même nom que votre implémentation:

<?xml version="1.0" encoding="utf-8" ?>
<queries>
<query name="All" defaultSort="Id">
<count>SELECT COUNT(*) FROM Class</count>
<select>
SELECT class.Id,
class.name as Name,
class.credits as Credits
FROM Class class
</select>
</query>
</queries>

Pour chaque requête, vous devez définir:

  • La requête qui compte le nombre total d’éléments (nécessaire pour la pagination).
  • La requête allant chercher les éléments (la “vraie” requête).

Pour les requêtes destinées à retourner un seul élément, la requête qui compte est optionnelle.

Le mappeur de DTO utilisera l’alias des colonnes pour trouver la propriété correspondante dans le DTO. Le mappeur de DTO supporte les propriétés simple, les sous DTOs et la plupart des collections.

Démarrer l’application exemple

Les sources pour toute la série de billets sont disponible sur GitHub à http://github.com/jletroui/TransitioningToEventSourcing
.

Vous aurez simplement besoin de créer une base de données vide intitulée “DDDPart2″ dans SQLExpress avant de lancer l’application.

Liste des billets “migrer vers le Event Sourcing”:

From → Event Sourcing

15 Comments
  1. HI Julien

    Great series, I have one small question. Why did you decide to use your own rolled dto mapper. Did you have a reason to not use Automapper or something similar

  2. Julien permalink

    Thanks for your comment. I am not a great fan of reinventing the wheel. I search for serveral options before rewriting a mapper. NHibernate transformers, IBatis, Automapper. But all had problems, due to the fact they have not been designed and optimized for this particular use. Some are not supporting collections, some are very verbose and do not support mapping by convention, etc.. Given this is a fairly small amount of code, I preferred do my own instead of spending more time adapting existing mappers to my needs.

  3. James permalink

    If your Aggregates don’t utilize getters, how do you unit test your domain (for instance, when asserting before/after state)?

  4. Julien permalink

    @James

    A solution could be to keep assembly private getters purely for testing purposes. It is then possible to specify that the test assembly has access to the domain assembly private members in the Assembly.cs file. After step 4, the tests can rely on the stronger events instead.

  5. Dypsok permalink

    Bonjour,
    vous indiquer dans cette article que vos dto pour l’affichage sont idéalement “chargés avec une seule requête à la base de données”
    mais les sources sur github montrent que jusqu’au bout de l’exercice plusieurs requêtes sont requises (ce qui me semble tout à fait normal cela dit) , comme par exemple pour afficher la vue de détail d’un étudiant et qui plus est en continuant d’utiliser des jointures…
    Voici les requêtes auxquelles je fais mention :

    SELECT student.Id,
    student.firstName AS FirstName,
    student.lastName AS LastName,
    student.hasGraduated AS HasGraduated
    FROM Student student
    WHERE student.Id = @Id;

    SELECT reg.Id,
    class.Id AS [Class.Id],
    class.name AS [Class.Name],
    class.credits AS [Class.Credits]
    FROM Registration reg
    INNER JOIN Class class ON class.Id = reg.classId
    WHERE aggregateRoot = @Id;

    Dans ce sens il me semble que même avec une approche CRUD on peut avoir le même comportement… Utiliser les mêmes requêtes pour le même résultat… La dénormalisation elle même sous forme de vues SQL ou bien calculée sur chaque changement appliqué à l’entité est possible avec un modèle CRUD classique… Du coup je ne suis pas sûr que les arguments avancés en faveur d’une approche CQRS/ES soient pertinents dans ce cas!
    J’aimerais avoir votre avis sur la question.

  6. Julien permalink

    Merci pour votre commentaire! Vous soulevez un bon point. Le “view model” de l’exemple est juste un point de départ. Grâce au event sourcing, vous pouvez maintenant le dé-normaliser, et avoir une table par vue, là ou c’est nécessaire pour des raisons de performance par exemple. C’est uniquement parce que vous avez une séparation entre la partie transactionnelle de l’application (les agrégats) et la partie lecture que vous avez la liberté de dé-normaliser votre “view model”. Vous n’avez pas cette liberté avec un modèle CRUD.

Trackbacks & Pingbacks

  1. Transitioning to Event Sourcing, part 1: the DDD “light” application | Julien's blog
  2. Tweets that mention Transitioning to Event Sourcing, part 2: go CQRS with DTOs | Julien's blog -- Topsy.com
  3. Transitioning your DDD “light” application to CQRS and Event Sourcing | Julien's blog
  4. Transitioning to Event Sourcing, part 4: track state changes | Julien's blog
  5. Transitioning to Event Sourcing, part 3: commands | Julien's blog
  6. Transitioning to Event Sourcing, part 5: use events for updating your domain database | Julien's blog
  7. Transitioning to Event Sourcing, part 6: store events | Julien's blog
  8. Transitioning to Event Sourcing, part 7: build a view model | Julien's blog
  9. Transitioning to Event Sourcing, part 8: remove the domain database, go event sourced | Julien's blog

Leave a Reply

Note: XHTML is allowed. Your email address will never be published.

Subscribe to this comment feed via RSS