|
|
| Cascading Deletes |
Level 200
DevForce Express |
April
26,
2007 |
Suppose we have
a certain type of entity with
related children. When we delete
an instance of that entity, we
want all of its children to be
deleted as well. What’s
the best approach to implementing
this capability in DevForce?
When performing operations on the graph of an object, we need to keep
in mind the potential difference between how that object graph appears
in the PersistenceManager’s local cache, and how it may appear
in the data source.
Suppose our object graph is for an Order object, and that the related
objects we want to delete consist solely and entirely of the related
OrderDetail objects. Assume, for a moment, the following:
- We will access the OrderDetails
of the Order to be deleted
through the Order object’s
OrderDetails relation property;
and
- We did not prefetch any
OrderDetails
In that case, the OrderDetails
in the cache for Order A will
be those that existed at the
time we first referenced that
OrderDetails property in our
code. If we have not referenced
it prior to attempting the
delete on Order A, it will
be referenced for the first
time in whatever method we
provide to do the deletion.
But either way, additional
time will pass between the
moment when we delete the Order
and its OrderDetails and the
moment when we commit those
deletions to the datasource
using a SaveChanges(). Unless
we have some locking scheme
in place to prevent other users
from adding OrderDetails to
Order A, the possibility exists
that they will do so, and that
we will still end up with orphaned
OrderDetail records in the
database.
If we don’t want that
to happen, then we have two
choices:
|
1.
|
Implement a locking
scheme; |
2.
|
Implement a trigger on the database
that deletes the OrderDetail records
when their Order record is deleted. |
3.
|
Set the value of the Tag property
for one of the RadioButtons to
one of the possible values of the
targetted business object property.
Do the same for the remaining RadioButtons,
until each possible value that
the business object property can
take on is represented by exactly
one RadioButton. |
All right, suppose
we’ve done one of the preceding,
or that we’re willing to
live with an occasional orphaned
record in the database.* How
can we implement the deletion
of the OrderDetails?
There are three basic options (short of writing ad hoc code everywhere
we want the operation to occur):
|
1.
|
Override the Delete() method
in the Order class |
2.
|
Provide a separate method – let’s
call in DeleteGraph() – in
the Order class to do the job,
leaving the Delete() method alone;
or |
3.
|
Provide some other object to
watch the PersistenceManager for
Order deletions and step in to
delete the related OrderDetails
whenever such a deletion is requested. |
Overriding
the Delete() Method
This option is the one that comes to mind first for many developers. However,
prior to version 3.5.0 of DevForce, it was a pseudo-option, because the
Delete() method in a DevForce business class was inherited from System.Data.DataRow,
where it is marked as sealed (non-overrideable). But, beginning with version
3.5.0, the Entity class contains a Delete() method which shadows System.Data.DataRow,
is marked as virtual, and can thus be overridden. |
| |
|
* the
incidence of which we could minimize
by always doing a DataSourceOnly
retrieval of the OrderDetail records
just before marking them for deletion;
and always calling SaveChanges()
immediately after so marking them. |
| Here’s a Delete()
override that addresses the use
case we proposed above for deletion
of an Order and its related OrderDetails: |
| |
C#:
public
override void Delete()
{
EntityList<OrderDetail> orderDetails =
new EntityList<OrderDetail>(this.OrderDetails);
int orderDetailsCount = orderDetails.Count;
for (int counter
= 0; counter < orderDetailsCount; counter++) {
orderDetails[0].Delete();
}
base.Delete();
}
VB.NET:
Public Overrides Sub Delete()
Dim orderDetails As EntityList(Of OrderDetail) = New EntityList(Of
OrderDetail)(Me.OrderDetails)
Dim orderDetailsCount As Integer = orderDetails.Count
Dim counter As Integer = 0
Do While counter < orderDetailsCount
orderDetails(0).Delete()
counter += 1
Loop
MyBase.Delete()
End Sub
|
Calling
Delete() on an Entity Cast
to a DataRow
Since Entity.Delete() shadows
rather than overrides DataRow.Delete(),
the developer must be aware
that calling Delete() on a
business object that has been
cast to a DataRow will produce
a different result than calling
Delete() directly on the business
object (in the case where Delete()
has been overridden in the
developer class). The former
will only delete the specified
DataRow itself, and will perform
none of the supplementary actions
coded into the Delete() override.
You could, for example, substitute for the following line from the above
method: |
| |
C#:
base.Delete();
VB.NET:
MYbase.Delete(); |
| this one: |
| |
C#:
((DataRow).Delete();
VB.NET:
CType(Me,
DataRow).Delete() |
Neither statement
results in a recursive method call,
since the referenced method, in
both cases, is a different one
from the one in which the statement
resides.
Providing a DeleteGraph()
Method
In this option you leave the
Order class’s Delete() method alone and provide
a separate method to do your cascading deletes. We’ll say here, for convenience,
that we name this method DeleteGraph(), but you can of course call it whatever
you like. Whatever its name, its operation will be as follows: |
1. |
Delete all desired related records,
then |
2. |
Delete the Order object itself |
| Such a method might
look like the following (assuming
for simplicity, that the OrderDetail
objects are the only related objects
to be deleted): |
| |
C#:
public int DeleteGraph() {
int numberOfItemsDeleted = 0;
EntityList<OrderDetail> orderDetails =
new EntityList<OrderDetail>(this.OrderDetails);
int orderDetailsCount =
orderDetails.Count;
for (int counter = 0;
counter < orderDetailsCount;counter++){
orderDetails[0].Delete();
}
this.Delete();
return (orderDetailsCount + 1);
}
VB.NET:
Public Function DeleteGraph() As Integer
Dim
numberOfItemsDeleted As Integer = 0
Dim
orderDetails
As EntityList(Of OrderDetail) = New EntityList(Of OrderDetail)(Me.OrderDetails)
Dim
orderDetailsCount As Integer = orderDetails.Count
Dim counter As
Integer = 0
Do
While counter < orderDetailsCount
orderDetails(0).Delete()
counter += 1
Loop
Me.Delete()
Return
(orderDetailsCount + 1)
End Function |
This
method remains subject to the
limitations previously discussed
concerning other related OrderDetails
that may exist but have not
yet been retrieved into the
local cache; but assuming you’ve
covered that base to your satisfaction,
it
will do the job. To use it, you simply
call
|
| |
currentOrder.DeleteGraph()
instead of:
currentOrder.Delete() |
whenever
you want the cascading delete. This
is a simple and straightforward
approach.
Provide a PersistenceManager Watcher
In
this option you provide a class
that watches the PersistenceManager
for requests to delete Order
objects, and intervenes before
those deletions are consummated
to delete the related child
objects.**
It does the latter by means of a handler for
the Order table’s Row_Deleting event. The
Row_Deleting handler is set
up in the class’s constructor. In the implementation below, a Row_Deleted
handler is also set up, and
provides feedback on what occurred;
but that is only for pedagogical
purposes. |
| |
** The
class as shown here is only watching
the Order table, and only for deletions;
however, clearly its scope could
be enlarged to include other tables
and other operations |
| |
C#:
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Windows.Forms;
using Model;
using IdeaBlade.Persistence;
namespace UI {
class PmWatcher {
public PmWatcher(PersistenceManager pPersMgr) {
mPersMgr = pPersMgr;
SetWatchersForOrder();
//Could set watchers here for other tables, as desired
}
private void
SetWatchersForOrder() {
DataTable orderTable =
mPersMgr.GetTable(typeof(Order));
orderTable.RowDeleting +=
new DataRowChangeEventHandler(OrderTable_RowDeleting);
orderTable.RowDeleted
+=new DataRowChangeEventHandler(OrderTable_RowDeleted);
}
#region Row_Deleting Handler
/// <summary>
/// Delete related OrderDetail rows before consummating the
delete of the targetted Order.
/// </summary>
void OrderTable_RowDeleting(object sender, DataRowChangeEventArgs e) {
DeleteRelatedObjects(e);
}
private static void DeleteRelatedObjects(DataRowChangeEventArgs
e) {
Order currentOrder = (Order)e.Row;
EntityList<OrderDetail> orderDetails = new
EntityList<OrderDetail>(currentOrder.OrderDetails);
int orderDetailsCount =
currentOrder.OrderDetails.Count;
for (int counter
= 0; counter < orderDetailsCount; counter++) {
orderDetails[0].Delete();
}
}
#endregion
#region Row_Deleted Handler
/// <summary>
/// Report number of items in PM that are marked for deletion.
/// </summary>
/// <remarks>
/// This handler's action is intended simply for pedagogical
purposes, to provide
/// feedback that the RowDeleting handler actually worked.
/// </remarks>
private void
OrderTable_RowDeleted(object sender, DataRowChangeEventArgs e) {
DisplayCountOfRecordsDeleted();
}
private void
DisplayCountOfRecordsDeleted() {
EntityList<Entity> deletedEntities = new
EntityList<Entity>(mPersMgr.GetEntities(DataRowState.Deleted));
String msg = String.Format("Here in
RowDeleted handler. " +
"There are now {0} records marked for deletion in the
PM.",
deletedEntities.Count.ToString());
MessageBox.Show(msg);
}
#endregion
#region Private Fields
PersistenceManager mPersMgr;
#endregion
}
}
VB.NET:
Imports System
Imports
System.Collections.Generic
Imports System.Text
Imports System.Data
Imports
System.Windows.Forms
Imports Model
Imports
IdeaBlade.Persistence
Namespace UI
Friend Class
PmWatcher
Public Sub New(ByVal pPersMgr As PersistenceManager)
mPersMgr = pPersMgr
SetWatchersForOrder()
'Could set watchers
here for other tables, as desired
End Sub
Private Sub
SetWatchersForOrder()
Dim
orderTable As DataTable = mPersMgr.GetTable(GetType(Order))
AddHandler
orderTable.RowDeleting, AddressOf
OrderTable_RowDeleting
AddHandler
orderTable.RowDeleted, AddressOf
OrderTable_RowDeleted
End Sub
#Region "Row_Deleting Handler"
''' <summary>
''' Delete related OrderDetail rows before consummating the
delete of the targetted Order.
''' </summary>
Private Sub
OrderTable_RowDeleting(ByVal sender As Object, ByVal e As
DataRowChangeEventArgs)
DeleteRelatedObjects(e)
End Sub
Private Shared Sub DeleteRelatedObjects(ByVal
e As DataRowChangeEventArgs)
Dim
currentOrder As Order = CType(e.Row, Order)
Dim
orderDetails As EntityList(Of OrderDetail) = New
EntityList(Of
OrderDetail)(currentOrder.OrderDetails)
Dim
orderDetailsCount As Integer
= currentOrder.OrderDetails.Count
Dim counter As Integer = 0
Do While counter < orderDetailsCount
orderDetails(0).Delete()
counter += 1
Loop
End Sub
#End Region
#Region "Row_Deleted Handler"
''' <summary>
''' Report number of items in PM that are marked for
deletion.
''' </summary>
''' <remarks>
''' This handler's action is intended simply for
pedagogical purposes, to provide
''' feedback that the RowDeleting handler actually worked.
''' </remarks>
Private Sub
OrderTable_RowDeleted(ByVal sender As Object, ByVal e As
DataRowChangeEventArgs)
DisplayCountOfRecordsDeleted()
End Sub
Private Sub
DisplayCountOfRecordsDeleted()
Dim
deletedEntities As EntityList(Of Entity) = New
EntityList(Of
Entity)(mPersMgr.GetEntities(DataRowState.Deleted))
Dim msg As String = String.Format("Here in RowDeleted handler.
" & "There are now {0} records marked for deletion in the
PM.", deletedEntities.Count.ToString())
MessageBox.Show(msg)
End Sub
#End Region
#Region "Private Fields"
Private mPersMgr As
PersistenceManager
#End Region
End Class
End Namespace
|
This
watcher must be activated somewhere,
before the Order deletions
can be initiated. In the attached
sample app (“CascadingDeleteWithPmWatcher”)
we have done so in the Form_Load
event handler for the application’s
main form. That handler calls
another method that issues
the following statement:
|
| |
C#:
mPmWatcher = new PmWatcher(mPersMgr);
VB.NET:
mPmWatcher = New PmWatcher(mPersMgr)
|
where
mPmWatcher was declared elsewhere
at the class level.
This
approach to the problem has
the advantage of leaving Delete()
as the method to be used even
for objects with related objects
that need to be deleted. It also centralizes the exceptional delete
logic, which you may consider
either an advantage or a disadvantage,
depending upon your perspective.
Conclusion
None
of the methods illustrated
for implementing cascading
deletes is clearly superior
to the others for all circumstances. Make
your choice based on other
factors in your development
environment, or simply according
to your personal taste. They
all work! |
|
|
All
Tech Tips |