NHiberate session.Get() gets the cached entity. How to get the latest entity from the DB?

195 Views Asked by At

I'm using the CQRS pattern to get the data from the DB using NHibernate.

Here is the CommittingTransactionCommandHandler which calls the UpdateProductHandler

class CommittingTransactionCommandHandler
{
    private readonly ISession session;
    private readonly ICommandHandler<TCommand> _inner; //Generic Command Handlers

    public CommittingTransactionCommandHandler(ISession session)
    {
        this.session = session;
    }

    public async Task Execute(TCommand command)
    {
        using (var txn = session.BeginTransaction(IsolationLevel.Unspecified))
        {
            try
            {
                await _inner.Update(command); // This calls the UpdateProducthandler's Update method
                txn.Commit();
            }
            catch (Exception ex)
            {                   
                throw;
            }
        }
    }
}

Here is the Command Handler for the Update.

class UpdateProductHandler : ICommand
{
    private readonly ISession session;
    public UpdateProductHandler(ISession session)
    {
        this.session = session;
    }

    public async Task Update(int id, ProductIdentity productIdentity)
    {
        var product = session.Get(id);
        product.Update(productIdentity);
    }
}

Here is the Query Handler for the Get

class GetProductHandler
{
    private readonly ISession session;
    public GetProductHandler(ISession session)
    {
        this.session = session;
    }

    public async Task<Product> Get(int id)
    {
        var product = session.Get(id);
        if (product == null)
            throw new Exception("Entity not found");
        return Task.FromResult(product);
    }
}

Here is the code for the Product entity

class Product
{
    public virtual int Id { get; protected set; }
    public virtual string Name { get; protected set; }
    public virtual string Description { get; protected set; }
    public virtual int? Version { get; protected set; }

    public virtual void Update(ProductIdentity productIdentity)
    {
        Name = productIdentity.Name;
        Description = productIdentity.Description;
    }
}

The flow is

CommittingTransactionCommandHandler is a generic command handler. This is called from the API, which internally invokes the UpdateProductHandler. The transaction is opened in this and committed here.

The scenario is that

  1. I get a Product from the DB using the GetProductHandler. (In this case, the version number of the Product is 10.)

  2. I'm updating the Product using the UpdateProductHandler and commits the session which is under the transaction. (Here version number of the Product is 11)

  3. Immediately after the Update Product, I query the same Product using the GetProductHandler and loads it in the Edit mode in the UI. (But the Product fetched using the GetProductHandler has a version number 10 and not 11.)

  4. Here is the issue, instead of getting the latest update from the DB, the above GetProductHandler, gets the previous state of the object.(Found using the version number)

  5. Now, if I try to update the Product, I get a Stale Object State Exception since the version number is 10 which is not the latest version of the Product.

I've tried with session.Refresh(product) but all in vain as it affects the other transactions.

How can I resolve this?

1

There are 1 best solutions below

0
On

As Ayende explains, Get will NOT always hit the database. It will only hit the database if entity not found in first level cache.

You have not explained how you are updating the entity; what is inside product.Update method. If you are updating using API that does not update underlying first level cache (SQL/HQL/Linq), then the problem you stated in question is obvious. In that case,

  • You load entity in session cache using Get (Version 10). This hits the database.
  • Then, you call Update (database hit) which will not update the first level cache (Version changed to 11 in database; NOT in cache).
  • Then you call Get again. NHibernate looks if entity is already loaded in session cache. Yes; it is (Version 10). It just returns it. No database hit.

Solution 1:

Not sure why that Update method is needed. You can directly update the entity like below:

public async Task Update(int id, ProductIdentity productIdentity)
{
    var product = session.Get(id);
    product.Version = 11;
    //Update other properties here
    ...
    ...
    //That's it. You are already calling `Flush` somewhere somehow
}

Solution 2:

As Ayende NOT recommended (and I am not recommending either), don't use Get and write a query that will hit the database. Use Linq/HQL/SQL API instead.

select customer from s.Linq<Customer>()
where customer.Id = customerId
select customer
).FirstOrDefault();

Every time that I see something like that, I wince a little inside. The reason for that is quite simple. This is doing a query by primary key. The key word here is a query.

This means that we have to hit the database in order to get a result for this query. Unless you are using the query cache (which by default you won’t), this force a query on the database, bypassing both the first level identity map and the second level cache.

Get and Load are here for a reason, they provide a way to get an entity by primary key. That is important for several aspects, most importantly, it means that NHibernate can apply quite a few optimizations for this process.