2009/04/23

Understanding ActiveRecord Sessions 2

This article series deals with the internal session keeping of Castle ActiveRecord. This installment introduces the concept of a scope and how scopes are used within ActiveRecord.

Introducing Scopes

Having a session per database call is suboptimal. There is no chance to use transactions, which is a showstopper for most serious uses. Another issue is lazy loading: Lazy loading prevents NHibernate from loading large collections until they are requested, avoiding to load unnecessary data. Lazy loading is however only possible when done within a single session.

Castle ActiveRecord uses scopes to overcome this. A scope represents a single unit of work. It is not necessarily a single transaction, but may span multiple transactions. Prior to NHibernate 2.0, when transactions were not mandatory, scopes did not even have to support transactions.

The scopes that are part of the ActiveRecord package do always share a single session that is used for all persistance related calls. However, it is possible to implement a scope that uses a new session, but the changed semantic should be clearly communicated in such a case.

Localizing Scopes

In order to access scopes without holding a reference to the object, they stored in thread static stacks. The central interface for this is IThreadScopeInfo, which is implemented by various classes to cover different situations such as web applications. WebThreadScopeInfo for example uses a HttpContext for storing the scopes while the default ThreadScopeInfo implementation uses a thread static field.

In order to acquire the current IThreadScopeInfo implementation, ThreadScopeAccessor can be used. This class is a singleton providing the current ScopeInfo under

public IThreadScopeInfo ScopeInfo

Additionally it acts a proxy to this scope, delegating calls to the scope info in the property above. This allows to access the current scope without keeping a field for it using

ThreadScopeAccessor.Instance.XXX();

where XXX is one of the methods defined in IThreadScopeInfo.

With the IThreadScopeInfo available, the current scope can be requested by SessionFactoryHolder using the following methods:

ISessionScope GetRegisteredScope()
bool HasInitializedScope

Scope Initialization

When a scope is created, it has no sessions stored at first. That is intentional. While the ActiveRecordStarter configures the ISessionFactoryHolder at startup, it cannot configure an arbitrary number of ISessionScope implementations.

Therefore the initialization of scopes is implemented using a simple protocol between ISessionFactoryHolder and ISessionScope:

  1. Upon creation, the ISessionScope registers itself with the current IThreadScopeInfo.

    void IThreadScopeInfo.RegisterScope(ISessionScope scope)
  2. ISessionFactoryHolder fetches the scope the next time it has to deliver a session to the ActiveRecordBase methods.

    bool IThreadScopeInfo.HasInitializedScope
    ISessionScope IThreadScopeInfo.GetRegisteredScope()
  3. The ISessionFactoryHolder asks the ISessionScope whether it already has a suitable session stored. Scopes do not hold only one session. Due to different database connections by root type, they need to hold a dictionary of sessions. The scope doesn't know the key to the sessions because the key is provided by the ISessionFactoryHolder everytime a session is required.

    bool ISessionScope.IsKeyKnown(object key)
  4. If there is a session registered with the scope, the session is requested and the protocol ends.

    ISession ISessionScope.GetSession(object key)
  5. If there is no suitable session available the ISessionFactoryHolder asks the ISessionScope whether it accepts an existing session or if it wants to create its own session.

    bool ISessionScope.WantsToCreateTheSession
  6. Depending on the answer to 5, the ISessionFactoryHolder either opens a session itself or provides a suitable ISessionFactory to the ISessionScope so that the session can be created by the scope itself.

    ISession ISessionScope.OpenSession(ISessionFactory sessionFactory, IInterceptor interceptor)
  7. The session is registered with the ISessionScope by the holder. This is done regardless who in fact created the session.

    void ISessionScope.RegisterSession(object key, ISession session)
  8. The freshly registered session is fetched from the scope.

    ISession ISessionScope.GetSession(object key)

The different Scope Types

Scopes generelly have two attributes that describe there behaviour: by FlushAction and by SessionScopeType. The FlushAction controls whether changes are automatically flushed to the database. The SessionScopeType describes the behaviour of the scope in common, most important whether the scope supports transactions.

If the FlushAction is defined as FlushAction.Never, no writing to the database will occur unless ISessionScope.Flush() is called by the using code. This behaviour gives full control to the using code, but requires it to control flushing. This is important: If the changes are not flushed, queries will still find an older state in the database although the entity has already changed.

FlushAction.Auto instructs the NHibernate session to flush its first-level-cache whenever needed. That means that if the cache contains an unflushed change to an entity, the session will write those changes back before it runs a query against the entity's type.

The SessionScopeType has four values:

SessionScopeType.Undefined
SessionScopeType.Simple
SessionScopeType.Transactional
SessionScopeType.Custom

Undefined should never be used; it is more an error state than a valid scope type. Simple and Transactional define whether the scope supports transactional behaviour. Custom can be used for own implementations that fall in between. An example would be a SessionScope that uses transactions only for specific sessions.

The next part of the series will show how ActiveRecord usage differs when using a scope.

No comments: