NPersist has just been updated with a few new features that motivate stepping up the version number from v1.0.9 to v1.0.10. Inside NPersist has also been updated to cover the new features (as well as with a new section detailing the workings of an already existing aspect of the Inverse Manager - how it deals with lazy synchronization of inverse properties).
The new features in NPersist v1.0.10 are:
Read and Write Consistency Enforcement
The IContext interface has two new properties called ReadConsistency and WriteConsistency, each taking a ConsistencyMode enum which can be Optimistic or Pessimistic (or Default, which translates to Optimistic).
Optimistic Read Consistency (the default) is what NPersist has been doing so far – taking an optimistic approach to the consistency of data read into memory, not throwing exceptions to the left and right because objects are loaded outside of or in different transactions.
If you set ReadConsistency to Pessimistic, however, NPersist will enforce that objects that reference each other in a graph are not loaded during different transactions. The main use case this is supposed to address is when you want Lazy Loading rather than “Lucky Loading”, which is in all fairness what NPersist has been giving you to date.
The problem with Lazy Loading is that Dinesh was right when he concluded it often represents something of a Wild West attitude to data consistency.
Sometimes all you care about is data consistency in the database. As long as the proper consistency rules and constraints are in place to ensure that inconsistent data is not entered into the database, you’re good. In such a case, you’d probably go with Optimistic Read Consistency.
But other times it is important to ensure that the data in memory is consistent and accurate – perhaps because the in memory version of the data is used for some business process with real world side effects, such as sending out bills to customers.
In such cases, you would normally make sure that you read up all the data that you are going to need for the process to complete within a single transaction. Assuming the appropriate isolation levels are used, this ensures that we get a version of the data that nobody could have modified while we read it.
This doesn’t actually preclude lazy loading, however – it only means that the lazy loading will also have to take place within the transaction. Lazy Loading can help you avoid having to figure out exactly what data you’re going to need in advance (especially when that depends on sometimes complicated evaluations of some of the other data that should be fetched first) and having to write explicit code for fetching the conditional data (just accessing the properties will do). However, if you want Pessimistic Read Consistency, you’re going to want to be sure that the Lazy Loading doesn’t take place outside of the transaction.
So that’s what NPersist does when you set context.ReadConsistency = ConsistencyMode.Pessimistic – it throws a ReadConsistencyException if you try to Lazy Load outside of the right transaction.
In summation, a ReadConsistencyException will be thrown under the following conditions when ReadConsistency is set to Pessimistic.
- If an object is loaded or created outside of a transaction.
- If a property is lazy loaded outside a transaction or in another transaction than the object that the property belongs to was loaded or created in.
- If a property is loaded with a reference to an object that was loaded or created in another transaction than the object that the property belongs to was loaded or created in.
When it comes to Write Consistency, again we see that Optimistic Write Consistency is what NPersist has been giving you so far (and, again, it will remain the default behavior so that by updating to the new version of NPersist you will not immediately get ten million exceptions thrown in your face). Simply put, with Optimistic Write Consistency objects don’t have to be loaded and saved in the same transaction – instead we rely on Optimistic Concurrency to maintain write consistency (which is turned on by default in NPersist).
Pessimistic Write Consistency means that you won’t be using Optimistic Concurrency – instead NPersist will throw a WriteConsistencyException if you try to persist an object outside of the transaction it was loaded or created. In summation, a WriteConsistencyException will be thrown under the following conditions when WriteConsistency is set to Pessimistic.
- If an object is loaded or created outside of a transaction.
- If a property is written to outside of the transaction that the object that the property belongs to was loaded or created in.
- If a property is written to with a reference to an object that was loaded or created outside the transaction that the object that the property belongs to was loaded or created in.
- An object is inserted, updated or removed from the database outside the transaction that the object was loaded or created in.
Read and Write Consistency enforcement gives NPersist potential to address a wider range of use cases where higher levels of consistency should be enforced both for in memory and in database data.
Note that you have been able to address both these scenarios before (pessimistic read and write consistency) by encapsulating your operations correctly with transactions – it is just that NPersist would not help you out with enforcing this consistency before by throwing exceptions if you made a mistake.
Thus, when you have existing code where Pessimistic Consistency modes should be switched on, you probably know it already and have the appropriate transactions in place – turning Pessimistic Read/Write Consistency on will then help you enforce that consistency.
Eager List Count Loading
When NPersist loads an object with values from the database, it begins by loading the values for its non-list properties, whereas list properties default to being loaded lazily on first access. However, as of v1.0.10, NPersist will now default to loading the Count values for those list properties directly (“eagerly”) along with the non-list values when the object is first loaded.
NPersist does so using sub-selects in the SQL, so that loading a Customer with ID 42, the SQL might look something like this:
Select ID, FirstName, LastName, Email, (Select Count(*) From Orders Where Orders.CustomerID = Customers.ID) As OrdersCount From Customers Where ID = 42
This means that if you access just the Count property of the list object in one of your list properties that hasn’t been loaded with items yet, no lazy loading of the list has to be triggered. Before, NPersist would load the list with items from the database in order to be able to answer the call to the Count property whereas now this can be answered using the eagerly fetched count value.
Since your immediate reaction to this is probably that it is a horrible idea and you ask yourself how you can switch it off, the answer is: In the ListCountLoadBehavior property on the PersistenceManager (set it to Lazy: context.PersistenceManager.ListCountLoadBehavior = LoadBehavior.Lazy) or, if you prefer doing it in the xml mapping file, use the new “count” attribute in the domain element (. If you think the idea has merit but only for a few of your classes or properties, the “count” attribute is present on class and property level as well in your xml mapping file. Please refer to Inside NPersist for further explanation on how to use these new attributes).
This change of default behavior is why I felt the version of NPersist should be stepped up from v1.0.9 to v1.0.10 (we have otherwise had a decidedly, ahem, “conservative” (lazy) approach to stepping up version numbers) - to draw attention to it so you don’t get this changed behavior without noticing it in case you really don’t want it.
But before you rush to turn it off, allow me a minute to explain why I think this new behavior is useful and why making it the new default behavior is a good idea.
First off, it can obviously save you a fairly expensive lazy loading of list items in lists whenever you have code that looks something like this:
if (myCustomer.Orders.Count < 1)
return;
If that would be the only dividend, though, I would have agreed that eager list count loading should be off by default and turned on explicitly only where you have situations like that one.
The reason, rather, that defaulting to eagerly loading the list counts is that the third new NPersist feature - that I’m going to describe now - can make use of it to reduce the number of list properties being triggered to lazy load even further.
Inverse Property Resolution
Say that you load a number of customers from the database. Then you go on to load a number of orders from the database. Ten of the orders you load up happen to belong to the same customer, who happens to be loaded into memory by the previous query. Furthermore, those ten orders happen to be all the orders belonging to that customer. In other words, NPersist should be able to use these orders to fill the Orders property of the customer they belong to, so that when that list property is accessed it doesn’t have to be lazy loaded.
But the problem is – how could NPersist know that it has all the orders it needs to fill the Orders property of the customer? Well, if the count property for that Orders property had happened to have been eagerly loaded, that would allow NPersist to know that whenever it has found ten orders belonging to that customer, it could mark the property as loaded and fill it with the orders.
The orders do not even have to be loaded as part of the same query – the ten orders can come from many different queries spread out over time. But hold on, the paranoid developer may object at this point: What if the number of orders belonging to the customer changes in the database between these queries – couldn’t that give funny results?
Why, yes it very well could – glad you pointed it out, Mr Paranoid Developer! That’s why this new feature will also respect the new Pessimistic ReadConcurrency setting, should you choose to activate it.
Because this effect is really no worse than all the other inconsistencies of just the same kind that can happen to in memory graphs of the data if you allow the graphs to be partially loaded outside of (or in different) transactions. This is a good illustration of why Dinesh is right to feel skeptical about lazy loading.
However, in many situations it really isn’t catastrophic so long as you don’t end up with corrupt data in the database, and for all those situations you’re really not much more exposed to “weirdness” using this new feature than the standard lazy loading outside transactions. And when any such weirdness is highly undesirable, just set ReadConsistency to Pessimistic.
The module in NPersist responsible for knowing and filling list properties based on their eagerly loaded count values is, of course, the Inverse Manager, since it exploits the inverse property meta data information in order to do this.
Loading an order, the Inverse Manager uses the knowledge that the customer.Orders property is the inverse of the order.Customer property that is being filled with a reference to the customer when the order object is loaded to know that it can add a back reference to the order to the customer’s list property (making that list property partially loaded until it reaches the number of items matching the eagerly loaded Count value).
The opportunity for the inverse manager to intelligently resolve inverse references like this when objects are being loaded is new to NPersist – until now, the Inverse Manager has only been involved in synchronizing inverse properties when you write to them, never really getting involved when you loaded objects from the database (except for the Lazy Synchronization feature, described in the new Lazy Inverse Property Relationship Synchronization section in Inside NPersist).
The new Inverse Property Resolution feature of the Inverse Manager has the potential to significantly reduce the number of lists that cause a lazy loading operation being triggered on the first access to them. It also resolves reference properties (not just lists) meaning for example that One-One relationships will also be resolved by this new responsibility of the Inverse Manager - and resolving those relationships doesn’t even rely on eager list count loading being turned on.
But being able to resolve inverse relationships involving list properties is of course the main reason that eager list count loading is turned on by default. It is the feature that enables the Inverse Manager to perform Inverse Property Resolution on list properties.
The longtime reader of this blog may recall that the genius behind all this is of course in accordance with ancient tradition Roger Alsing (then Johansson) who came up with all this in a response to my O/R Mapping Challenge. As usual, I was merely the code monkey
However, the longtime reader with uncanny eye for detail may also remember that Roger’s suggestions (which have now been implemented) don’t actually meet the requirements of my challenge – the sub-selects in the SQL are strictly not allowed by my own stupid rules
However, they do allow for a far more powerful Inverse Property Resolution than a feature which would pass my challenge and which I am currently pondering an implementation for:
In the special case that all objects of a type are loaded (the conditions specified in my challenge) NPersist could add the information to an internal structure that for all inverse list properties referencing objects of this type, the Inverse Manager could go ahead and do its job (it wouldn’t need the eager counts for this since this is a special situation that would allow it to know that all objects for the lists would be there).
I’m not sure if I should add it, since it does feel like a kind of contrived optimization hack, perhaps more put in place to meet my own challenge than provide benefits for the users of NPersist…but on the other hand it would allow for a very efficient handling of that special case and in my opinion it isn’t even really a corner case scenario (some CMS load all active data for articles and such into memory in the front ends at application startup). So I may go ahead and implement it after all, finally making NPersist join the group of mappers able to pass my own challenge
Nonetheless, the current new set of features should be widely useful and help improve the overall performance and consistency of NPersist. And NPersist almost passes my challenge now!
Let me know what you think!