Chapter 8
Object-Relational Behaviors Design Patterns
Introduction
To organize the object-relational behaviors, the design patterns of this category can be divided into the following three main sections:
Identity map: Tries to provide a method to fetch each record from the database only once.
Lazy load: Tries to load an object's data when needed.
Unit of work: Tries to send business transactions to the database as a single transaction.
The choice between these three design patterns depends on the level of logical complexity that we want to implement.
Structure
In this chapter, we will cover the following topics:
Object-relational behaviors design patterns
Unit of work
Identity map
Lazy load
Objectives
In this chapter, you will be introduced to object-relational behaviors design patterns and learn how to manage business transactions properly. You will also learn how to improve performance and efficiency by reducing references to the data source or when necessary.
Object-relational behaviors design patterns
Chapter 7, Data Source Architectural Patterns, tried to explain how different objects can be linked to tables in the database. In this chapter, we face another challenge: paying attention to behaviors. Behaviors mean how the data should be fetched from the database or how it should be stored in it. For example, suppose a lot of data is fetched from the database, and some have changed. It will be very important to answer the question of which of the data has changed or how to store the changes again in the database, provided that the data consistency is not disturbed.
Ensuring that the data is not read and changed by someone else is an issue related to concurrency management and design patterns. How to manage and apply changes is something that the unit of work design pattern can be useful for. When using the unit of work to improve efficiency, we need to ensure that the data that has been read will not be re-read. This problem can be solved with the identity map design pattern.
When the domain model is used, most models connect with other models. Reading a model will lead to retrieving all its relationships, again jeopardizing efficiency. To solve this problem, you can use the lazy load design pattern.
Unit of work
Name:
Unit of work
Classification:
Object-relational behaviors design patterns
Also known as:
---
Intent:
This design pattern tries to send business transactions to the database as a single transaction.
Motivation, Structure, Implementation, and Sample code:
Suppose we are implementing an infrastructure in which we want to manage the projects and people involved in an organization's project. The project and team member information of each project will be stored in separate tables in the database. There are different ways to implement this scenario. A simple way is to send each business request directly to the database and manage the transaction by placing and defining the transaction at the database level. This method has a problem, and sending a series of small requests to the database is necessary. In a system with high requests and user volume, these small requests will become the root of a big problem.
Another way is to manage these business requests at the application level and, after completing the business transaction process, send the entire business transaction to the database in the form of a single transaction. The unit of work design pattern can be useful in achieving this design.
Suppose we assume that we are facing only one model named project. The events that happen in this model are the events that lead to the creation of a new project, the deletion or editing of the existing project, and the reading of the project. With this hypothesis, to track changes at the application level, you can simply define several collections or arrays to track the changes and send them to the database at the right time.
There are different ways to implement the unit of work pattern. One of the most common methods is to combine the implementation of this design pattern with the repository design pattern. For example, suppose we have two repositories named UserRepository and BankAccountRepository. These two repositories have their own DbContext object and are connected to a common database. Figure 8.1 shows this relationship:
Figure%208.1.png
Figure 8.1: User and Bank account repositories with separated DbContexts
In this case, because UserRepository and BankAccountRepository have different objects from DbContext, their work is sent to the database in the form of two transactions (two units of work). These two repositories must use a shared DbContext object to solve this problem. Figure 8.2 shows this arrangement:
Figure%208.2.png
Figure 8.2: User and Bank account repositories with shared DbContext
As shown in Figure 8.2, the two repositories UserRepository and BankAccount Repository use a shared DbContext object so that the work of these two repositories can be sent to the database in the form of one transaction (one unit of work). As stated in the repository design pattern, the generic repository can also be used to implement this design pattern. Next, we will implement the unit of work design pattern with a generic repository.
According to the preceding explanations, the class diagram of this model in the presence of the generic repository model can be considered as follows:
Figure%208.3.png
Figure 8.3: UnitofWork with generic repository UML diagram
As can be seen in Figure 8.3 class diagram, the GenericRepository class is defined as generic and implements the IRepository interface. The UserRepository class can also inherit from the GenericRepository class and change some of its implementations (we have not implemented this part and assumed that the behaviors of the UserRepository are consistent with the GenericRepository). The UnitOfWork class implemented the IUnitOfWork interface and used the IRepository interface inside. This is because all access to repositories happens through the UnitOfWork object. Refer to the following code for this structure:
public interface IUnitOfWork
{
void Commit();
}
public class UnitOfWork : IUnitOfWork
{
private SampleDbContext _context = new();
private IRepository
private IRepository
public IRepository
{
get
{
if (_userRepository == null)
_userRepository = new GenericRepository
return _userRepository;
}
}
public IRepository
{
get
{
if (_bankAccountRepository == null)
_bankAccountRepository =
new GenericRepository
return _bankAccountRepository;
}
}
public void Commit() => _context.SaveChanges();
}
As you can see in the preceding code, when creating a repository object, a DbContext object is sent to it, and this makes both repositories have the same DbContext object. Also, for the preceding code, the generic repository design pattern is implemented as follows:
public interface IRepository
{
TEntity Find(TKey id);
List
void Add(TEntity user);
void Update(TEntity user);
void Delete(TKey id);
}
public class GenericRepository
where TEntity : class
{
internal SampleDbContext _context;
internal DbSet
public GenericRepository(SampleDbContext context)
{
_context = context;
_dbSet = context.Set
}
public virtual List
public virtual TEntity Find(TKey id) => _dbSet.Find(id);
public virtual void Add(TEntity entity) => _dbSet.Add(entity);
public virtual void Delete(TKey id) => _dbSet.Remove(_dbSet.Find(id));
public virtual void Update(TEntity entityToUpdate)
{
_context.Entry(entry).State = EntityState.Modified;
}
}
Remember that this implementation can be improved from the repository and unit of work model. It is presented in the simplest implementation mode just to facilitate content transfer. Now, to use this structure, you can proceed as follows:
IUnitOfWork unitOfWork = new UnitOfWork();
unitOfWork.UserRepository.Add(new User { Id = 1, Name = "Ahmad" });
unitOfWork.BankAccountRepository.Add(
new BankAccount { Id = 101, AccountNumber = 12345});
unitOfWork.Commit();
In the preceding code, a UnitOfWork object is created. A new user is added through the UserRepository, and a bank account is defined through the BankAccountRepository. Finally, the changes are sent to the database as one transaction.
Notes:
To track changes using this design pattern, it will be necessary to record the changes somewhere. There are two ways to do this:
Caller registration: The user must register the changes in the unit of work object to be applied in the database. The advantage of this method is its flexibility in sending changes to apply to the database. The problem with this method is that the user may forget to register the changes for any reason.
Object registration: In this method, the registration process is done through the methods in the target object. Usually, when performing the retrieval operation, the desired object is registered as a clean version. Then, as soon as a change occurs on this object, the Dirty version is formed. Despite having overhead, two versions of the object can significantly help detect changes.
This design pattern is usually used next to the repository design pattern.
The significant point is that the entity framework has implemented this change tracking process and unit of work design pattern in the SaveChanges method. Here the question arises whether this feature in the entity framework still needs to implement this design pattern in the systems or not. As with the repository design pattern, the answer to this question is also challenging, but implementing a unit of work in systems can be very useful.
Consequences: Advantages
Provides better control for transaction management.
Due to the reduction in the number of visits to the database, it provides better efficiency for batch operations.
The ability to maintain flexibility and testability of codes will be improved, and unit tests can be compiled and executed easily using mock mechanisms.
With the existence of a unit of work, there will be no need to use classes like DbContext and so on in the business layers. It will enable a loose coupling between the business layer and the framework used in the data access layer to easily make changes in the data access layer without changing the business layer (for example, entity framework can be replaced with another ORM).
Consequences: Disadvantages
For business logic and simple data access, often the use of this design pattern increases the complexity because today, the majority of ORMs offer the features presented in this design pattern. Using this design pattern will be useful when a new feature is added to them apart from the features provided by ORM.
Applicability:
This design pattern can be useful when there is a need to separate the communication of the business layer from the data layer and optimize the number of communication times with the database.
Using this design pattern in Domain-Driven Design (DDD) is very practical.
This design pattern will be useful when we face a series of requests and want to process them in one transaction.
Related patterns:
Some of the following design patterns are not related to the unit of work design pattern, but to implement this design pattern, checking the following design patterns will be useful:
Repository
Identity map
Identity map
Name:
Identity map
Classification:
Object-relational behaviors design patterns
Also known as:
---
Intent:
This design pattern tries to provide a method to fetch each record from the database only once. For this purpose, the records fetched for the first time are kept in a mapping set to be retrieved from this set whenever necessary.
Motivation, Structure, Implementation, and Sample code:
For example, consider the following code:
User user1 = userService.GetByName("Vahid");
User user2 = userService.GetByName("Vahid");
In the preceding code, data is read twice from the database and placed in two objects. In this situation, making changes to user1 has nothing to do with user2. Even if we want to save both objects in the database, one of the objects will be written on the other object. With this method, there is a risk that will result in concurrency problems. The identity map design pattern tries to solve this problem.
The identity map design pattern stores the records that have been read once from the database in a set so that when that record is needed later, instead of retrieving it from the database, the record is returned from the set that formed the identity map. In this way, the overall efficiency of the application will be improved. To implement an identity map, you can have a mapping set for each table in the database. Also, when implementing an identity map, issues related to concurrency should be considered.
Among the applications of this design pattern, we can mention the ability to cache data. The following class diagram shows the identity map design pattern:
Figure%208.4.png
Figure 8.4: Identity Map design pattern UML diagram
The Figure 8.4 class diagram shows that the UserMap class has a set of mappings named _mappings. This class has methods to add, delete and read records from the mapping set. Suppose we assume that the UserMap class is available from somewhere like the repository before sending the request to read the record to the database. In that case, that record is first searched by the Get method in the UserMap mapping set. If the desired record is not found in this set, the request to the database is sent, and the received response is first recorded in the mapping set using the Add method, and then the response is returned to the requester. With these explanations, the Figure 8.4 class diagram can be implemented as follows:
public class UserDbSet
{
public static List
{
new User() { Id = 1, Username = “Vahid”, Email = “vahid@gmail.com” },
new User() { Id = 2, Username = “Ali”, Email = “ali@yahoo.com” },
new User() { Id = 3, Username = “Reza”, Email = “reza@gmail.com” },
new User() { Id = 4, Username = “Maral”, Email = “maral@gmail.com” },
new User() { Id = 5, Username = “Hassan”, Email = “hassan@yahoo.com” }
};
}
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
}
public class UserMap
{
private readonly Dictionary
public void Add(User user)
{
if (!_mappings.ContainsKey(user.Id))
_mappings.Add(user.Id, user);
}
public User? Get(int id)
{
if (_mappings.ContainsKey(id))
return _mappings[id];
return null;
}
public void Remove(int key)
{
if (_mappings.ContainsKey(key))
_mappings.Remove(key);
}
}
public interface IUserRepository
{
User Get(int id);
}
public class UserRepository : IuserRepository
{
private readonly UserMap _usermap;
public UserRepository() => _usermap = new UserMap();
public User Get(int id)
{
var cachedUser = _usermap.Get(id);
if (cachedUser == null)
{
var user = UserDbSet.Users.FirstOrDefault(x => x.Id == id);
_usermap.Add(user);
return user;
}
return cachedUser;
}
}
As seen in the preceding code, when the Get method is executed in the UserRepository class, the mapping set in the UserMap is first checked through the Get method. If this record already exists in this mapping set, then the record is returned without referring to the database; otherwise, the record is read from the database and registered in the mapping set, and returned.
Notes:
Retrieving data from the database multiple times and placing it in several objects can damage its accuracy. The same multiple retrieving from an external source can affect performance.
There will be one mapping for each database table in a similar structure (database structure and model structure are the same).
Consider the following code:
var query1 = ctx.Users.Where(x => x.Name == “Vahid”);
var user1 = query1.FirstOrDefault();
var user2 = query1.FirstOrDefault();
var query2 = ctx.Users.Where(x => x.Id == 1);
var user3 = query2.FirstOrDefault();
Console.WriteLine(user1.GetHashCode());
Console.WriteLine(user2.GetHashCode());
Console.WriteLine(user3.GetHashCode());
By executing the preceding code, we realize that all three objects, user1, user2, and user3, are one object. This means that the entity framework does the same thing as the identity map behind the scenes and does not allow loading an object more than once.
A key will be needed to design the mapping set, so the record can be found later based on that key. The best option for the key is to choose the table’s primary key, although other combinations can also be used as keys.
This design pattern can also be implemented generically. In this case, the whole process can be implemented using one class. The key selection is important in implementing the generic method. Because, in this case, all sets of mappings must have a fixed formula for the key.
How and where to store the mapping set is also important in this design pattern. This collection should be stored in a way that is different for each session. For some read-only data that never change, the storage method and location will not matter, and those data can be shared between sessions.
Since concurrency management is important when using this design pattern, the optimistic offline lock and design patterns are widely used.
Usually, using this design pattern in the unit of work is a better design. Because the unit of work is the data entry and exit point, if there is no unit of work, the presence of this design pattern next to the registry design pattern can be useful.
There is no need to use this design pattern for immutable objects. The reason for this is also clear. When an object is immutable, its value will not change; when the value does not change, there is no need to worry about the anomalies caused by the change. Among the most important immutable objects are value objects.
Consequences: Advantages
This design pattern can be used to implement the cache mechanism and thus reduce the number of references to the database.
Consequences: Disadvantages
This design pattern can manage the collision event in one session but cannot do anything for the collision event between several sessions. To solve this problem, you must use optimistic offline and pessimistic offline locks.
Applicability:
This design pattern can be useful when data needs to be read only once from the source.
Related patterns:
Some of the following design patterns are not related to the identity map design pattern, but to implement this design pattern, checking the following design patterns will be useful:
Unit of work
Optimistic offline lock
Pessimistic offline lock
Registry
Lazy load
Name:
Lazy load
Classification:
Object-relational behaviors design patterns
Also known as:
---
Intent:
This design pattern tries to load an object’s data when needed. In this case, while object initialization, no data is loaded, which can positively affect performance.
Motivation, Structure, Implementation, and Sample code:
Suppose there is a requirement in which the customer’s information is returned along with his orders. But in some places of the program, only customer information is needed, and in other places, orders are needed. In this case, receiving customer orders for each request will not be pleasant and can harm efficiency. Another way is to fetch the customer’s information from the database in every request but not fetch his orders. Wherever orders are needed, then the orders are picked up. After retrieving orders, if new orders are needed, the same list of previous orders can be returned.
The preceding method implements lazy load using the lazy initialization method. There are other methods to implement this design pattern, such as virtual proxy, value holder, and ghost, which we will learn about later.
Lazy initialization method
In Figure 8.5, you can see the class diagram of the Lazy Initialization method for the lazy load design pattern:
Figure%208.5.png
Figure 8.5: Lazy Initialization UML diagram
As shown in Figure 8.5 diagram, CustomerService receives order information from ExternalSource only once and only when needed. According to Figure 8.5 class diagram, the following codes can be considered:
public class Order
{
public int Id { get; set; }
public double Price { get; set; }
public int CustId { get; set; }
}
public class Customer
{
private List
public int Id { get; set; }
public string Name { get; set; }
public List
{
get
{
if (_orders == null)
{
Console.WriteLine($"Loading orders for customer: {this.Name}");
_orders = OrderDbSet.Orders
.Where(x => x.CustId == this.Id).ToList();
}
return _orders;
}
}
}
As it is clear in the preceding code, inside the Orders property, it is checked whether the list of orders has already been loaded. If it is not loaded, the list of orders will be loaded. If you prefer to Orders again, the list of orders will not be loaded from the beginning, and the same list as before will be referred to. It can also be seen in the preceding code that Orders have no value when the Customer information is fetched, and when it is needed, the values inside will be filled. To use this code, you can proceed as follows:
1. Customer customer = CustomerDbSet.Customers.FirstOrDefault(x => x.Id == 1);
2. List
3. List
In line 1, customer information is fetched. This information was not fetched in this line because Orders information was unnecessary.
In line 2, Orders are needed for the first time, so in this line, the list of customer orders will be fetched from the database.
In line 3, where Orders are needed again, fetching has not happened, and the same list of previous orders is returned.
NOTE: .NET has a class called Lazy, which you can easily delay object initialization until it is used. For example, consider the following code:
Lazy
According to the preceding code, no instance of Customer is created. To create n instance, the instance should be received through lazyCustomer.Value. Referencing lazyCustomer.Value again will return the same object as before. The Lazy class allows you to provide the initialization process in the form of a Lambda expression to the constructor of the Lazy class:
Lazy
{
Customer obj = new Customer();
//do something more with obj
return obj;
});
Now, using the Lazy class, you can rewrite the Customer class as follows:
public class Customer
{
private Lazy> _orders;
public int Id { get; set; }
public string Name { get; set; }
public Customer()
{
_orders = new Lazy>(() =>
{
return OrderDbSet.Orders.Where(x => x.CustId == this.Id).ToList();
});
}
public List
}
Virtual proxy method
In this method, the virtual proxy object has the same structure as the main object. The main object is created the first time the virtual proxy object is requested. Then, the originally created object is returned whenever a reference is made to the virtual proxy. This implementation method combines proxy design patterns and lazy initialization method:
public interface IService
{
public List
public int Id { get; set; }
public string Name { get; set; }
}
public class Customer : IService
{
private List
public int Id { get; set; }
public string Name { get; set; }
public Customer() =>_orders = OrderDbSet.Orders
.Where(x => x.CustId == this.Id).ToList();
public List
}
public class CustomerProxy : IService
{
private IService _service;
private void InitIfNeeded()
{
if (_service == null)
_service = new Customer();
}
public List
{
get
{
InitIfNeeded();
return _service.Orders;
}
}
public int Id { get; set; }
public string Name { get; set; }
}
Ghost method
In this method, the desired object is incompletely loaded, and only information related to the primary key or main information is loaded. Other information that has not been loaded will be loaded on the first reference to them. The first reference to data that has not been loaded will cause all data to be loaded.
public class Customer
{
private string _name;
private List
private bool _isOrdersLoaded;
private bool _isloaded;
public int Id { get; set; }
public string Name
{
get
{
if (!_isloaded)
Load();
return _name;
}
}
public List
{
get
{
if (!_isOrdersLoaded)
LoadOrders();
return _orders;
}
}
private void Load()
{
var customer = CustomerDbSet.Customers
.FirstOrDefault(x => x.Id == this.Id);
this._name = customer.Name;
_isloaded = true;
}
private void LoadOrders()
{
_orders = OrderDbSet.Orders.Where(x => x.CustId == this.Id).ToList();
_isOrdersLoaded = true;
}
}
Value holder method
In this method, another object called the value holder is responsible for managing lazy loading, and this object is used in the main object. The problem with this method is that the user must be aware of the presence of the value holder:
public class Customer
{
private ValueHolder _valueHolder;
public int Id { get; set; }
public string Name { get; set; }
public Customer() => _valueHolder = new ValueHolder(this.Id);
public List
class ValueHolder
{
private List
private readonly int _id;
public ValueHolder(int id) => _id = id;
public List
{
if (_orders == null)
_orders = OrderDbSet.Orders.Where(x => x.CustId == _id).ToList();
return _orders;
}
}
}
Notes:
Using this design pattern when producing web applications or websites is very important. For example, loading an image or iframe only when the page is scrolled enough can have a significant impact on the initial loading speed of the web page.
The opposite of lazy loading is the eager loading method. The eager method will load the required resources once the code is executed.
Consequences: Advantages
Because it is not necessary to set all the objects, it increases the efficiency.
Using this design pattern in web-based applications will reduce overall bandwidth consumption.
Consequences: Disadvantages
Using this design pattern can increase the complexity because we always need to check whether the desired object has been loaded or not, or basically whether the desired object needs to be loaded lazily or not.
Applicability:
This design pattern can be very effective in implementing the singleton design pattern.
IQueryable or IEnumerable types support a type of lazy load called deferred execution.
The lazy load can be used in entity Framework when we need to get data from the Join of several tables.
When the complete loading of the object is not needed or is expensive, using this design pattern can be useful.
Related patterns:
Some of the following design patterns are not related to the lazy load design pattern, but to implement this design pattern, checking the following design patterns will be useful:
Eager loading
Singleton
Proxy
Conclusion
In this chapter, you learned how to properly design and manage business transactions using the unit of work design pattern. You also learned how to prevent redundant references to data sources using an identity map design pattern. Finally, you learned how to use a lazy load design pattern to retrieve only the required data from the data source. Now that you are familiar with these design patterns in this chapter, you will be familiarized with object-relational structures design patterns in the next chapter.
Join our book's Discord space
Join the book's Discord Workspace for Latest updates, Offers, Tech happenings around the world, New Release and Sessions with the Authors:
https://discord.bpbonline.com