Common connection and transaction management approaches
Connection and transaction management are one of the most important concepts for applications that use databases. When you enable a database connection, when to start a transaction, how to release the connection...etc.
As everyone knows, .Net uses connection pooling. Therefore, creating a connection is actually getting a connection from the connection pool, which will do so because there is a cost to create a new connection. If no connection exists in the connection pool, a new connection object is created and added to the connection pool. When you release the connection, it actually sends this connection object back to the connection pool. This is not a release in a practical sense. This mechanism is provided by .Net. Therefore, we should release the connection object after use. This is the best practice.
In the application, there are two common parties to create/release a database connection:
The first method:Create a connection object when the web request arrives. (Application_BeginRequest is located in the event), the same connection object is used to handle all database operations, and the connection is closed/released at the end of the request (Application_EndRequest event).
This is a simple but inefficient method, for reasons:
- Perhaps this web request does not require the database to be operated, but the connection will be enabled. This is an inefficient way to use it for connection pools.
- This may make the web request run longer and the database operations will require some execution. This is also an inefficient way to use connection pools.
- This is feasible for web applications. If your application is Widnows Service, this may not be implemented.
- Similarly, this is the best scenario for using transactional database operations. If an operation fails, all operations will be rolled back. Because the transaction locks some data columns (event data tables) in the database, it must be short-lived.
The second method:Create a connection when needed (as long as before using it) and release it after using it. This is quite efficient, but it has to be tedious and repeated (create/release connection).
Connection and transaction management of ABP
ABP combines the above two connection management methods and provides a simple and efficient model.
1. Repository classes
Warehousing is the main class for database operations. ABP opens a database connection and enables a transaction when entering the repository method. Therefore, you can safely use connected to the warehousing method. After the warehousing method is completed, the transaction is committed and the connection is released. If the warehousing method throws any exception, the transaction will be rolled back and the connection will be released. In this mode, the warehousing method is unity (a unit of work). ABP is fully automatic when handling the above actions. Here, there is a simple storage:
public class ContentRepository : NhRepositoryBase<Content>, IContentRepository { public List<Content> GetActiveContents(string searchCondition) { var query = from content in <Content>() where && ! select content; if ((searchCondition)) { query = (content => (searchCondition)); } return (); } }
This example uses NHibernate as the ORM framework. As shown above, there is no need to write program code for any database connection operation (Session in NHibernate).
If the warehousing method calls another warehousing method (generally, if the work unit method calls the method of another work unit), all use the same connection and transaction. The first warehousing method is responsible for managing connections and transactions, while the other warehousing methods called by it are simply used and not managed.
2. Application service classes
A method of applying services is also considered to use a unit of work. If we have an application service method as follows:
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 = , EmailAddress = }; _personRepository.Insert(person); _statisticsRepository.IncrementPeopleCount(); } }
In the CreatePerson method, we add a new person to use the person repository and use the statistics repository to increase the total number of people. Two repositories share the same connection and transaction in this example, because this is an application service method. ABP opens a database connection and opens a transaction to enter the CreationPerson method. If no exception is thrown, then submits the transaction to the end of the method. If an exception is thrown, the transaction will be rolled back. Under this mechanism, all database operations become unitary (units of work) in CreatePerson.
3. Unit of work
The work unit works in the background to replace warehousing and application services. If you want to control the connections and transactions of the database, you need to operate the work unit directly. Here are two examples of direct use:
The first and best way to use UnitOfWorkAttribute is as follows:
[UnitOfWork] public void CreatePerson(CreatePersonInput input) { var person = new Person { Name = , EmailAddress = }; _personRepository.Insert(person); _statisticsRepository.IncrementPeopleCount(); }
Therefore, the CreatePerson method is transformed into a unit of work and manages database connections and transactions, and both warehouse objects use the same unit of work. It should be noted that if this is the method of the application service, there is no need to add the UnitOfWork attribute, see the unit of work method: Chapter 3, 3.3.5.
The second example is to use the (...) method as follows:
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 = , EmailAddress = }; using (var unitOfWork = _unitOfWorkManager.Begin()) { _personRepository.Insert(person); _statisticsRepository.IncrementPeopleCount(); (); } } }You can inject and use IUnitOfWorkManager as shown above. Therefore, you can create more limited scope units of work. In this mechanism, you can usually call the Complete method manually. If you don't call, the transaction will roll back and all exceptions will not be stored. The Begin method is rewritten to set the unit of work options.
This is great, but unless you have a good reason, use the UnitOfWork property less.
Work unit
1. Disable unit of work
You might want to disable the unit of work that applies the service method (because it is enabled by default). To do this, use the IsDisabled property of UnitOfWorkAttribute. Examples are as follows:
[UnitOfWork(IsDisabled = true)] public virtual void RemoveFriendship(RemoveFriendshipInput input) { _friendshipRepository.Delete(); }
Normally, you won't need to do this, because the methods of applying services should be unitary and usually use databases. In some cases, you may want to disable the unit of work of the App Service:
(1) Your method does not require any database operations and you do not want to enable unwanted database connections
(2) You want to use the unit of work within the limited scope of the UnitOfWorkScope class, as described above
Note that if the unit of work method calls this RemoveFriendship method, disable is ignored and it uses the same unit of work as the method that calls it. Therefore, be careful when disabling this feature. Similarly, the above program code works well because the warehousing method is the unit of work by default.
2. Non-transactional unit of work
The unit of work is transactional by default (this is its nature). Therefore, ABP starts/commits/rolls back a explicit database-level transaction. In some special cases, a transaction may cause problems because it may lock some data columns or tables in the database. In these situations, you may want to disable database-level transactions. The UnitOfWork property can obtain a Boolean value from its constructor to make it work like a non-transactional unit of work. The example is:
[UnitOfWork(false)] public GetTasksOutput GetTasks(GetTasksInput input) { var tasks = _taskRepository.GetAllWithPeople(, ); return new GetTasksOutput { Tasks = <List<TaskDto>>(tasks) }; }
It is recommended to do this [UnitOfWork(isTransaction:false)]. (readable and explicit).
Note that ORM frameworks (such as NHibernate and EntityFramework) store data internally in a single command. Suppose you have updated some entities to non-transactional UoW. Even in this situation, all updates will be completed at the end of the work unit of a single database command. However, if you execute the SQL query directly, it will be executed immediately.
There is a non-transactional UoW restriction here. If you are already in the transactional UoW area, setting isTransactional to false will be ignored.
Be careful when using non-transactional UoW, because in most cases, data integration should be transactional. If your method is just reading data and does not change the data, then of course it can be non-transactional.
3. The work unit calls other work unit (A unit of work method calls another)
If the unit of work method (a method with UnitOfWork attribute tag) calls another unit of work method, they share the same connection and transaction. The first method manages connections, the other method is just to use it. This is possible if all methods are executed under the same thread (or within the same web request). In fact, when the work unit area begins, all program code will execute in the same thread and share the same connection transaction until the work unit area terminates. This is the same for using the UnitOfWork property and the UnitOfWorkScope class. If you create a different thread/task, it uses the unit of work it belongs to.
Automatic saving changes (Automatically saving changes)
When we use the unit of work to the method, ABP automatically stores all changes at the end of the method. Suppose we need a method that updates the person name:
[UnitOfWork] public void UpdateName(UpdateNameInput input) { var person = _personRepository.Get(); = ; }
In this way, the name is modified! We didn't even call the _personRepository.Update method. The ORM framework will continuously track all changes in the entity within the work unit and reflect all changes into the database.
Note that this does not require UnitOfWork declaration in the application service, because they are by default using Units of Work.
4. GetAll() method of the warehouse interface (())
When you call the GetAll method outside the repository method, there must be an open database connection because it returns an IQueryable type object. This is required because IQueryable delays execution. It does not execute the database query immediately until you call the ToList() method or use IQueryable in the foreach loop (or access to the query result set). Therefore, when you call the ToList() method, the database connection must be enabled. Example:
[UnitOfWork] public SearchPeopleOutput SearchPeople(SearchPeopleInput input) { //Get IQueryable<Person> var query = _personRepository.GetAll(); //Add some filters if selected if (!()) { query = (person => ()); } if () { query = (person => == ); } //Get paged result list var people = ().Take().ToList(); return new SearchPeopleOutput { People = <List<PersonDto>>(people) }; }
Here, the SearchPeople method must be a unit of work, because the IQueryable is called in the method body and the database connection must be opened when () is executed.
Just like the GetAll() method, if you need a database connection and there is no repository, you must use a unit of work. Note that the application service method is the unit of work by default.
5. UnitOfWork attribute restrictions
In the following scenarios, you can use the UnitOfWork attribute tag:
(1) All interface-based methods of public or public virtual (such as application services are based on service interface)
(2) Public virtual methods of self-injection classes (such as MVC Controller and Web API Controller)
(3) All protected virtual methods.
It is recommended to mark the method as virtual. You cannot apply to private methods. Because ABP uses dynamic proxy to implement, while private methods cannot be implemented using inherited methods. When you don't use dependency injection and initialize the class yourself, the UnitOfWork property (and any proxy) won't work properly.
Options
There are many options to control the unit of work.
First, we can change all default values for all units of work in startup configuration. This is usually achieved using the PreInitialize method in our module.
public class SimpleTaskSystemCoreModule : AbpModule { public override void PreInitialize() { = ; = (30); } //...other module methods }
method
The work unit system operates seamlessly and invisible. However, under some special cases, you need to call its method.
SaveChanges:
ABP stores all changes at the end of the work unit, and you don't need to do anything. But sometimes, you may want to store all the changes in the process of the unit of work. In this case, you can inject IUnitOfWorkManager and call the() method. In the example, the Entity Framework is used to obtain the ID of the new entity when storing changes. Note that the current unit of work is transactional, and all changes in transactions will be rolled back when an exception occurs, even if SaveChange has been called.
event
The work unit has Completed/Failed/Disposed events. You can register these events and perform the required actions. Inject IUnitOfWorkManager and use the property to get the currently activated unit of work and register its events.
You may want to execute some program code successfully completed in the current unit of work. Example:
public void CreateTask(CreateTaskInput input) { var task = new Task { Description = }; if () { = ; _unitOfWorkManager. += (sender, args) => { /* TODO: Send email to assigned person */ }; } _taskRepository.Insert(task); }