Wednesday, June 9, 2010

DevExpress XPO – Deferred Deletion

One of the interesting features of XPO is the Deferred Deletion. Basically, instead of physically removing rows from your database, XPO marks them as deleted. Not only this can speed up the deletion but also helps to avoid some subtle concurrency issues.

One of our critical applications uses XPO as the ORM engine. As rows are deleted from the database, I never run a Purge mechanism which is designed to physically remove rows marked as deleted. My intention is simple – because all rows are kept in my database, I can always track any changes made by an end user. Even when a row is deleted, I can always peek into the database and conclude “you see, it’s not that our application deletes your data, in fact it’s been Mr.X/Mrs.Y who deleted it” (I mark every single row with the name of the last user who modifies the record).

There’s unfortunately an inconvenient issue with Deferred Deletion. Suppose you have a class (the class code has been simplified):

public class Person {
public int Oid { get; set; } // primary key
public string Name { get; set; }
public int ID_CITY { get; set; } // foreign key
}
and you delete an instance from the database.
Instead of just marking the GCRecord (a marking column in the database) as deleted, XPO also clears all foreign keys for such row! This is explained in the Remarks section of the documentation:

When a child object of a one-to-many association is deleted, XPO clears its reference to a parent object. This object remains orphan when restored. This behavior is by design, because the Deferred Deletion feature is not intended for the object restoration, but for overcoming complex foreign index constraints at the database level.


What does it mean?

Well, suppose you have a row in your database (NULL, 1, ‘The name’, 1) : (GCRecord, Oid, Name, ID_CITY). When XPO “deletes” it, the row becomes (11442123, 1, ‘The name’, NULL). The GCRecord column marks the row as deleted (with a random not null value) but in the same time the reference to the parent row is cleared.

How that’s possible – you ask – if the ID_CITY has the int type? Well, it does not matter whether you have int or int? in your model, XPO will always create a reference column as INT NULL (sic!).

Ok. So far, so good. Since Deferred Deletion is not designed to restore rows, it may not matter that the record has been altered in a way that it becomes an orphan (parent reference is gone!).

But now, you have a problem if you ever try to access such deleted record (and XPO allows that!) – your model insists that the ID_CITY has an int type, while the database holds a NULL value. XPO is then unable to create an instance of your business class.

Possible solutions?

First solution could be to alter your business classes so that all reference fields would always be of type int?. This does not seem acceptable, since it’s you to decide wheter a reference is required (int) or can be empty (int?) and XPO should not force you to stick with the only possibility.

Second, which works for me is to change the way you handle such reference properties in your business classes. It seems that before a query is sent to the database to update the GCRecord, XPO automatically invokes the setters of all reference properties (and sets them to null). Let’s then change the setter to handle this:

public class Child {
public Parent TheParent
{
get { ... }
set
{
// add this to prevent setting NULL value for your reference property
if ( this.IsDeleted ) return;
...
}
}
}

A remark - XPO claims that the Skip operator is implemented now. I’ll confirm that and write a note.

No comments: