Je pose donc cette question après avoir lu ce qui suit: Pourquoi ne devrais-je pas utiliser le modèle de référentiel avec Entity Framework? .
Il semble qu'il y ait un grand nombre de personnes qui disent oui et celles qui disent non. Ce qui semble manquer dans certaines réponses, ce sont des exemples concrets, que ce soit code
ou un bon raisonnement ou autre.
Le problème est que je continue de lire les gens qui répondent "bien EF est déjà une abstraction". Eh bien, c'est génial, et c'est probablement vrai, mais comment l'utiliseriez-vous sans le modèle de référentiel?
Pour ceux qui disent le contraire, pourquoi diriez-vous le contraire, qu'est-ce que vous avez personnellement rencontré?
Pour éviter cela, je suis un grand partisan d'Entity Framework, mais il présente certains inconvénients dont vous devez être conscient.
Je m'excuse également pour la longue réponse, mais c'est un sujet très brûlant avec de nombreuses opinions et de nombreuses considérations requises. Pour les petites applications, beaucoup de ces considérations n'ont pas d'importance, mais pour les applications de niveau entreprise, elles importent beaucoup.
Une partie de ce qui fait de la discussion EF un sujet brûlant est qu'elle conduit à une chaîne d'événements, où chaque solution introduit un nouveau problème (qui ne s'applique parfois que dans des cas plus avancés). Si je viens de vous donner la réponse finale (ou devrais-je dire actuelle), vous penseriez que j'omettais plusieurs autres solutions, donc je pense qu'il est pertinent de vous expliquer les solutions et comment elles ne sont pas la solution finale au problème .
comment l'utiliseriez-vous sans le modèle de référentiel?
La réponse courte à cela est que les référentiels (simples) sont un anti-pattern * pour Entity Framework.
EF fournit un contexte, qui donne essentiellement accès à l'ensemble de la base de données. Vous pouvez par exemple déclencher une requête qui renvoie toutes les entités Country avec leurs entités Province déjà remplies, avec les entités City de chaque province déjà remplies. En bref, cela vous permet d'exécuter des requêtes de type entité multiple (c'est une phrase que j'ai inventée moi-même pour expliquer la différence avec les référentiels).
Les référentiels, du moins leur implémentation de base, ont tendance à adopter une approche "un type d'entité par référentiel". Si vous voulez obtenir une liste de tous les pays avec toutes leurs provinces et toutes les villes de la province, vous devrez parler séparément aux CountryRepository
, ProviceRepository
et CityRepository
. En bref, les référentiels vous limitent à ne pouvoir exécuter que des requêtes de type entité unique. Pour le même exemple, vous devez lancer 3 requêtes de base de données distinctes afin d'obtenir tous les pays, leurs provinces et leurs villes.
Et ne vous méprenez pas, j'aime les dépôts. J'aime avoir les petites boîtes soignées afin que vous puissiez séparer votre stockage des différents objets de domaine, qui, par exemple vous permettrait d'obtenir les pays de votre base de données mais les provinces d'une API distante et les villes pour une deuxième API distante.
Mais cette séparation des types d'entités dans leurs propres boîtes privées se heurte beaucoup à l'avantage d'avoir des bases de données relationnelles, où une partie de l'avantage est que vous pouvez lancer une seule requête qui peut prendre en compte les entités liées (pour le filtrage, le tri ou le retour) .
Vous pourriez à juste titre répondre que "un référentiel peut toujours renvoyer plus d'un type d'entité". Et vous auriez raison. Mais si vous avez une requête qui renvoie à la fois les entités Foo
et Bar
, où la placez-vous? Dans le FooRepository
? Dans le BarRepository
? Il peut y avoir des exemples où le choix est facile, mais il y a aussi des exemples où le choix est difficile et plusieurs développeurs peuvent avoir des méthodes de catégorisation différentes et ainsi la base de code devient incohérente et le véritable objectif de l'approche "un type d'entité par référentiel" sera jeté par la fenêtre.
* Quand je dis que les référentiels sont un anti-modèle, ce n'est pas une déclaration globale, mais plutôt qu'ils contrecarrent spécifiquement le but d'EF. Sans EF ou une solution similaire, les référentiels ne sont pas un anti-modèle.
Les objets de requête sont le seul moyen réel de contourner l'approche "un type d'entité par référentiel". La manière la plus courte que je puisse décrire ce qu'est un objet de requête est que vous devez le considérer comme un "référentiel à une méthode".
Les référentiels souffrent de devoir traiter avec plusieurs types d'entités, et plus il y a de méthodes, plus les types d'entités qu'il va probablement gérer. En séparant chaque méthode de référentiel en un objet de requête qui lui est propre, vous avez simplement supprimé la suggestion contradictoire selon laquelle "ce référentiel ne gère qu'un seul type", et suggérez plutôt que "cet objet de requête exécute cette requête particulière, quels que soient les types d'entité il doit utiliser ".
Vous pouvez toujours utiliser des référentiels en même temps, et vous pouvez alors appliquer que les référentiels ne géreront jamais plus que leur type d'entité désigné.
Country
et Province
), elle appartient alors à son propre objet de requête privé (par exemple CountriesAndTheirProvincesQuery
).Country
), alors elle appartient au référentiel de ce type d'entité (par exemple CountryRepository
).Sur le plan technique, les objets de requête fonctionnent exactement comme les référentiels. La seule différence est que vous séparez la logique différemment en n'essayant plus de prétendre que vos requêtes de type multi-entités appartiennent à un référentiel de type mono-entité.
Il existe un deuxième problème concernant les référentiels. Comme ce sont des classes distinctes, elles ne dépendent pas les unes des autres. Cela signifie généralement que chaque référentiel utilisera son propre contexte EF (j'omets ici l'injection de dépendance car il contourne le centre de la réponse).
Supposons que vous effectuez une importation, qui ajoute des pays et des villes à la base de données. Cependant, vous voulez une sécurité transactionnelle, ce qui signifie qu'en cas de défaillance de any, alors rien doit être enregistré dans la base de données.
Mais lorsque vous devez gérer deux référentiels qui ont chacun leur propre contexte, comment pouvez-vous appeler sciemment SaveChanges()
sur un contexte avant de savoir que SaveChanges()
de l'autre contexte a réussi ? Vous devrez deviner, et vous allez être bloqué manuellement en annulant la validation du premier contexte lorsque la validation du deuxième contexte finit par échouer.
En séparant les référentiels, vous avez supprimé leur capacité à avoir un contexte partagé, dont vous avez besoin lorsque vous traitez des transactions qui opèrent sur plusieurs types d'entités en même temps .
Dans toute base de code ou domaine suffisamment grand où j'ai utilisé des référentiels et EF, j'ai fini par implémenter une unité de travail pour au moins quelque peu contrer le problème de la sécurité transactionnelle.
En termes très simples, une unité de travail est une collection de tous les référentiels, elle oblige les référentiels à partager le même contexte et permet au développeur de valider/restaurer directement le contexte de tous les référentiels en même temps. Un exemple simple:
public class UnitOfWork : IDisposable
{
public readonly FooRepository FooRepository;
public readonly BarRepository BarRepository;
public readonly BazRepository BazRepository;
private readonly MyContext _context;
public UnitOfWork()
{
_context = new MyContext();
this.FooRepository = new FooRepository(_context);
this.BarRepository = new BarRepository(_context);
this.BazRepository = new BazRepository(_context);
}
public void Commit()
{
_context.SaveChanges();
}
public void Dispose()
{
_context.Dispose();
}
}
Et un exemple d'utilisation simple:
using (var uow = new UnitOfWork())
{
uow.FooRepository.Add(myFoo);
uow.BarRepository.Update(myBar);
uow.BazRepository.Delete(myBaz);
uow.Commit();
}
Et maintenant, nous avons la sécurité transactionnelle. Soit les trois objets sont traités dans la base de données, soit aucun ne l'est.
Peut-être que vous l'avez remarqué, peut-être que vous ne l'avez pas fait, mais vous devriez voir de fortes similitudes avec les DbContext
et les UnitOfWork
d'E que je viens de créer. Ils sont essentiellement la même chose. Ils représentent une seule transaction vers la base de données et offrent un accès aux collections de tous les types d'entités disponibles:
public class UnitOfWork
{
public readonly FooRepository FooRepository;
public readonly BarRepository BarRepository;
public readonly BazRepository BazRepository;
public void Commit() { }
}
public class MyContext : DbContext
{
public Set<Foo> Foos { get; private set; }
public Set<Bar> Bars { get; private set; }
public Set<Baz> Bazs { get; private set; }
public int SaveChanges() { }
}
DbContext
d'EF est satisfaisant la définition de ce qu'est une unité de travail :
Une unité d'oeuvre assure le suivi de tout ce que vous faites pendant une transaction commerciale qui peut affecter la base de données. Lorsque vous avez terminé, il comprend tout ce qui doit être fait pour modifier la base de données à la suite de votre travail.
Alors pourquoi faisons-nous cela? Eh bien, tout simplement, parce que les développeurs essaient toujours d'abstraire les dépendances. Nous ne voulons pas que la couche métier dépende directement d'EF. C'est exactement la même raison pour laquelle vous avez créé des référentiels en premier lieu: pour que votre logique métier n'utilise pas directement EF.
Mais quel est l'intérêt de tout cela? Pourquoi utilisons-nous EF, puis des référentiels anti-modèles, puis une unité de travail anti-anti-modèles pour que tout soit réalisable? Cela coûte tellement d'efforts. Nous devons écrire manuellement des filtres de recherche au lieu de pouvoir compter de manière innée sur la capacité d'EF à analyser (à peu près) toute méthode lambda que nous lui jetons. Pourquoi passons-nous tous ces efforts à la place pour utiliser EF de la manière dont il est déjà prévu de fonctionner hors de la boîte?
Et je dois admettre que j'ai cette question depuis longtemps, mais je trouve peu de soutien à mon opinion. Si vous me permettez de savonner pendant un moment; mon avis à ce sujet est que c'est pourquoi EF est appelé Entity Framework et non Entity Library.
La différence entre les frameworks et les bibliothèques est souvent sémantique et peut être débattue, mais je pense qu'une ligne agréable peut être tracée comme expliqué ici :
Une bibliothèque effectue des opérations spécifiques et bien définies.
Un framework est un squelette où l'application définit la "viande" de l'opération en remplissant le squelette. Le squelette a encore du code pour relier les parties mais le travail le plus important est fait par l'application.
Cette description d'un cadre correspond à EF sur un tee-shirt. Il fait à peu près toute l'interaction DB pour nous, mais il nous oblige à étendre DbContext
avec les entités (et la configuration du modèle) que nous attendons d'EF.
Nous faisons abstraction des dépendances (bibliothèques) parce que nous le pouvons, et parce que l'avantage de le faire (permutabilité) l'emporte de loin sur l'inconvénient (effort requis pour implémenter l'abstraction). Mais les cadres, le squelette d'un système, ne sont pas facilement remplacés car ils ne peuvent pas être facilement abstraits. L'effort est beaucoup plus important que la probabilité de devoir remplacer la dépendance, et cela ne vaut donc plus la peine de le faire.
Je pense que pour couper beaucoup de code passe-partout, il serait avantageux de considérer EF comme un cadre autour duquel nous construisons l'application et dont nous ne pouvons pas nous éloigner facilement (de la même manière que pour une bibliothèque). Cela signifie que nous pouvons supprimer complètement les référentiels et l'unité de travail, car leur seul objectif est de donner accès aux fonctionnalités qu'EF possède déjà; et utilisez plutôt EF directement et acceptez que son utilisation soit un choix architectural que nous n'implémentons pas avec l'intention de s'en éloigner facilement.
Cela signifie que nous pourrions supprimer les référentiels et les unités de travail, et plutôt que notre logique métier traite directement le contexte. Remarquez comment le code de logique métier ne change pratiquement pas:
// OLD
using (var uow = new UnitOfWork())
{
uow.FooRepository.Add(myFoo);
uow.BarRepository.Update(myBar);
uow.BazRepository.Delete(myBaz);
uow.Commit();
}
// NEW
using (var db = new MyContext())
{
db.Foos.Add(myFoo);
db.Bars.Update(myBar);
db.Bazs.Delete(myBaz);
db.SaveChanges();
}
Le problème est que je continue de lire les gens qui répondent "bien EF est déjà une abstraction". Eh bien, c'est génial, et c'est probablement vrai, mais comment l'utiliseriez-vous sans le modèle de référentiel?
En utilisant EF directement et en n'essayant plus de l'abstraire derrière un mur de référentiels auto-développé (et éventuellement une unité de travail).
Pour ceux qui disent le contraire, pourquoi diriez-vous le contraire, qu'est-ce que vous avez personnellement rencontré?
La réponse est une sorte de récapitulation de mon expérience avec EF au cours des 6 à 7 dernières années. Les référentiels de base introduisent à eux seuls plus de problèmes qu'ils n'en résolvent. Il existe des solutions avancées qui résolvent les problèmes introduits par les référentiels de base; mais vous finissez par arriver à un point où vous commencez à vous demander s'il n'est pas préférable de simplement choisir de ne pas utiliser de référentiels afin de ne pas avoir à dépenser l'effort pour les faire jouer correctement avec EF.
Peuvent-ils être faits pour bien jouer avec EF? Chose sûre. Vaut-il la peine de créer toute cette abstraction? Cela dépend beaucoup de la probabilité que vous vous éloigniez d'EF (ou que vous utilisiez une banque de données incompatible avec EF).
Le cadre d'entité n'est pas une abstraction complète, seulement partielle. Il ne fait qu'abstraire l'interrogation des données.
Par exemple, que se passe-t-il si vous avez une entité Order
, mais que votre application grandit, elle devient un goulot d'étranglement des performances. Votre administrateur de base de données suggère ensuite de le diviser en deux tableaux, current_order
pour les commandes passées au cours des 30 derniers jours, et older_order
pour tout le reste. Pour compliquer encore les choses, pour augmenter encore les performances, vous DBA fait le PK de current_order
int16 et older_order
int64 PK, donc maintenant les deux tables représentant une seule entité n'ont même pas le même schéma.
Pour compliquer encore les choses, certaines commandes prennent plus de 30 jours pour être exécutées; comment Entity Framework résoudra-t-il cette complexité? Astuce: Ce ne sera pas le cas, et à moins que vous n'utilisiez le modèle de référentiel pour abstraire l'accès aux données, cette modification est sur le point de rendre votre code un gâchis.
Que faire si vous devez passer de Postgres à MongoDb? Opps, EF ne prend pas en charge MongoDB, il y a cette abstraction.
Le fait est qu'EF utilise le modèle d'enregistrement actif, et il pourrait donc être juste de dire qu'il ne fournit pas du tout d'abstraction des données car il est directement lié à la source de données, et n'est qu'un outil pour simplifier l'interrogation des données.