Complexities with NHibernate, DataTransferObjects and the Assembler Pattern(Ben Scott's Blog)
Posted on 2008-01-21 19:29 落花人独立 阅读(740) 评论(0) 编辑 收藏 举报Overview
I've recently implemented an enterprise application using NHibernate. In order to isolate the UI from changes to the domain model, the Data Transfer Object pattern (Fowler 03, MS) was decided upon. This gave us the additional benefit of being able to create our Dto's in such a way as that they would be easily bound to the UI, reducing complexity in the front end and giving us the ability to support different UI's with minimal effort.
The application architecture is something like this:
- Presentation Layer (ASP.Net Web Pages)
- Delegate / Service Agent Layer - implementations are dependency injected using Spring.Net, and web service proxies are wrapped and stripped of the type information generated by Visual Studio.Net.
- Web Service Layer (ASP.Net Web Services) - Wrapper functions decorated with the .Net attributes. Service Implementations are injected using Spring.Net
- Service Implementations - Contains the business logic of the application, performs assembly and dis-assembly of Dto's
- Persistence Layer - Data Access Objects that use NHibernate
To achieve this, several hand-coded two-way Assembler classes that constructed Dto's from Domain Objects (aka business entities) and then updated those Domain Objects were written. For simple classes this was an easy approach, and we could have used CodeSmith to generate some of the code for us.
However, due to our reasonably complex Dto's that weren't 1-1 with the Domain Objects our assemblers started becoming incredibly complex. While it is a tradeoff of the Dto/Assembly/Domain Object patterns that the complexity and nastiness associated with the loose coupling everywhere else in the application stack lives in the Assembler, it was a painful enough process to force me to re-consider the approach next time around.
The main difficulty arises when persisting changes made to the Dto. Since the Dto doesn't contain all of the data from the Domain Object in a 1-1 mapping, the process of updating our domain objects goes something like this:
- Using the ID and Delta (for concurrency management) values from the Dto, if the Dto.IsDirty = true then call the Data Access Object's GetXxx(id, delta) method to re-retrieve the domain object. (note: NHibernate caching is supposed to prevent a database operation, but our application server is stateless and to date I haven't been able to implement any of the recommended caching providers).
- Manually map the subset of fields on the Dto to the domain object.
- Process each child Dto in the same manner.
- Call the Data Access Object's StoreXxx(Dto) method.
Our main Dto was unable to be retrieved as a single domain object graph, and so calls to multiple Dao's are made inside the assembler to perform the domain object update.
As you can imagine, the Assemblers rapidly became fragile and contained a number of performance issues due to the recursive "chatty" calls across Dao's for the same information.
The alternative approach that was used on a less complex project was to perform the retrieval of the required domain objects outside of the assembler, and pass them all is as parameters to the assembler method. This approach fell over with the complex Dto's due to the service having to know everything about the assembly operation, pass through a massively complex set of parameters (which would have become unmanagable) and most importantly it required that the complete set of domain objects was retrieved even if a single value in the Dto was changed.
Which brings me to today, and I'm no closer to finding a solution that meets our requirements. I have assessed the architectural tradeoffs of the Dto/Assembly/Domain Object patterns in detail, and while not always appropriate it meets the needs of our application and none of the alternatives offer the flexibility and separation that is required.
So, on to the approach!
Assembler Scenarios
Three unique scenarios must be managed by the Assembly classes;
- The initial creation of the Data Transfer Object
- The retrieval of a Data Transfer Object based on already persisted domain objects.
- The persistence of data contained within the Data Transfer Object via updates to the Domain Objects.
Initial Creation
This isn't really an issue. A Factory based approach that constructs the parentDto by instantiating a new Dto and assigning empty Dto's to each child property. Items associated with DropDownLists e.t.c are populated in individual Dto's and bound to properties of the ResponseDto object.
Retrieval
Can be time-consuming to write depending on the degree of change between the Domain Objects and Data Transfer Objects, but still not really a major headache. The Services call Data Access Objects in the persistence layer which return domain objects. The Assemblers take domain objects as arguments and construct Data Transfer Objects that usually have a "flatter" structure. Multiple domain objects are usually required to construct a single Dto, and they are not always retrieved in the same object graph which can complicate things.
List items are optionally populated depending on the requirement (e.g. Read Only or Updatable)
Read Only DTO
The first scenario is for the retrieval of read only Dto's. In this case, only the ID and DisplayValue of the property bound to a list is constructed.
Updatable DTO
In this case, the entire List is populated, given that in most cases some UI logic is bound to the additional properties of objects within the lists.
Storage
Still unresolved and I welcome any thoughts on the approach!
Options being considered at the moment:
- A mechanism
for caching commonly used List data to reduce the "chatty" Dao calls
inside the assembler in conjunction with some code generation and a
Translation engine that would perform the bulk of the mapping.
- Separating the mapping from the persistence problem:
- Making
the Dto to Domain Object assembly operation the exact inverse of the
Domain Object to Dto operation producing partially complete (remember
the Dto contains only a subset of the data from the domain object) set
of domain objects which are passed to the Data Access Object.
- Writing a mechanism to synchronise the partially complete domain objects with the database (if we weren't using NHibernate it would look something like a dynamically generated UPDATE operation).
- Working out a way to get NHibernate to do all this for us! It works perfectly with Domain Objects, but the issue of not having all the data in the Dto limits the amount of functionality within NHibernate I can use.