The Pragmatic DAL Architecture: Data Materialization & Concurrency Control
— The flexibility and maintainability of an application relies on the location of DAL processing.
Co-Authored by Bo Griffin
One of the responsibilities of a DAL is to produce objects that an application can use. The location where this processing occurs has a dramatic impact on the flexibility and maintainability of an application. In this article, we will go over eager data materialization, deferred data materialization, and concurrency control.
Eager Data Materialization
Eager materialization is the process of converting data into objects before leaving the data layer. Eager loading promotes a deterministic data-loading behavior. Code that interacts directly with the data-source is separate from the business logic. By separating the concerns, the application layer would become thinner. Both the DAL and the application layers would become more testable. The better test coverage can help diagnose whether a problem is data-related or application-related.
Deferred Data Materialization
Deferred materialization is the process of converting data into objects after leaving the data layer. This is possible using either lazy loaded properties or lazy loaded collections. The lazy loaded collections keep a reference to the DAL. When the application attempts to enumerate the collection, the actual loading of the data takes place. IQueryable <T> is an important collection interface. Implementations have the capability of capturing query filters and projections. This allows the application to express its intent and the implementation will evaluate the query remotely.
The deferred behavior, although powerful, can often cause developer headaches. Developers may assume incorrectly that the data is already fully loaded when all they have is a reference. Accessing the collection multiple times can result in accessing the database more times than the developer intended. The developers have to be careful managing the lifetime of the connection to the database. Disposing the connection too early may result in unexpected behavior. Depending on the situation, the data provider may either return the data that has already been loaded so far, or throw an exception. Since the application controls what parts of the objects are loaded, it may produce instances that are not all loaded equally. This may make the application behave unexpectedly. This makes unit-test coverage a much more important issue. Since the code is aware of the structure of the database, this keeps the database and application tightly coupled. Most schema changes to the database will require corresponding changes to the application.
IQueryable <T> providers can be difficult to change since not all providers have the same level of feature compatibility. Changing code from deferred to eager can be impact performance dramatically. If you had an IQueryable <T> method that needed to change to an IList, the side effect would often be loading a lot more data than was actually needed since any filtering would be executed in the application instead of in the database.
Deferred materialization, although powerful, can have its surprises.
Concurrency control is the behavior of how multiple writers are coordinated. Each model could have a configurable level of concurrency control. Each application may require different levels of transactional isolation. The most common options are:
- None – last writer wins
- Optimistic – first writer wins
- Pessimistic – first to acquire exclusive lock wins