Cascade delete with Linq To SQL

T

Thomas Jespersen

Hi

I have a problem getting Linq To SQL to cascade delete OnToMany
relations.

Lets say I have an Order with a collection of OrderLines. I never want
to get or save OrderLines directly... I always want to fetch
OrderLines through the Order.

If I add an OrderLine to an Order, and save the Order. The added
OrderLine gets saved to the database automatically.

But if I remove the OrderLine, it doesn't get deletede in the datbase,
when saving the order.

Can anyone tell me how to get Linq To SQL to cascade delete my
OrderLines?

I already tried DeleteRule = "CASCADE" on the EntitySet<OrderLine> (on
the Order class), but it does not work.


Here are two tests that illustrates the problem (i have at
TestInitialize, which creates an empty database and inserts an order
with 2 order lines).


This test fails on the last assert (the order line is not deleted in
the database when saving the order).

[TestMethod]
public void CanRemoveOrderLine()
{
const int orderId = 1234;
Order order = from o in dataContext.Order where o.Id == orderId
select o).Single();
Assert.AreEqual(2, order.OrderLines.Count(), "There should be 2
order lines on this order.");

//Remove the first orderLine
order.OrderLines.RemoveAt(0);
dataContext.SubmitChanges();

string countOrderLinesSql = String.Format("Select Count(*) From
Order Where OrderId = '{0}'", orderId);
Assert.AreEqual(1, ExecuteScalarSql<int>(countOrderLinesSql),
"There should now only be 1 Order line belonging to the order.");
}


This test however is working! (the new order line is saved when saving
the order)

[TestMethod]
public void CanAddOrderLineToOrder()
{
const int orderId = 1234;
Order order = from o in dataContext.Order where o.Id == orderId
select o).Single();
Assert.AreEqual(2, order.OrderLines.Count(), "There should be 2
order lines on this order.");

//Add a new Order line
var newOrderline = new OrderLine() {Order = order};
order.AddOrderLine(newOrderline);
dataContext.SubmitChanges();

string countOrderLinesSql = String.Format("Select Count(*) From
Order Where OrderId = '{0}'", orderId);
Assert.AreEqual(3, ExecuteScalarSql<int>(countOrderLinesSql),
"There should now be 3 Order line belonging to the order.");
}


My classes look something like this:


public class Order
{
[Column(Name = "Id", IsPrimaryKey = true)]
public Guid OrderId { get; set; }

[Association(OtherKey = "OrderId", DeleteRule = "CASCADE")]
public EntitySet<OrderLine> OrderLines { get; set; }
}


public class OrderLine
{
private EntityRef<Order> order;
[Column] private Guid OrderId;

[Column(Name = "Id", IsPrimaryKey = true)]
public Guid OrderLineId { get; set; }

[Association(Storage = "order", ThisKey = "OrderId")]
public Order Order
{
get { return order.Entity; }
set
{
order.Entity = value;
OrderId = value.OrderId;
}
}
}

Why does adding work, and not deleting?

Any help is much appreciated.

: Thomas
 
F

Frans Bouma [C# MVP]

Thomas said:
Hi

I have a problem getting Linq To SQL to cascade delete OnToMany
relations.

Lets say I have an Order with a collection of OrderLines. I never want
to get or save OrderLines directly... I always want to fetch
OrderLines through the Order.

If I add an OrderLine to an Order, and save the Order. The added
OrderLine gets saved to the database automatically.

But if I remove the OrderLine, it doesn't get deletede in the datbase,
when saving the order.

Can anyone tell me how to get Linq To SQL to cascade delete my
OrderLines?

I already tried DeleteRule = "CASCADE" on the EntitySet<OrderLine> (on
the Order class), but it does not work. (code snip)

Why does adding work, and not deleting?

Because adding isn't deleting.

THe thing is that in-memory entity graph management doesn't have to
reflect into database entity graph management. Consider the fact that
you fetch for example a collection of 20 customer entities. In-memory
you pass this collection to a routine which post-processes the fetched
entity collection to filter out some customers by applying a set of
rules. Because of this, 2 customer entity objects are removed from the
collection. The 18 entities left are displayed to the user and the user
edits them.

The user then clicks 'save'. The collection of 18 entities are saved to
the db. However, do you now also expect to have the 2 entities removed
from the collection in-memory due to business rule constraints to be
removed from the db? No of course not.

So removing from a collection is ambiguous.

Another aspect is that cascading deletes by an O/R mapper are not
doable, unless temp tables are used during the process and even then
it's not always successful (consider the situation where 2 or more paths
lead from entity A to entity B and you remove A)

Say you fetch order O and O has 10 orderlines. However you fetch only
the orderlines of O which have a quantity > 10, and 3 of the orderlines
match that filter. If you now delete O through the o/r mapper, the o/r
mapper first has to remove the FK sides pointing to O. However at least
7 orderline entities aren't in-memory, so the O/R mapper doesn't know
the ID's for these. This means that the o/r mapper has to fetch them
first, create a query for them and then remove the order lines, then O.
But there might be more entities referring to O, so this becomes a
painful process.

If you extrapolate this to a hierarchy with more levels, and more
branches, (like removing a customer, the orders have to go, the
orderlines etc.) it becomes a nightmare if all these entities have to be
fetched. You also run the risk of having the situation where you have to
refer to a set of entities which aren't there anymore, becuase you
already removed them. That's also the reason why for example sqlserver
not always allows you to enable cascading deletes inside the db.

In situations with inheritance across multiple tables, it gets
impossible unless temptables is used. See the in-depth article about
this over at the hibernate blog:
http://in.relation.to/Bloggers/MultitableBulkOperations

FB

--
------------------------------------------------------------------------
Lead developer of LLBLGen Pro, the productive O/R mapper for .NET
LLBLGen Pro website: http://www.llblgen.com
My .NET blog: http://weblogs.asp.net/fbouma
Microsoft MVP (C#)
------------------------------------------------------------------------
 
T

Thomas Jespersen

Hi Frans

Thanks for a very insightful answer. One can really tell that you know
a thing or two about OR Mappers ;-)

Now that you "spell it out" I fully understand why this is so hard.
I've been working with NHibernate for several years, and I guess that
I just expect this feature of an OR Mapper.

In NHibernate you simply set CascadeStyle = AllDeleteOrphan, and it
will delete an object if it is removed form a collection. Your
description shows me how this will not be a good behavior in all
cases.

I guess I have to come up with some clever way, to get this behavior
without requiring the user of my client code to delete the order lines
manually.


Do you know, if it is possible to get the original object form the
Linq to SQL datacontext, so I can compare my current OrderLines
collection with the original?


: Thomas
 
F

Frans Bouma [C# MVP]

Thomas said:
Hi Frans

Thanks for a very insightful answer. One can really tell that you know
a thing or two about OR Mappers ;-)

Now that you "spell it out" I fully understand why this is so hard.
I've been working with NHibernate for several years, and I guess that
I just expect this feature of an OR Mapper.

In NHibernate you simply set CascadeStyle = AllDeleteOrphan, and it
will delete an object if it is removed form a collection. Your
description shows me how this will not be a good behavior in all
cases.

I guess I have to come up with some clever way, to get this behavior
without requiring the user of my client code to delete the order lines
manually.

:)

LLBLGen Pro has a feature where you can inject an entity collection
inside another entity collection which is then tracking removed entities
from the collection. So in your case, you set this
RemovedEntitiesTracker of the OrderLines collection to a new entity
collection, and when an orderline entity is removed from the orderlines
collection, it's added to the collection set as RemovedEntitiesTracker.

When the user is done, you grab the removedentitiestracker and simply
remove all entities in that collection.

In Linq to Sql you could do something similar I think, by adding some
code to the Order class, which binds an eventhandler to the OrderLines'
ListChanged event and then checks if an entity was removed from it. If
so, it adds that entity to a list of entities you keep inside Order
similar to the RemovedEntitiesTracker feature I discussed above. :)
Do you know, if it is possible to get the original object form the
Linq to SQL datacontext, so I can compare my current OrderLines
collection with the original?

Not that I'm aware of

FB
--
------------------------------------------------------------------------
Lead developer of LLBLGen Pro, the productive O/R mapper for .NET
LLBLGen Pro website: http://www.llblgen.com
My .NET blog: http://weblogs.asp.net/fbouma
Microsoft MVP (C#)
------------------------------------------------------------------------
 
T

Thomas Jespersen

Hi Frans

Thanks for yet another great answer. You really deserve your MVP
titel. I just don¨t get how you get the time to be so active in all
these online communities.

Anyway... I think I have an idea to how I can acomplish my goal. The
orginal code was just a cut-down-version. In reality I have a DDD
Repository, whith my own Get, Save and Delete methods. As I would
never save an "aggregate" without having the entire object graph in
memory I figured that I could write come thing link this.

When saving: Delete From OrderLine Where Id not in ('id's of all order
lines in the collection')

When deleting: Delete From OrderLine Where OrderId = 'id of the
order'

Ofcause the Save and Delete methods on the Repository lives on an
abstract base class. So I might end up using reflection to acomblish
this, so I don't have to write custom code on the Order entity.

This might not be a bullet proof solution i all scenarios, but our
design has very strict guidelines regarding setting up Aggregates,
Entities and Value objects. E.g. no Entities has collections of other
Aggregates; all entity and value object collections must use
EntitySet<T> and so on.

I know you know about Domain Driven Design, so these terms
(Repositories, Aggregates, Entities and Value objects) makes sense to
you.

Thanks again for you answer and every thing else ;-)

: Thomas
 
F

Frans Bouma [C# MVP]

Thomas said:
Thanks for yet another great answer. You really deserve your MVP
titel. I just don¨t get how you get the time to be so active in all
these online communities.

Thanks :)
Well, I just type fast ;)
Anyway... I think I have an idea to how I can acomplish my goal. The
orginal code was just a cut-down-version. In reality I have a DDD
Repository, whith my own Get, Save and Delete methods. As I would
never save an "aggregate" without having the entire object graph in
memory I figured that I could write come thing link this.

When saving: Delete From OrderLine Where Id not in ('id's of all order
lines in the collection')

When deleting: Delete From OrderLine Where OrderId = 'id of the
order'

Ofcause the Save and Delete methods on the Repository lives on an
abstract base class. So I might end up using reflection to acomblish
this, so I don't have to write custom code on the Order entity.

And be sure you don't remove entities from the db which are removed
from the collection for other reasons ;)

FB

--
------------------------------------------------------------------------
Lead developer of LLBLGen Pro, the productive O/R mapper for .NET
LLBLGen Pro website: http://www.llblgen.com
My .NET blog: http://weblogs.asp.net/fbouma
Microsoft MVP (C#)
------------------------------------------------------------------------
 
T

Thomas Jespersen

Hi again

I successfully implemented a generic Casade delete (e.g. when I delete
an order all order lines are deleted as well)... see the code at the
end of the post for inspiration.

However, I'm still having problem deleting orphaned entities on
EntitySet<TEntity>. That is... if I delete one order line form an
order. I tried hooking up the ListChanged event on EntitySet, as you
suggested, but when this event triggers the entity is already removed
form the EntitySet, so I don't know which entity was removed. I have
to figure out a way to track the original collection, to determine
which objects has been removed.

I'm still working on a generic solution for this. Any suggestions are
welcome.



Anyway... here is my generic code for the RepositoryBase<T>, that
cascade delete entities. It is not fully tested.

public abstract class RepositoryBase<T> where T : Entity, IAggregate,
new()
{
public virtual T GetById(Guid id) {...}

public virtual void Save(T aggregate) {...}

public virtual void Delete(T aggregate)
{
DeleteRecrusive(aggregate);

RepositoryDataContext.DataContext.SubmitChanges();
}

protected static void DeleteRecrusive(Entity entity)
{
const BindingFlags flags = BindingFlags.NonPublic |
BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetField |
BindingFlags.GetProperty | BindingFlags.FlattenHierarchy;

//Check all properties on the aggregate, to determine if there
are Entities or collection of Entities, that needs to be deleted
foreach (PropertyInfo propertyInfo in
entity.GetType().GetProperties((flags)))
{
//Make sure this property is not an indexer.
if (propertyInfo.GetIndexParameters().Length == 0)
{
//If this were an indexer, we could not get its value.
Object entityCandidate = propertyInfo.GetValue(entity,
null);

if (entityCandidate != null)
{
//If this is an EntityRef<TEntity>, and it is ot
an aggregate in itselfs
if (entityCandidate.GetType().Name ==
"EntityRef`1" && !(entityCandidate is IAggregate))
//... then delete it
DeleteRecrusive((Entity)entityCandidate);

//If this is an EntitySet<TEntity>...
if (entityCandidate.GetType().Name ==
"EntitySet`1")
{
//... then delete all entities
foreach (var entityToDelete in
(IList)entityCandidate)
{
if ((entityCandidate is IAggregate))
throw new Exception("An entity should
never have a collection of Aggregates.");

DeleteRecrusive((Entity)entityToDelete);
}
}
}
}
}

//Determine the type of the entities
Type entityType = entity.GetType();

//Get a Linq To SQL table for the type
ITable table =
RepositoryDataContext.DataContext.GetTable(entityType);

//Mark the entity for deletion
table.DeleteOnSubmit(entity);
}
}

This dosn't work if you have Foreign key constraints on you tables,
because it seams to me that Linq to SQL does not know that it should
delete rows in a specific order! If anyone knows how to solve this,
I'm very interested.

: Thomas
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top