-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathunit-of-work.html
258 lines (230 loc) · 22.2 KB
/
unit-of-work.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<link type="text/css" rel="stylesheet" href="bootstrap.min.css" />
</head>
<body>
<div class="document-contents">
<h3 id="DocCommonApproaches">Préambule à la gestion des connexions et transactions</h3>
<p>La gestion des connexions et transactions et l'un des concepts crtiques d'une application qui communique avec une base de données. Lorsqu'un connexion s'ouvre, lors qu'une transaction démarre, comment finaliser et libérer proprement ces composants. </p>
<p>Comme vous les savez probablement, .NET utilise une pile de connexions. (connection pooling) La création d'une connexion va donc récupérer une connexion à partir de cette pile au lieu de créer réellement une nouvelle connexion en raison du coût lié à cette opération. S'il n'y a plus de connexion disponible dans la pile, une nouvelle sera alors créée et stockée dans la pile. Lorsque vous libérez la connexion, elle retourne à ce moment dans la pile. Elle n'est pas réellement détruite. Ce méchanisme fournit par .NET est prêt à l'emploi. Nous devons donc toujours libérer la connexion lorsqu'elle n'est pas utile. C'est une bonne pratique.</p>
<p>Il y a deux approches pour créer et libérer une connexion dans une application:</p>
<p><strong>Première approche</strong>: Créer une connexion lorsque la requête web atteint le serveur (évènement Application_BeginRequest
dans global.asax), l'utiliser pour toutes les opérations nécessaires avec la base de données et la fermer/libérer à la fin du traitement de la requête. (évènement Application_EndRequest).
C'est simple mais pas très efficace... Pourquoi ?</p>
<ul>
<li>Il peut arriver qu'il n'y ait pas d'interaction requise avec la base de données lors d'une requête au serveur, mais la connexion sera tout de même ouverte. C'est une usage contre productif du système de pile de connexions.</li>
<li>Il y aura peut être des cas où une requête sera longue alors que les interactions avec la base de données sont de courte durée. Cela pénalise encore une fois le système de pile de connexions.</li>
<li>Cela n'est possible que dans une application web. Si votre application est un service Windows, cela ne peut pas être implémenter.</li>
</ul>
<p>Il est donc admis comme bonne pratique, d'interagir avec la base de données d'une manière <strong>transactionnelle</strong>. Si une opération échoue, toutes les étapes de cette opération sont annulées. Comme la transaction peut verrouiller certains lignes de la base de données (table des évènements),
elle doit être de très courte durée.</p>
<p><strong>Seconde approche</strong>: Créer la connexion juste avant d'en avoir besoin (juste avant de l'utiliser) et la libérer à la fin de son utilisation. C'est le plus efficace mais également le plus lourd à mettre en place car cela implique de répéter le code d'ouverture/fermeture à pleins d'endroits.</p>
<h3 id="DocAbpApproach">La Gestion des Connexions & Transactions d'ASP.NET Boilerplate</h3>
<p>ASP.NET Boilerplate mixe les deux approches et fournit un modèle simple et efficient.</p>
<h4 id="DocRepositoryClasses">Classes d'entrepôts</h4>
<p>Le principe se base sur les classes <a href="/Pages/Documents/Repositories">entrepôts</a>, là où les interactions avec la base données sont exécutées. ASP.NET Boilerplate <strong>ouvre</strong> une connexion (elle pourrait ne pas être immédiatement ouverte et basée plutôt sur le premier usage avec la base de données en se basant sur les fonctionnalités de l'ORM) et démarre une <strong>transaction</strong> au moment où le processus <strong>entre</strong> dans une méthode de l'entropôt. (repository). Vous pouvez donc utiliser cette connexion sans danger dans les méthodes de votre entrepôt. A la fin de la méthode, la transaction est <strong>
commitée</strong> et la connexion est <strong>libérée</strong>. Si la méthode de l'entrepôt rencontre une erreur, (<strong>exception</strong>) la transaction fait l'objet d'un <strong>
roll back</strong> et la connexion est libérée. De cette façon, une méthode est <strong>atomique</strong> (une <strong>unité de travail</strong>). ASP.NET Boilerplate fait tout cela automatiquement. Regardez cet exemple:</p>
<pre lang="cs">public class ContentRepository : NhRepositoryBase<Content>, IContentRepository
{
public List<Content> GetActiveContents(string searchCondition)
{
var query = from content in Session.Query<Content>()
where content.IsActive && !content.IsDeleted
select content;
if (string.IsNullOrEmpty(searchCondition))
{
query = query.Where(content => content.Text.Contains(searchCondition));
}
return query.ToList();
}
}
</pre>
<p>Cet exemple utilise l'ORM NHibernate. Comme indiqué précédemment, il n'y a pas de code dans cet extrait pour gérer la connexion/Déconnexion (L'objet Session avec NHibernate).</p>
<p>Si une méthode de l'entrepôt appelle une autre méthode d'entrepôt (en général, si une méthode d'une unité de travail appelle une autre méthode d'unité de travail) les mêmes connexion et la transaction seront utilisées. La première méthode appelée prépare le nécessaire (connection et
transaction) pour les suivantes.</p>
<h4 id="DocAppServices">Services Applicatifs</h4>
<p>Une méthode d'un <a href="/Pages/Documents/Application-Services">service applicatif</a> est aussi considérée comme une unité de travail. Imaginons que nous avons une méthode <a href="/Pages/Documents/Application-Services">
d'un service applicatif </a> comme celle ci-dessous:</p>
<pre lang="cs">public class PersonAppService : IPersonAppService
{
private readonly IPersonRepository _personRepository;
private readonly IStatisticsRepository _statisticsRepository;
public PersonAppService(IPersonRepository personRepository, IStatisticsRepository statisticsRepository)
{
_personRepository = personRepository;
_statisticsRepository = statisticsRepository;
}
public void CreatePerson(CreatePersonInput input)
{
var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };
_personRepository.Insert(person);
_statisticsRepository.IncrementPeopleCount();
}
}</pre>
<p>Dans la méthode CreatePerson, nous insérons une personne en utilisant son repository et nous incrémentons le nombre total de personnes avec l'entrepôt StatisticsRepository. Les deux entrepôts <strong>partagent</strong> la même connexion et transaction dans cet exemple puisque c'est une méthode d'un service applicatif. ASP.NET Boilerplate ouvre une connexion à la base de données et démarre une transaction à l'entrée de la méthode CreatePerson et réalise un commit de la transaction à la sortie de la méthode si aucune erreur ne se produit, ou effectue un roll back. De cette manière, toutes les opérations dans la méthode CreatePerson deviennent <strong>atomiques</strong> (<strong>Unité de Travail ou Processus Unitaire</strong>).</p>
<h4 id="DocUow">Unité De Travail</h4>
<p>Une unité de travail s'exécute <strong>implicitement</strong> pour les méthodes d'entrepôts et de services applicatifs. Vous devrez l'utiliser <strong>explicitement</strong> si vous voulez contrôler la gestion des connexions et transactions en dehoers de ces types de classes. Il existe deux manières de faire.</p>
<h5>L'attribut UnitOfWork</h5>
<p>L'approche prévilégiée est d'utiiser l'attribut <strong>UnitOfWorkAttribute</strong>:</p>
<pre lang="cs">[UnitOfWork]
public void CreatePerson(CreatePersonInput input)
{
var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };
_personRepository.Insert(person);
_statisticsRepository.IncrementPeopleCount();
}</pre>
<p>De cette façon, la méthode CreatePerson devient unitaire et gère la connexion à la base de données, comme cela se fait dans les entrepôts. (partage de la connexion/transaction). Notez bien qu'il n'y a pas besoin de cet attribut si vous êtes dans une méthode d'un service applicatif. Reportez vous à la section '<a href="#DocUowRestrictions">restrictions des unité de travail</a>' sur ce sujet.</p>
<p>Il existe d'autres options relatives à l'attribut UnitOfWork. Regardez la section 'Unité de Travail en Détail' pour plus d'informations.</p>
<h5>IUnitOfWorkManager</h5>
<p>La seconde approche est l'usage de la méthode <strong>IUnitOfWorkManager.Begin(...)</strong> comme illustré ci dessous:</p>
<pre lang="cs">public class MyService
{
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IPersonRepository _personRepository;
private readonly IStatisticsRepository _statisticsRepository;
public MyService(IUnitOfWorkManager unitOfWorkManager, IPersonRepository personRepository, IStatisticsRepository statisticsRepository)
{
_unitOfWorkManager = unitOfWorkManager;
_personRepository = personRepository;
_statisticsRepository = statisticsRepository;
}
public void CreatePerson(CreatePersonInput input)
{
var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };
using (var unitOfWork = _unitOfWorkManager.Begin())
{
_personRepository.Insert(person);
_statisticsRepository.IncrementPeopleCount();
unitOfWork.Complete();
}
}
}</pre>
<p>Vous pouvez injecter et utiliser IUnitOfWorkManager comme indiqué ici. (si vous héritez votre service applicatif de la classe ApplicationService, vous pouvez à ce moment utiliser directement la propriété<strong> CurrentUnitOfWork</strong>. Sinon, vous devrez l'injecter). Vous créez ainsi une étendue plus <strong>limitée</strong> de l'unité de travail. Avec ce procédé, vous pouvez appeler manuellement la méthode <strong>Complete</strong>. Si vous ne le faites pas, la transaction fait l'objet d'un roll back et les modifications ne sont pas rendues persistentes.</p>
<p>La méthode Begin possède des surcharges pour paramétrer les <strong>options de l'unité de travail</strong>.</p>
<p>Il est toutefois préférable d'utiliser l'attribut <strong>UnitOfWork</strong> si vous n'avez pas de raison particulière de faire autrement.</p>
<h3 id="DocUowDetails">L'Unité de travail en Détail</h3>
<h4 id="DocDisablingUow">Désactivation de l'unité de travail</h4>
<p>Vous pouvez désactiver l'unité de travail pour <strong>une méthode donnée d'un service applicatif</strong>
(car c'est activé par défaut). Il faut utiliser l'attribut UnitOfWorkAttribute pour cela. Exemple:</p>
<pre lang="cs">[UnitOfWork(IsDisabled = true)]
public virtual void RemoveFriendship(RemoveFriendshipInput input)
{
_friendshipRepository.Delete(input.Id);
}
</pre>
<p>Normalement, vous n'avez pas à faire ça puisqu'une méthode de service applicatif devrait toujours être atomque et utiliser la base de données. Il peut toutefois y avoir des situations où il faut désactiver les unités de travail pour une méthode de service applicatif lorsque:</p>
<ul>
<li>Votre méthode n'a pas besoin d'interagir avec la base de données.</li>
<li>Vous voulez que votre unité de travail puisse s'exercer sur un temps très court avec une classe UnitOfWorkScope
comme précédemment.</li>
</ul>
<p>Notez qu'une unité de travail appelle cette méthode RemoveFriendship,
la désactivation est ignorée et la même unité de travail que la méthode appelante est utilisée. C'est pourquoi il faut utiliser la désactivation avec de la prudence. Le code ci-dessus fonctionne bien puisque les méthodes d'entrepôts sont réglées comme unitaires par défaut.</p>
<h4 id="DocUowNoTransaction">Unité de travail non transactionnelle</h4>
<p>Une unité de travail est transactionnelle par défaut (de part sa nature). Se faisant, ASP.NET
Boilerplate démarre/commit/rollback explicitement une transaction au niveau base de données. Dans quelques cas spéciaux, les transactions peuvent causer problème lorsqu'elles vérouillent des lignes ou des tables de la base de données. Dans ces situations, vous pouvez décider de désactiver les transactions. L'attribut UnitOfWork peut prendre un booléen dans son constructeur pour travailler de manière non transactionnelle. Exemple:</p>
<pre lang="cs" class="hljs cs">[UnitOfWork(<span lang="tr">isTransactional: </span><span class="hljs-keyword">false</span>)]
public GetTasksOutput GetTasks(GetTasksInput input)
{
var tasks = _taskRepository.GetAllWithPeople(input.AssignedPersonId, input.State);
return new GetTasksOutput
{
Tasks = Mapper.Map<List<TaskDto>>(tasks)
};
}
</pre>
<p>Je suggère d'utiliser cet attribut de cette façon: <strong>[UnitOfWork(isTransactional: false)]</strong>.
Je pense que c'est plus lisible et explicite. Mais vous pouvez aussi utiliser [UnitOfWork(false)].</p>
<p>Pensez bien que les frameworks ORM (comme NHibernate et EntityFramework) enregistrent les modifications en une seule commande. Imaginez que vous mettiez à jour quelques entités en dehors de toutes transactions UOW. Même dans cette situation, tous les updates sont exécutés à la fin de l'unité de travail en une commande de base de données. Mais si vous exécutez une requête SQL, son exécution est immédiate.</p>
<p>There is a restriction for non-transactional UOWs. If you're already in a
transactional unit of work scope, setting isTransactional to false is ignored.</p>
<p>Utilisez les unités de travail non transactionnelles prudemment car la plupart du temps, il est préférable de veiller à l'intégrité des données. Si votre méthode fait juste de la lecture, elle peut évidemment être non-transactionnelle..</p>
<h4 id="DocUowCallsOtherMethod">Une méthode d'une unité de travail en appelle une autre</h4>
<p>Si une méthode d'une unité de travail (une méthode déclarée avec l'attribut UnitOfWork) appelle une autre méthode d'une autre unité de travail, ils partagent la même connexion et transaction. La première méthode gère la connexion, les autres l'utilisent. C'est vrai pour les méthodes exécutées sur le même <strong>Thread</strong> (ou la même requête pour les appllications web). Actuellement, lorsqu'un bloc (scope) d'une unité de travaille commence, tous les codes exécutés sur le même thread partagent la même connexion et transaction jusqu'à ce que le bloc prenne fin. C'est vrai pour l'attribut UnitOfWork comme pour la classe UnitOfWorkScope.</p>
<h4>Etendue d'une unité de travail</h4>
<p>You pouvez créer différentes transactions, isolées, ou créer un bloc non-transactionnel dans une transaction. NET définit <a href="https://msdn.microsoft.com/en-us/library/system.transactions.transactionscopeoption(v=vs.110).aspx" target="_blank">
TransactionScopeOption</a> pour ça. Vous pouvez régler ScopeOption pour cette unité de travail. </p>
<h4 id="DocAutoSaveChanges">Enregistrement automatique des modifications</h4>
<p>Lorsque nous utilisons une unité de travail pour une méthode, ASP.NET Boilerplate enregistre tous les changements à la fin de la méthode, automatiquement. Imaginons que nous avons besoin d'une méthode pour mettre à jour person:</p>
<pre lang="cs">[UnitOfWork]
public void UpdateName(UpdateNameInput input)
{
var person = _personRepository.Get(input.PersonId);
person.Name = input.NewName;
}
</pre>
<p>C'est tout, le nom a été changé! Nous n'avons pas appelé la méthode _personRepository.Update. Le framework O/RM surveille tous les changements opérés sur les entités dans une unité de travail et les font persister en base de données.</p>
<p>Remarquez bien qu'il est inutile de déclarer UnitOfWork pour une méthode d'un service applicatif puisqu'elle sont déjà en mode UnitOfWork.</p>
<h4 id="DocRepositoryGetAll">La méthode IRepository.GetAll()</h4>
<p>Quand vous appelez GetAll() en dehors d'une méthode d'un entrepôt, il doit y avoir une connexion ouverte car elle retourne IQueryable. Il est attendu que l'exécution est déférée car IQueryable<T>. Elle ne produit pas directement de requêtes sur la base de données à moins que vous appeliez la méthode ToList() ou utilisiez une boucle foreach (ou quoique ce soit qui accède aux items retournés). Donc, quand vous appelez la méthode ToList(), la connexion doit être active.</p>
<p>Considérez la méthode suivante:</p>
<pre lang="cs">[UnitOfWork]
public SearchPeopleOutput SearchPeople(SearchPeopleInput input)
{
//Get IQueryable<Person>
var query = _personRepository.GetAll();
//Add some filters if selected
if (!string.IsNullOrEmpty(input.SearchedName))
{
query = query.Where(person => person.Name.StartsWith(input.SearchedName));
}
if (input.IsActive.HasValue)
{
query = query.Where(person => person.IsActive == input.IsActive.Value);
}
//Get paged result list
var people = query.Skip(input.SkipCount).Take(input.MaxResultCount).ToList();
return new SearchPeopleOutput { People = Mapper.Map<List<PersonDto>>(people) };
}
</pre>
<p>Ici, la méthode SearchPeople doit être en unité de travail car la méthode ToList() d'IQueryable est appelée dans le corps de la méthode, et la connexion de la base de données doit être ouverte lorsque la méthode IQueryable.ToList() est exécutée.</p>
<p>Comme la méthode GetAll(), vous devez utiliser l'unité de travail si la connexion à la base de données est nécessaire en dehors de l'entrepôt. Rappelez vous que les méthodes de services applicatifs sont en unité de travail par défaut.</p>
<h4 id="DocUowRestrictions">Les restrictions de l'attribut d'UnitOfWork</h4>
<p>Vous pouvez utiliser l'attribut UnitOfWork pour;</p>
<ul>
<li>Toutes les méthodes <strong>public</strong> ou <strong>public virtual</strong> pour les classes qui implémentent des interfaces (Comme les services applicatifs utilisent l'interface service).</li>
<li>Toutes les méthodes <strong>public virtual</strong> qui s'injectent des classes (Comme <strong>MVC Controllers</strong> et <strong>Web API Controllers</strong>).</li>
<li>Toutes les méthodes <strong>protected virtual</strong>.</li>
</ul>
<p>Il est conseillé de toujours mettre cette méthode en <strong>virtual</strong>. Vous <strong>ne pouvez pas les utiliser sur les méthodes privées</strong>.
En effet, ASP.NET Boilerplate fabrique dynamiquement des proxy et les méthodes privées ne peuvent pas être visibles par les classes dérivées. L'attribut UnitOfWork (et tous les proxy) ne fonctionne pas si vous n'utilisez pas <a href="/Pages/Documents/Dependency-Injection">dl'injection de dépendance</a> et n'instanciez pas la classe vous même.</p>
<h3 id="DocUowOptions">Options</h3>
<p>Il y a des options qui peuvent être utilisées pour changer le comportement d'une unité de travail.</p>
<p>Nous pouvons changer d'abord les valeurs par défaut de toutes les unités de travail dans la <a href="/Pages/Documents/Startup-Configuration">configuration de démarrage</a>. C'est généralement fait dans la méthode PreInitialize de votre <a href="/Pages/Documents/Module-System">module</a>.</p>
<pre lang="cs">public class SimpleTaskSystemCoreModule : AbpModule
{
public override void PreInitialize()
{
Configuration.UnitOfWork.IsolationLevel = IsolationLevel.ReadCommitted;
Configuration.UnitOfWork.Timeout = TimeSpan.FromMinutes(30);
}
//...other module methods
}
</pre>
<p>Ensuite, nous devons surcharger une unité de travail particulière. Pour cela, le constructeur de l'attribut <strong>
UnitOfWork </strong>et la méthode <strong>Begin</strong> d'IUnitOfWorkManagermethod ont des surcharges qui autorisent des options.</p>
<h3 id="DocUowMethods">Methodes</h3>
<p>Le système UnitOfWork travaille discrètement. Mais, dans certains cas spéciaux, vous devez appelez ses méthodes.</p>
<h4 id="DocUowSaveChanges">SaveChanges</h4>
<p>ASP.NET Boilerplate enregistre tous les changement à la fin d'une unité de travail, vous n'avez rien à faire. Mais, quelques fois, vous pouvez charger à enregistrer des modificatinos en base de données au milieu des opérations d'une unité de travail. Dans ce cas, vous pouvez injecter IUnitOfWorkManager et appeler la méthode IUnitOfWorkManager.Current.<strong>SaveChanges()</strong>. Un exemple de cet usage pourrait être de devoir sauber des changement pour obtenir l'identitfiant d'une <a href="/Pages/Documents/Entities">Entité</a> fraichement insérée par <a href="/Pages/Documents/EntityFramework-Integration">
EntityFramework</a>. Remarquez que comme une unité de travail est transactionnelle, tous les changements sur la transaction seront annulés si une erreur se produit en base de données, même les précédentes mises à jour.</p>
<h3 id="DocEvents">Les évènements</h3>
<p>Une unité de travail dispose des évènements <strong>Completed</strong>, <strong>Failed</strong> et <strong>Disposed</strong>.
Vous pouvez vous abonner à ces évènements et exécuter les opérations requises. Injectez IUnitOfWorkManager et utilisez la propriété IUnitOfWorkManager.Current pour obtenir l'unité de travail et vous abonner à ces évènements.</p>
<p>Vous pouvez avoir besoin d'exécuter du code lorsque l'unité de travail actuel est exécuté avec succès. Exemple:</p>
<pre lang="cs">public void CreateTask(CreateTaskInput input)
{
var task = new Task { Description = input.Description };
if (input.AssignedPersonId.HasValue)
{
task.AssignedPersonId = input.AssignedPersonId.Value;
_unitOfWorkManager.Current.Completed += (sender, args) => { /* TODO: Send email to assigned person */ };
}
_taskRepository.Insert(task);
}</pre>
</div>
</body>
</html>