Alternatives au modèle de référentiel pour encapsuler la logique ORM?

24

Je viens de devoir désactiver un ORM et c'était une tâche relativement intimidante, car la logique de requête fuyait partout. Si j'avais déjà eu à développer une nouvelle application, ma préférence personnelle serait d'encapsuler toute la logique de requête (à l'aide d'un ORM) pour la protéger du changement. Le modèle de référentiel est assez gênant à coder et à maintenir, donc je me demandais s'il y avait d'autres modèles pour résoudre le problème?

Je peux prévoir des articles sur le fait de ne pas ajouter de complexité supplémentaire avant qu'elle ne soit réellement nécessaire, d'être agile, etc. mais je ne suis intéressé que par les modèles existants qui résolvent un problème similaire de manière plus simple.

Ma première pensée a été d'avoir un référentiel de type générique, auquel j'ajoute des méthodes selon les besoins à des classes de référentiels de type spécifiques via des méthodes d'extension, mais les tests unitaires des méthodes statiques sont terriblement douloureux. C'EST À DIRE:

public static class PersonExtensions
{
    public static IEnumerable<Person> GetRetiredPeople(this IRepository<Person> personRep)
    {
        // logic
    }
}
Dante
la source
5
En quoi consiste exactement le modèle de référentiel que vous trouvez difficile à coder et à maintenir?
Matthew Flynn
1
Il existe des alternatives. L'une d'elles utilise le modèle d'objet de requête qui, bien que je ne l'ai pas utilisé, semble être une bonne approche. Le référentiel à
mon humble avis

Réponses:

14

Tout d'abord: un référentiel générique doit être considéré comme une classe de base et non comme des implémentations complètes. Cela devrait vous aider à mettre en place les méthodes CRUD courantes. Mais vous devez toujours implémenter les méthodes de requête:

public class UserRepository : EntityFrameworkRepository<User>, IUserRepository
{
    public UserRepository(DbContext context){}

    public ICollection<Person> FindRetired()
    {
        return context.Persons.Where(p => p.Status == "Retired").ToList();
    }
}

Seconde:

Ne reviens pas IQueryable. C'est une abstraction qui fuit. Essayez d'implémenter cette interface vous-même ou utilisez-la sans connaître le fournisseur de base de données sous-jacent. Par exemple, chaque fournisseur de base de données possède sa propre API pour charger les entités avec impatience. C'est pourquoi c'est une abstraction qui fuit.

Alternative

Comme alternative, vous pouvez utiliser des requêtes (par exemple, comme défini par le modèle de séparation commande / requête). CQS est décrit dans wikipedia.

Vous créez essentiellement des classes de requête que vous invoquez:

public class GetMyMessages
{
    public GetMyMessages(IDbConnection connection)
    {
    }


    public Message[] Execute(DateTime minDate)
    {
        //query
    }
}

la grande chose avec les requêtes est que vous pouvez utiliser différentes méthodes d'accès aux données pour différentes requêtes sans encombrer le code. vous pouvez par exemple utiliser un service Web dans un, nhibernate dans un autre et ADO.NET dans un troisième.

Si vous êtes intéressé par une implémentation .NET, lisez mon article: http://blog.gauffin.org/2012/10/griffin-decoupled-the-queries/

jgauffin
la source
Le modèle alternatif que vous avez suggéré ne créerait-il pas encore plus d'encombrement sur des projets plus importants, car ce qui serait une méthode dans le modèle de référentiel est maintenant une classe entière?
Dante
3
Si votre définition d'avoir de petites classes est encombrante, eh bien oui. Dans ce cas, vous ne devez pas non plus essayer de suivre les principes SOLID car leur application produira toujours plus de classes (plus petites).
jgauffin
2
Les classes plus petites sont meilleures, mais la navigation des classes de requête existantes ne serait-elle pas plus gênante que de parcourir l'intellisense d'un référentiel (domaine) pour voir si la fonctionnalité nécessaire se trouve là-dedans et sinon, l'implémenter.
Dante
4
La structure du projet devient plus importante. Si vous placez toutes les requêtes dans un espace de noms comme YourProject.YourDomainModelName.Queriesil sera facile de naviguer.
jgauffin
Une chose que je trouve difficile à faire dans le bon sens avec CQS est les requêtes courantes. Par exemple, étant donné un utilisateur, une requête obtient tous les étudiants associés (notes différentes, propres enfants, etc.). Maintenant, une autre requête doit obtenir des enfants pour un utilisateur, puis effectuer certaines opérations sur eux - la requête 2 peut donc tirer parti de la logique commune de la requête 1, mais il n'existe aucun moyen simple de le faire. Je n'ai pas pu trouver d'implémentation CQS qui facilite cela facilement. Les référentiels aident beaucoup à cet égard. Pas un fan de l'un ou l'autre modèle, mais juste partager mon point de vue.
Mrchief
8

Tout d'abord, je pense que l'ORM est déjà une abstraction suffisamment grande pour votre base de données. Certains ORM fournissent une liaison à toutes les bases de données relationnelles courantes (NHibernate a des liaisons avec MSSQL, Oracle, MySQL, Postgress, etc.). Donc, construire une nouvelle abstraction par-dessus ne me semble pas rentable. En outre, arguant du fait que vous devez "résumer" cet ORM n'a pas de sens.

Si vous voulez toujours construire cette abstraction, j'irais à l'encontre du modèle de référentiel. C'était génial à l'ère du SQL pur, mais c'est assez gênant face à l'ORM moderne. Principalement parce que vous finissez par réimplémenter la plupart des fonctionnalités ORM, comme les opérations CRUD et les requêtes.

Si je devais construire de telles abstractions, j'utiliserais ces règles / modèles

  • CQRS
  • Utilisez autant que possible les fonctionnalités ORM existantes
  • Encapsuler uniquement la logique complexe et les requêtes
  • Essayez de cacher la «plomberie» de l'ORM à l'intérieur d'une architecture

Dans la mise en œuvre concrète, j'utiliserais directement les opérations CRUD d'ORM, sans aucun emballage. Je ferais également des requêtes simples directement dans le code. Mais les requêtes complexes seraient encapsulées dans leurs propres objets. Pour «masquer» l'ORM, j'essayerais d'injecter le contexte de données de manière transparente dans un service / objets d'interface utilisateur et je ferais de même pour interroger les objets.

La dernière chose que je voudrais dire, c'est que beaucoup de gens utilisent ORM sans savoir comment l'utiliser et comment en tirer le meilleur profit. Les personnes recommandant des référentiels sont généralement de ce type.

En tant que lecture recommandée, je dirais le blog d'Ayende , en particulier cet article .

Euphorique
la source
+1 "La dernière chose que je voudrais dire, c'est que beaucoup de gens utilisent ORM sans savoir comment l'utiliser et comment en tirer le meilleur profit. Les personnes recommandant des référentiels sont généralement de ce type"
Misters
1

Le problème avec l'implémentation qui fuit est que vous avez besoin de nombreuses conditions de filtre différentes.

Vous pouvez affiner cette API si vous implémentez une méthode de référentiel FindByExample et l'utilisez comme ceci

// find all retired persons
Person filter = new Person {Status=PersonStatus.Retired};
IEnumerable<Person> found = personRepository.FindByExample(filter);


// find all persons who have a dog named "sam"
Person filter = new Person();
filter.AddPet(new Dog{Name="sam"});
IEnumerable<Person> found = personRepository.FindByExample(filter);
k3b
la source
0

Pensez à faire fonctionner vos extensions sur IQueryable au lieu d'IRepository.

public static class PersonExtensions
{
    public static IQueryable<Person> AreRetired(this IQueryable<Person> people)
    {
        return people.Where(p => p.Status == "Retired");
    }
}

Pour effectuer un test unitaire:

List<Person> people = new List<Person>();
people.Add(new Person() { Name = "Bob", Status = "Retired" });
people.Add(new Person() { Name = "Sam", Status = "Working" });

var retiredPeople = people.AsQueryable().AreRetired();
// assert that Bob is in the list
// assert that Sam is not in the list

Aucune moquerie fantaisiste n'est requise pour les tests. Vous pouvez également combiner ces méthodes d'extension pour créer des requêtes plus complexes selon les besoins.

Jared S
la source
5
-1 a) IQueryableest une mauvaise mère, ou plutôt une interface DIEU. Sa mise en œuvre est très courante et il y a très peu (le cas échéant) de fournisseurs LINQ to Sql complets. b) Les méthodes d'extension rendent impossible l'extension (l'un des principes fondamentaux de la POO).
jgauffin
0

J'ai écrit un modèle d'objet de requête assez soigné pour NHibernate ici: https://github.com/shaynevanasperen/NHibernate.Sessions.Operations

Cela fonctionne en utilisant une interface comme celle-ci:

public interface IDatabases
{
    ISessionManager SessionManager { get; }

    T Query<T>(IDatabaseQuery<T> query);
    T Query<T>(ICachedDatabaseQuery<T> query);

    void Command(IDatabaseCommand command);
    T Command<T>(IDatabaseCommand<T> command);
}

Étant donné une classe d'entités POCO comme celle-ci:

class Database1Poco
{
    public int Property1 { get; set; }
    public string Property2 { get; set; }
}

Vous pouvez créer des objets de requête comme celui-ci:

class Database1PocoByProperty1 : DatabaseQuery<Database1Poco>
{
    public override Database1Poco Execute(ISessionManager sessionManager)
    {
        return sessionManager.Session.Query<Database1Poco>().SingleOrDefault(x => x.Property1 == Property1);
    }

    public int Property1 { get; set; }
}

Et puis utilisez-les comme ceci:

var database1Poco = _databases.Query(new Database1PocoByProperty1 { Property1 = 1 });
Shayne
la source
1
Bien que ce lien puisse répondre à la question, il est préférable d'inclure les parties essentielles de la réponse ici et de fournir le lien de référence. Les réponses de lien uniquement peuvent devenir invalides si la page liée change.
Dan Pichelman
Mes excuses, j'étais pressé. Réponse mise à jour pour montrer un exemple de code.
Shayne
1
+1 pour améliorer la publication avec plus de contenu.
Songo