This content was cut from my most recent MSDN article on Persistence Patterns for length and possibly quality. What the heck, it's an important topic that throws some people off and it's also something that many people who are new to ORM's frequently don't understand. I'm also misusing the term "reachability" a bit here, so substitute "cascading save" wherever that term pops in.
Normal
0
false
false
false
EN-US
X-NONE
X-NONE
Many persistence tools support cascading updates and deletes through an object graph. This is
both a powerful tool for improved productivity and a source of confusion and
errors.
To explain this concept, let’s imagine that we’re building a
new invoicing system that has an Invoice class like this one:
// The Invoice
class is purposely simplified
// for this
example
public class Invoice
{
public IList<InvoiceLine>
Lines { get; set;
}
public Customer Customer { get;
set; }
}
The Invoice class has a list of InvoiceLine objects and a
“many-to-one” reference to a Customer object.
When our new invoicing system receives a message directing it to enter a
new Invoice, our system creates a new Invoice object, all the related
InvoiceLine objects, and assigns the proper existing Customer object to our new Invoice. That process is shown in the CreateInvoice()
method of the InvoiceService class shown below:
public class InvoiceService
{
private
readonly IUnitOfWork
_unitOfWork;
public
InvoiceService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public NewInvoiceConfirmation CreateInvoice(NewInvoiceMessage message)
{
// Read in
the NewInvoiceMessage object, create
// the
children InvoiceLine objects,
// and
attach an existing Customer object
Invoice
invoice = buildInvoiceHeader(message);
invoice.Lines =
buildInvoiceLines(message);
invoice.Customer =
lookupExistingCustomer(message);
//
Persist the new invoice
_unitOfWork.MarkNew(invoice);
// Commit
the transaction
_unitOfWork.Commit();
return
buildConfirmation(invoice);
}
}
In order to finish the new Invoice transaction, we need to
save the header information for the Invoice object, the reference from the new
Invoice to the existing Customer, and each of the individual InvoiceLine
objects. However, in the code above for
the InvoiceService.CreateInvoice() method we only marked the top Invoice object as a new object in our Unit of
Work. That’s because we’re depending on
our persistence tool to use a cascading update policy to walk the Invoice
structure and save all the new InvoiceLine objects at the same time.
In my last article I talked about using Object Relational
Mapping to create a persistence solution by configuring a mapping between the
object model and the database structure.
For the Invoice class in our new invoicing system, I would configure the
mapping from Invoice like this (using Fluent NHibernate):
public class InvoiceMap :
ClassMap<Invoice>
{
public
InvoiceMap()
{
// The
Cascade.All() directive forces NHibernate
// to
save the InvoiceLine children of an Invoice
// object
whenever the Invoice object is saved
HasMany<InvoiceLine>(x
=> x.Lines).Cascade.All();
// The
Cascade.None() directive tells NHibernate
// NOT to
save the Customer object when
// the
Invoice is saved.
//
// The
link from Invoice to Customer will be persisted
References(x =>
x.Customer).Cascade.None();
}
}
In this example, we’ve made the InvoiceLine children of an
Invoice object “reachable” when a new Invoice object is saved. In the case of our new invoicing system, I’m
assuming that new Invoices can only be created for existing Customer objects. Because the Customer object isn’t modified by
creating a new Invoice, we certainly don’t want the Customer object saved (or
even checked by our persistence tool for possible modifications) when a new
Invoice object is saved.
Because of the potential for errors due to incorrect
assumptions about how cascading updates are configured, my strong advice is to
use a battery of integration tests against your service layer to ensure that
data is persisted correctly. Tightening
up the rules and policies for cascading inserts, saves, and deletes is a common
way to optimize the performance of a system that uses Object Relational Mapping
for persistence. Again, this
optimization process will be much safer if it is supported by automated tests
that can catch regression errors caused by changing “reachability” rules.
Lastly, almost every business application of any size that
we build will contain hierarchical information.
Reachability is another great example of how an ORM persistence tool can
save development time versus old fashioned “hand-rolled” data access code. In this case, NHibernate is dealing with running the insert, update, or delete statements in the correct order and dealing with moving the new autonumber Id's from the parent to the children records. Believe it or not, I do know how to write that ADO.Net code from scratch and even how to write sproc's to do the data access, but call me lazy and shiftless 'cause I'd rather just go _unitOfWork.Save( myNewInvoice ) or _unitOfWork.Delete( myOldInvoice ) and get on with the next feature.
Posted
Sat, May 30 2009 5:34 PM
by
Jeremy D. Miller