|



|
Session Management and Caching
About Sessions
The XPO Session has evolved since XPO version 1. In the first version of XPO, the Session object encapsulated the object cache as well as the Dictionary, which stores all the metadata XPO has collected about persistent classes in the current application. In XPO 6.1, we have made substantial changes to this system by extracting a separate data layer. The data layer lies between the connection provider (interface IDataStore) and the Session and its task is to handle all the metadata management that was previously part of the Session. The main benefit of this change is that Session creation has now become an extremly lightweight process - in many use cases it's desirable to create new Session instances rapidly and regularly, but with XPO 1 this wasn't really an option because of the overhead of the Dictionary.
The Session as an Identity Map
As was already the case in XPO 1, the Session implements an object cache, which serves the purpose of an Identity Map. The Identity Map is best explained in Martin Fowler's book "Patterns of Enterprise Application Architecture" (summary here), but in short, its task is to make sure that when the same object is part of two separately queried result sets, then the exact same object should be returned - not a clone or a copy of the object. The following test should always pass (assuming there's a Person object in the database with the Name Billy Bott):
|
|  |
|
Person person1 = session.FindObject<Person>(new BinaryOperator("Name", "Billy Bott")); Person person2 = session.FindObject<Person>(new BinaryOperator("Name", "Billy Bott")); Assert.AreSame(person1, person2);
Dim person1 As Person = session1.FindObject(Of Person)(New BinaryOperator("Name", "Billy Bott")) Dim person2 As Person = session1.FindObject(Of Person)(New BinaryOperator("Name", "Billy Bott")) Assert.AreSame(person1, person2)
|
Note: The Session's object cache is not an optional feature, it can't be switched off.
The inner workings of the object cache are really very simple. Every time a query to the database is made, XPO checks for new versions of the relevant objects in the database (using Optimistic Locking, see below for details on this). If the database has more current information than the object cache, the relevant objects are updated in the cache. New objects are created as necessary and added to the cache, and the resulting collection of objects is returned. So if an object exists in the cache already, it is never created a second time and the requirements of the Identity Map are fulfilled.
How current is the object cache?
When copies of data are stored on the client side, the question to ask is obviously "how do we make sure that this data is always as current as necessary?" But while that question is pretty easy to come up with, the answer to it is not as easy - mostly because "as current as necessary" means different things in different projects. XPO implements and allows for a number of different approaches to deal with the problem of cache currency.
-
Using a new Session instance creates a new object cache. The shorter your Session instance's lifetime, the more current your object cache content. This is the only approach that doesn't break object cache consistency (for details on object cache consistency, see below) and it's the only generally recommended approach.
-
Using Optimistic Locking (again, see below for details), XPO checks the currency of objects in the cache automatically. The following test code shows how automatic updates work:
|
using (Session session = new Session( )) { new Person(session, "Billy Bott").Save( ); }
Session session1 = new Session( ); Session session2 = new Session( );
XPCollection<Person> peopleSession1 = new XPCollection<Person>(session1); Assert.AreEqual(1, peopleSession1.Count); Assert.AreEqual("Billy Bott", peopleSession1[0].Name);
Person billySession2 = session2.FindObject<Person>(new BinaryOperator("Name", "Billy Bott")); billySession2.Name = "Billy's new name"; billySession2.Save( );
XPCollection<Person> newPeopleSession1 = new XPCollection<Person>(session1); Assert.AreEqual(1, newPeopleSession1.Count); Assert.AreEqual("Billy's new name", newPeopleSession1[0].Name);
Assert.AreEqual("Billy's new name", peopleSession1[0].Name); Assert.AreSame(peopleSession1[0], newPeopleSession1[0]);
Using session1 As Session = New Session New Person(session1, "Billy Bott").Save End Using
Dim session1 As New Session Dim session2 As New Session
Dim peopleSession1 As New XPCollection(Of Person)(session1) Assert.AreEqual(1, peopleSession1.Count) Assert.AreEqual("Billy Bott", peopleSession1(0).Name)
Dim billySession2 As Person = session2.FindObject(Of Person)(New BinaryOperator("Name", "Billy Bott")) billySession2.Name = "Billy's new name" billySession2.Save
Dim newPeopleSession1 As New XPCollection(Of Person)(session1) Assert.AreEqual(1, newPeopleSession1.Count) Assert.AreEqual("Billy's new name", newPeopleSession1(0).Name)
Assert.AreEqual("Billy's new name", peopleSession1(0).Name) Assert.AreSame(peopleSession1(0), newPeopleSession1(0))
|
There are several methods that allow the programmer to reload content from the database explicitely - or at least it seems like this is what happens. There's Session.Reload(object), which reloads a single object, and XPBaseObject.Reload(), which calls into the Session method to reload itself. There's an alternate overload Session.Reload(object, forceAggregatesReload), which reloads all aggregated properties in addition to the object itself. And finally, there's XPBaseCollection.Reload(), which reloads all objects in a collection.
To prevent breaking object cache consistency, the Session.DropCache() method is available to drop the complete object cache content at once. Be aware that dropping the object cache completely will invalidate all objects that have previously been loaded through that cache, so you MUST reload them all before continuing to work with them! This is mainly an alternative to the use of multiple sessions, for cases where it's desirable to reuse the same Session instance.
The above code example could be modified like this to show collection reloading:
|
using (Session session = new Session( )) { new Person(session, "Billy Bott").Save( ); }
Session session1 = new Session( ); Session session2 = new Session( );
XPCollection<Person> peopleSession1 = new XPCollection<Person>(session1); Assert.AreEqual(1, peopleSession1.Count); Assert.AreEqual("Billy Bott", peopleSession1[0].Name);
Person billySession2 = session2.FindObject<Person>(new BinaryOperator("Name", "Billy Bott")); billySession2.Name = "Billy's new name"; billySession2.Save( );
Assert.AreEqual("Billy Bott", peopleSession1[0].Name);
peopleSession1.Reload(); Assert.AreEqual("Billy's new name", peopleSession1[0].Name);
Using session1 As Session = New Session New Person(session1, "Billy Bott").Save End Using
Dim session1 As New Session Dim session2 As New Session
Dim peopleSession1 As New XPCollection(Of Person)(session1) Assert.AreEqual(1, peopleSession1.Count) Assert.AreEqual("Billy Bott", peopleSession1(0).Name)
Dim billySession2 As Person = session2.FindObject(Of Person)(New BinaryOperator("Name", "Billy Bott")) billySession2.Name = "Billy's new name" billySession2.Save
Assert.AreEqual("Billy Bott", peopleSession1(0).Name)
peopleSession1.Reload Assert.AreEqual("Billy's new name", peopleSession1(0).Name)
|
When reloading data selectively, it is important to note that this is not always as explicit as one might think. The Session.Reload(...) methods are actually the only ones that have an immediate and inevitable effect - when they are called, the given object is refreshed from the database under all circumstances. When collections are reloaded, internally the collection is cleared and marked as "not loaded". So when it's accessed the next time, the "normal" data loading algorithm applies and the currency of the objects in the collection depends on the automatic mechanism above, utilizing Optimistic Locking.
Object Cache Consistency
The topic of object cache consistency has been mentioned a number of times in the previous paragraphs - so what do we mean by this? To explain, consider how the object cache in a given Session instance is filled. Objects are loaded, in collections or singly, and they are stored in the object cache. It is possible that objects which are returned in the result set of a specific XPCollection may be of mixed age - some may have been queried a while ago and existed in the object cache since then, while others have been queried and created just now.
On the surface, it appears to be very desirable to have the most current information available, everywhere and at every time. But if we give the matter a little more thought, it becomes clear that an automatic inplace update of objects has a lot of drawbacks of its own. For example, in one part of the application, using data from a global session, one algorithm might be happily doing its work, while in another part, for the same session, a different algorithm suddenly updates data from the database - that might be fatal to the outcome of the first routine, which obviously doesn't expect the data it works with to change suddenly. The whole topic is even more complicated if you imagine UI controls bound to data collections or single objects, and all the countless other situations where application state depends on data. Automatic notification is often not possible, so changes to data catch everybody unawares.
As these explanations try to show, current data is not always the single most valuable target. Sometimes it's much more important to have data that is consistent in itself, where the objects are all of the same "generation" and contain information that forms a complete picture correctly, from the application's point of view. Every partial change to this picture would contort the result.
This is why we talk about object cache consistency. Generally, as soon as you use a single instance of the object cache, i.e. a single Session instance, to do two or more kinds of work, you will fill the object cache with data that might not be logically consistent. Even worse, every time some of this data is refreshed from the database selectively, it's extremely probable that you destroy this consistency. The logical conclusion is simple: one single instance of the object cache, and as a conclusion, one single Session instance, should only ever contain data that is guaranteed to be logically consistent. A good way to be sure of this is to use Session instances liberally, one for each logical "part", each data handling "process" your application performs.
Despite our belief in these general recommendations, we have tried to make XPO 6.1 so flexible in its design that you can make these decisions yourself. We are trying to shed some light on the pros and cons of single vs. multiple session scenarios, as well as the various reloading practices - it's your decision which way your application is going to go.
Variations
Some of you may have noticed that the problem with object cache consistency is very similar to the problems that relational database systems try to solve with isolation levels. Isolation levels are only defined within the context of single transactions, which is why we can't directly reuse that idea in XPO - the lifespan of a persistent object is longer, maybe much longer, than a single transaction. But we have introduced a flag to switch XPO's behavior when changed objects are encountered in the database. This can be used to configure the "isolation" of data in the object cache, and thereby influence object cache consistency.
The flag is accessible as XpoDefault.OptimisticLockingReadBehavior (for the default value) and as Session.OptimisticLockingReadBehavior (for the session instance specific value) and it can have the following values:
| Value | Description |
| Ignore | Changed objects are never reloaded. Best object cache consistency. |
| ReloadObjects | Changed objects are automatically reloaded. |
| ThrowException | When changed objects are encountered, a LockingException is thrown. |
| Mixed | Outside of transactions, the behavior is ReloadObjects, inside transactions it's Ignore. |
The default value for this flag is currently "Mixed".
Data Layer Caching
In addition to the object cache described above, XPO also includes functionality for a cache on the data level. This system caches queries and their results as they are being executed on the database server. Two classes must be combined to form a cache structure, DataCacheRoot and DataCacheNode. It's possible to build cache hierarchies out of a single DataCacheRoot instance and any number of DataCacheNode instances - this makes sense when certain parts of an application need to use different settings for their data access, such as particularly current data. The minimum setup of the structure needs one DataCacheRoot and one DataCacheNode. The DataCacheNode is the one that actually caches data, the DataCacheRoot keeps some information global to that cache hierarchy.
Whenever a query passes the cache that has been executed before, the result from that query is returned to the client immediately, without a roundtrip to the server. The data cache's currency can be scaled using the MaxCacheLatency property. This is a TimeSpan that defines the maximum age a query (and the corresponding result set) can reach before being dropped from the cache. If more than one DataCacheNode is connected to a DataCacheRoot, the DataCacheRoot also handles updates to tables with cached results. Every time a specific DataCacheNode contacts the DataCacheRoot (for whatever reason), the table update information is pushed from the Root to the Node.
This test code shows how a data cache with two nodes can be constructed, where caching takes place and where the DataCacheRoot stops caching because of updates. This sample uses an InMemoryDataStore, which could of course be replaced by any other connection provider.
|
|  |
|
InMemoryDataStore dataStore = new InMemoryDataStore(new DataSet( ), AutoCreateOption.SchemaOnly); DataCacheRoot cacheRoot = new DataCacheRoot(dataStore);
DataCacheNode cacheNode1 = new DataCacheNode(cacheRoot); DataCacheNode cacheNode2 = new DataCacheNode(cacheRoot);
SimpleDataLayer dataLayer1 = new SimpleDataLayer(cacheNode1); SimpleDataLayer dataLayer2 = new SimpleDataLayer(cacheNode2); Session session1 = new Session(dataLayer1); Session session2 = new Session(dataLayer2);
Person billySession1 = new Person(session1, "Billy Bott"); billySession1.Save( );
XPCollection<Person> session2Collection = new XPCollection<Person>(session2); Assert.AreEqual("Billy Bott", session2Collection[0].Name);
billySession1.Name = "Billy's new name"; billySession1.Save( );
for (int i = 0; i < 5; i++) { session2Collection.Reload( ); Assert.AreEqual("Billy Bott", session2Collection[0].Name); }
new DerivedPerson(session2).Save( );
session2Collection.Reload( ); Assert.AreEqual("Billy's new name", session2Collection[0].Name);
Dim dataStore As New InMemoryDataStore(New DataSet, AutoCreateOption.SchemaOnly) Dim cacheRoot As New DataCacheRoot(dataStore)
Dim cacheNode1 As New DataCacheNode(root1) Dim cacheNode2 As New DataCacheNode(root1)
Dim dataLayer1 As New SimpleDataLayer(cacheNode1) Dim dataLayer2 As New SimpleDataLayer(cacheNode2) Dim session1 As New Session(dataLayer1) Dim session2 As New Session(dataLayer2)
Dim billySession1 As New Person(session1, "Billy Bott") billySession1.Save
Dim session2Collection As New XPCollection(Of Person)(session2) Assert.AreEqual("Billy Bott", collection1(0).Name)
billySession1.Name = "Billy's new name" billySession1.Save
Dim i As Integer = 0 Do While (i < 5) session2Collection.Reload Assert.AreEqual("Billy Bott", session2Collection(0).Name) num1 += 1 Loop
New DerivedPerson(session2).Save
session2Collection.Reload Assert.AreEqual("Billy's new name", session2Collection(0).Name)
|
Please note that in many practical use cases you'll need only one DataCacheNode. So the simplest sample code for data caching can look like this, using a connection provider for MS SQL Server:
|
XpoDefault.DataLayer = new SimpleDataLayer(new DataCacheNode( new DataCacheRoot(XpoDefault.GetConnectionProvider( MSSqlConnectionProvider.GetConnectionString("server", "database"), AutoCreateOption.DatabaseAndSchema))));
XpoDefault.DataLayer = New SimpleDataLayer( New DataCacheNode( _ New DataCacheRoot( XpoDefault.GetConnectionProvider( _ MSSqlConnectionProvider.GetConnectionString("server", "database"), _ AutoCreateOption.DatabaseAndSchema))))
|
This uses static methods of the connection provider and the XpoDefault class to create an instance of the MSSqlConnectionProvider, then wraps it it the caching classes and a SimpleDataLayer, and stores the whole thing in the XpoDefault class again for further use. All sessions that are now created with the default Session constructor will use this data layer, including the caching functionality.
Optimistic Locking
The primary purpose of Optimistic Locking is to prevent multiple clients from making modifications to the same object in the database. This is made possible by a field, by default called OptimisticLockField, that XPO adds to all persistent class tables. When objects are read from the database, the optimistic locking field is read together with the object content and stored. When a modification is made to an object in the database, the optimistic locking field is first checked and if it has been changed in the meantime, XPO throws an exception. If everything is okay, the modification is written to the database and the optimistic locking field is updated - incremented normally, as this is an integer field by default.
As you have seen in various places in this article, the Optimistic Locking functionality accounts for a number of additional features in XPO. It is possible to switch off Optimistic Locking by setting the Session.LockingOption property to LockingOption.None instead of the default LockingOption.OptimisticLocking. But be aware that by doing this, you will not only lose the change detection functionality described in the previous paragraph, but also the most of the selective reloading functionality - the only things that will still work are the XPBaseObject.Reload() and the Session.DropCache() methods.
Common Usage Scenarios
ASP.NET Applications
- Create an instance of ThreadSafeDataLayer and share that between all threads by storing it to XpoDefault.DataLayer. Make sure that the dictionary you pass into the constructor of ThreadSafeDataLayer is completely initialized; the easiest way to do this is to call CreateObjectTypeRecords right after UpdateSchema. This should happen during initialization of your application, for example in the Application_Start method in Global.asax.
|
IDataStore dataStore = XpoDefault.GetConnectionProvider( MSSqlConnectionProvider.GetConnectionString("server", "database"), AutoCreateOption.DatabaseAndSchema); using(SimpleDataLayer dataLayer = new SimpleDataLayer(dataStore)){ using (Session session = new Session(dataLayer)) { session.UpdateSchema( ); session.CreateObjectTypeRecords( ); XpoDefault.DataLayer = new ThreadSafeDataLayer(session.Dictionary, dataStore); } }
Dim dataStore As IDataStore = XpoDefault.GetConnectionProvider( _ MSSqlConnectionProvider.GetConnectionString("server", "database"), _ AutoCreateOption.DatabaseAndSchema) Using dataLayer As SimpleDataLayer = New SimpleDataLayer(dataStore) Using session1 As Session = New Session(dataLayer) session1.UpdateSchema session1.CreateObjectTypeRecords XpoDefault.DataLayer = New ThreadSafeDataLayer(session1.Dictionary, dataStore) End Using End Using
|
Windows Forms Applications - lots of windows and dialogs
** to be completed **
Service/Automation Applications - batch manipulation of data
** to be completed **
To learn more about XPO, please write to us at: info@devexpress.com.
To order your copy, visit our online order page.
|