News, examples, tips, ideas and plans.
Thoughts around ORM, .NET and SQL databases.

Thursday, March 20, 2014

NonTransactionalReads in DataObjects.Net 5.0

NonTransactionalReads is a specific Session option that allows to read once fetched entities right from Session cache without the requirement to have an open transaction. This mode might be useful for desktop application that have only one Session instance for the entire lifetime of application or for services like auditing, logging, etc. that need to read persistent fields without having any idea of sessions, transactions, etc.

To demonstrate the approach, let's start with a sample. Here is the persistent class with one lazy loading field:
[HierarchyRoot]
public class Country : Entity
{
    [Field, Key]
    public int Id { get; private set; }

    [Field(Length = 30)]
    public string Name { get; set; }

    [Field(Length = 3)]
    public string Code { get; set; }

    [Field(LazyLoad = true)]
    public string Description { get; set; }

    public Country(Session session)
        : base(session) {}
}
Say, we want to have a cache of countries in our application. We create some countries on Domain start, e.g.:

var domainConfiguration = DomainConfiguration.Load("Default");
var domain = Domain.Build(domainConfiguration);

using (var session = domain.OpenSession())
using (var tx = session.OpenTransaction()) {
    new Country(session) {
        Code = "AND",
        Name = "Andorra",
        Description = "A tiny country in the middle of Europe"
    };
    new Country(session) {
        Code = "ATA",
        Name = "Antarctica",
        Description = "Icy paradise for penguins"
    };
    // other countries...
    tx.Complete();
}

To implement some sort of cache in earlier version of DataObjects.Net you would load countries in every transaction or avoid frequent loading of persistent objects from database by converting them into some kind of POCO and cache them.

In DataObjects.Net 5.0 you can do this in a more comfortable way using the new NonTransactionalReads flag.
// Configuring session
var options = new SessionConfiguration(SessionOptions.Default | SessionOptions.NonTransactionalReads);

var session = domain.OpenSession(options);
Dictionary<string, Country> cache;

// Loading cache with data in one transaction
using (var tx = session.OpenTransaction()) {
    cache = session.Query.All<Country>().ToDictionary(i => i.Code);
    tx.Complete();
}

// accessing data from another transaction
using (var tx2 = session.OpenTransaction()) {

    // cached objects are not being reloaded from database. they are consumed as is
    var antarctica = cache["ATA"];
    Console.WriteLine(antarctica.Name);

    // accessing a lazy loading field. session loads it on demand
    Console.WriteLine(antarctica.Description);

    tx2.Complete();
}

// disposing session. Data is not accessible anymore
session.Dispose();

What is more interesting, the same behavior can be achieved without using any transactions at all. Let's re-write the sample:
// Configuring session
var options = new SessionConfiguration(SessionOptions.Default | SessionOptions.NonTransactionalReads);

var session = domain.OpenSession(options);
Dictionary<string, Country> cache;

// Loading cache with data without any transaction
cache = session.Query.All<Country>().ToDictionary(i => i.Code);

// accessing data 
var antarctica = cache["ATA"];
Console.WriteLine(antarctica.Name);

// accessing a lazy loading field. session loads it on demand
Console.WriteLine(antarctica.Description);

// disposing session. Data is not accessible anymore
session.Dispose();

So, the key points of the NonTransactionalReads mode:
  1. No matter how data is loaded from database (with the help of transaction or not), it is accessible from outside the boundaries of the transaction, if any.
  2. Data is cached on Session level. As long as the Session is alive (not disposed), the data will be available from its cache.
  3. In case of accessing LazyLoad fields, Entity references and EntitySets, data is automatically fetched from database on demand.

7 comments:

  1. What happens if you reload the Country in the same session and change the Country instance? Is the cached instances also changed?
    Caching at domain level is the next step?

    ReplyDelete
  2. Hello Marco,

    entities are always cached in session (at least via weak references). If you try to access entity that is still available in session cache its state will be reused and you'll get old field values. If you try to fetch entity via LINQ query for example cached state will be refreshed with newly loaded data.

    New option does not change how DO caches entities within session it changes the way how DO handles entity state expiration. Without NonTransactionalReads option particular entity is considered expired when corresponding transaction completes. With NonTransactionalReads option entity is considered expired only when corresponding transaction is rolled back. This gives lesser number of fetches from the database at the expense of relaxed guarantees about data validity.

    Regarding your second question, we have plans to implement some kind of domain-level caching in 5.0, but actual technical design of this feature is not defined yet.

    ReplyDelete
  3. So you could use this mode in a web app for GET HttpRequest to minimize the reads.. Correct?
    Also in combination with the Xtensive.Orm.Web extension? (but how to handle different sessions for GET and Other HttpRequest?)

    ReplyDelete
  4. This option would not affect Web extension much.
    SessionManager creates one transaction per request thus there is little need for using techniques described in this post.

    ReplyDelete
  5. Any update on the caching part of DO 5.0?

    ReplyDelete
  6. Can you explain what is the difference between SessionOptions.NonTransactionalReads and SessionOptions.AutoTransactionOpenMode with regards to reads only?

    ReplyDelete