Linq
Par Jean-Baptiste le mardi 8 avril 2008, 19:06 - Code - Lien permanent
Je disais en février que j'avais un peu de mal encore à voir comment utiliser Linq. Depuis, de l'eau a un peu coulé sous les ponts, et des articles intéressants ont commencé à voir le jour à droite à gauche donnant une orientation intéressante à cette techno. A mon tour donc d'y aller de ma participation.
Avant de commencer, pour les impatients il est possible de télécharger le source de mes essais. Ce billet n'a pas vocation à être dogmatique, et j'attends avec une certaine impatience d'éventuels retours.
Bien, la mise au point étant faite, commençons.
Première approche
La première idée qui m'est venue à l'esprit est que les repositories retournent un IQueryable, et puissent également prendre en paramètre des query expressions. Ce billet [1] décrit assez bien cette approche, et voici un petit résumé de code:
[csharp] IQueryable<T> Find(Func<IQueryable<T>, IQueryable<T>> transformer)
Ce qui nous permet donc d'écrire ça:
[csharp]
var results = Repository<Customer>.Find(
customers => from c in customers
where c.FirstName == "Rob"
select c
);
Je trouve ça finalement assez moyen que les repositories retournent un IQueryable. Les repositories ne sont pas une couche d'accès aux données ou de requêtage, mais des objets du domaine capable de retourner des références vers des AggregateRoot globaux. De plus, nous savons que nous sommes dans une repository de Customer, et pourtant nous écrivons toute la partie de sélection de notre requête. Finalement, la seule partie qui nous intéresse c'est tout ce qui concerne le where.
N'utilisons que les prédicats
Ce billet utilise cette approche, et fait finalement un excellent résumé du problème:
jusque là, pour abstraire mon domaine de la notion de criteria NHIbernate, j'écrivais ma propre solution métier, et l'implémentation de repository avait la charge de traduire ça en quelque chose de compréhensible pour elle. Linq nous épargne normalement tout ce travail de traduction puisque c'est justement ça le coeur de son métier: exprimer des requêtes indépendamment du fournisseur de données. Ensuite, ça tombe bien, il se trouve que LinqToNHibernate existe déjà et sert donc à traduire une requête linq en criteria NHibernate
Du coup l'idée de base est d'utiliser une méthode ressemblant à ça:
[csharp] IList<T> Find(IList<Func<T, bool>> criteria)
C'est déjà beaucoup mieux, et nous pouvons écrire quelque chose comme ça:
[csharp] IList<Func<Customer, bool>> criteria = new List<Func<Customer, bool>>(); criteria.Add(c => c.Name == "test"); criteria.Add(c => c.SocialNumber == "jhkjh"); Repositories.Customers.Find(criteria);
C'est bien, mais pas top comme dirait l'autre. A utiliser ce n'est pas forcément très élégant, je n'ai jamais été très fan des add en série. De plus la signature de notre méthode est peut être un poil effrayante. Avec cet approche nous nous limitons à faire nécessairement ou ET entre tous nos prédicats.
Le retour du pattern Specification (mais était-il seulement parti?)
L'idée est de revenir un peu au pattern Specification, en lui ajoutant la possibilité de se traduire en expression:
[csharp]
public class Criteria<T>
{
public abstract Expression<Func<T, bool>> GetExpression();
public abstract bool IsSatisfiedBy(T element);
}
Voilà pour la classe de base. J'ai laissé le IsSatisfiedBy à titre purement informatif.
Avec un peu de travail, et l'utilisation de Criteria composite (et merci à cet article pour certains détails techniques) , on peut se retrouver à écrire le code suivant:
[csharp]
Criteria<Customer> criteria = CustomerCriteria.NameIs("toto")
.Or(CustomerCriteria.NameIs("un test"));
IList<Customer> result = Repositories.Customers.Find(criteria);
L'avantage de toujours utiliser nos propres critères est je trouve multiple:
- on rend l'utilisation du domaine plus explicite,
- on encapsule le comportement des expressions,
- on peut se servir des ces objets directement dans une couche de présentation pour matérialiser une recherche faite par un utilisateur.
Implémentation
Côté implémentation de repository avec NHibernate, en fait tout est très simple. LinqToNHibernate ajoute une extension method à notre session: Linq<T>, qui nous ressort donc un IQueryable sur le type d'entité voulu. A partir de là, on peut appliquer toute la sauce linq habituel.
La méthode find décrite plus haut peut s'écrire de la sorte:
[csharp]
public IList<T> Find(Criteria<T> criteria)
{
IQueryable<T> query = from item in Session.Linq<T>() select item;
query.Where(criteria.GetExpression());
return query.ToList<T>();
}
Limites et critiques
Le fait de ne pas jouer directement avec un IQueryable nous limite de fait à ne pas pouvoir utiliser entièrement les query expressions côté domaine. Personnellement, c'est exactement ce que je cherchais, car le but à la base des repositories est justement d'encapsuler ce genre de comportements. Pour fermer l'utilisation de mon domaine, tout en utilisant la puissance de linq, il est clair que j'ai eu besoin de pas mal de code en plus, et qu'il va falloir encore plus en quand le domaine va se complexifier, notamment si on veut définir tous les prédicats possible.
Actuellement, il n'est pas non plus possible de trier côté base dans mon implémentation.
Par rapport à ce billet, il n'est qu'un résumé (trop) rapide du cheminement qui m'a conduit à la dernière approche. Me connaissant j'ai du passer à côté de l'explication de concepts essentiels, donc n'hésitez pas encore une fois à poser des questions via les commentaires.
Le code
Il est possible de télécharger mon source ici, cependant j'ai plusieurs remarques. Tout d'abord, les noms des méthodes et des classes varient un peu, car j'ai été un peu paresseux et je n'ai pas nécessairement tout bien renommé comme il faut. Ensuite, le fichier FOO.gdb livré dans l'archive est la base de données Firebird que j'ai utilisé. Libre à vous bien entendu de vous en servir.
EDIT
Je viens d'uploader une nouvelle version, car une boulette s'était glissée dans le code suite à un refacto hasardeux. Merci à Charles pour sa précieuse contribution 
Notes
[1] je n'aime pas beaucoup la manière d'utiliser les repositories de ce billet. Nulle part il n'est dit explicitement dans le domaine quelle repository est utilisable, ce que je trouve assez dangereux en plus de brouiller la notion d'AggregateRoot



Commentaires
Thanks for this information. This is very eye-opening. It seems as though Slenda is not so splendid!