Save -> Query without Flush inbetween.

73 views
Skip to first unread message

John T

unread,
Apr 10, 2012, 6:49:10 AM4/10/12
to nhusers
Hi guys,

I've hit a problem where we have a case that we are creating an
entity, saving it, then Query<T>()ing for it from cache and modifying
it. I.e. not flushing in between the initial Save() and Query().

However, as I understand from a now quite old article[1] on Ayende's
blog that this is "not possible" unless we explicity use Get()/Load().

Has the situation improved since then, or is the same restriction
still in place?

Many thanks,
John.

[1] http://ayende.com/blog/3988/nhibernate-the-difference-between-get-load-and-querying-by-id

Oskar Berggren

unread,
Apr 10, 2012, 8:31:09 AM4/10/12
to nhu...@googlegroups.com
Not sure if I understand what the question is, but NH is not a query
execution engine - querying will hit the database.

/Oskar

> --
> You received this message because you are subscribed to the Google Groups "nhusers" group.
> To post to this group, send email to nhu...@googlegroups.com.
> To unsubscribe from this group, send email to nhusers+u...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/nhusers?hl=en.
>

John Thornborrow

unread,
Apr 10, 2012, 9:18:21 AM4/10/12
to nhu...@googlegroups.com
Hi, 

The question is simply: Does the restriction as documented in Ayende's article in 2009 still exist today?

I appreciate Querying will hit the DB - but this is a problem introduced by NH and its Cache (because it will not insert immediately when ISession.Save() is called.)

Over-Simplified code to demonstrate point:

public void Foo(ISession session) {
  session.Save(new Client { Id = "123" });
  var client = session.Query<Client>().Single(each => each.Id == "123"); // InvalidOpException - Sequence does not contain any elements

Regards,
John.

Oskar Berggren

unread,
Apr 10, 2012, 9:40:54 AM4/10/12
to nhu...@googlegroups.com
>> > However, as I understand from a now quite old article[1] on Ayende's
>> > blog that this is "not possible" unless we explicity use Get()/Load().

I understand the point of Ayende's article to mainly concern
performance. If you know the primary key, the fastest way is
Get()/Load(), and you should use them unless there is a good reason
not to.

Using a query method will bypass the cache and always execute in the
database. However, using automatic flush mode, NH can automatically
insert the row for the newly saved object when the same table is hit
by the query.

/Oskar

John Thornborrow

unread,
Apr 10, 2012, 10:42:31 AM4/10/12
to nhu...@googlegroups.com
Sorry, I've just realised the article by Ayende doesn't accurately explain (though my sample code does) what the problem is.

A better description is found here, in this StackOverflow thread: http://stackoverflow.com/questions/1286827/nhibernate-save-get-problem

On Tue, Apr 10, 2012 at 3:27 PM, John Thornborrow <thornbor...@gmail.com> wrote:
Hi,

We are using Automatic flush mode, but it is very explicitly and precisely not flushing when we query. The code example I posted in my previous post is factually correct, the second line (i.e. query) will/does throw an exception.

Cannot viably use Get()/Load() because the Query is composed further up the chain, thus the Law of Demeter kicks in and well.. it's pretty ugly for the domain to know what is and isn't a concern of the DB anyway. :)

Though I must ask.. why implement a Cache if it is not going to be Queryable?

Regards,
John.

John Thornborrow

unread,
Apr 10, 2012, 10:27:34 AM4/10/12
to nhu...@googlegroups.com
Hi,

We are using Automatic flush mode, but it is very explicitly and precisely not flushing when we query. The code example I posted in my previous post is factually correct, the second line (i.e. query) will/does throw an exception.

Cannot viably use Get()/Load() because the Query is composed further up the chain, thus the Law of Demeter kicks in and well.. it's pretty ugly for the domain to know what is and isn't a concern of the DB anyway. :)

Though I must ask.. why implement a Cache if it is not going to be Queryable?

Regards,
John.

Catalin Placinta

unread,
Apr 10, 2012, 11:08:43 AM4/10/12
to nhu...@googlegroups.com
Hi John,

I am not sure that I've understood your question; if you want to control yourself the instance when the flush is being done, you only have to modify the FlushMode property of your ISession object, when you create it:

var session = sessionFactory.OpenSession();
session.FlushMode = FlushMode.Never;

If you set the FlushMode to Never, you will have to call it specifically when you want flush the session.

Catalin Placinta

unread,
Apr 10, 2012, 11:23:42 AM4/10/12
to nhu...@googlegroups.com
Hi John,

HQL and ICriteria APIs are not build on the L1 cache, a query built with these APIs will always hit the database/L2 cache.
L1 cache can only be accessed through 2 methods: Get() and Load().

John Thornborrow

unread,
Apr 10, 2012, 11:24:24 AM4/10/12
to nhu...@googlegroups.com
Hi Catalin,

That's one possibility, but it is not the desired approach.

Our scenario is as follows:

We have a Web (WCF) service that receives commands to perform actions on various entities within the domain.

We would like to execute all of those commands in one Transaction. As we are using WCF, we want to use TransactionScope, so we can take advantage of TransactionFlow. We are led to believe that NHibernate integrates with TransactionScope (a.k.a. Ambient Transaction). 

The client application sends a List<> of Commands to the service. There may, or may not be a "Create" command within this list. The other commands will always be a "Modify" command that will modify data on an existing entity.

So sometimes the commands will be a List containing one Create command, followed by a number of Modify commands (all for the same entity). Other times it will just be a list of Modify commands (to modify an existing entity).

In summary, the client application is sending a Create command, with a list of Modify commands to be executed immediately after the entity is created, but if any of the modifications fail for any reason, then everything is rolled back, including the initial Create.

Is that any clearer? :)

Regards,
John.

Catalin Placinta

unread,
Apr 10, 2012, 11:30:18 AM4/10/12
to nhu...@googlegroups.com
Actually there is no need to implement a query API over the L1 cache, as you can do this query yourself:

-------------------------------------

        public IList<T> LoadAll(bool isStateless = false)
        {
            IList<T> result = new List<T>();

            try
            {
                switch (isStateless)
                {
                    case false:

                        ISession session = this.GetSession();

                        BeginTransaction(isStateless);

                        // Check whether there are entities of our type in the cache 1L:
                        // - If not, use ICriteria to query the database.
                        // - If yes, read the identity keys for our entities, and perform a Get<T>() for each of them.

                        if (session.Statistics.EntityKeys.Count > 0)
                        {
                            var identifiers =
                                session.Statistics.EntityKeys.Where(key => key.EntityName == typeof(T).FullName).Select
                                    (key => (int)key.Identifier).ToList();

                            if (identifiers.Count > 0)
                            {
                                result = identifiers.Select(identifier => session.Get<T>(identifier)).ToList();
                            }
                            else
                            {
                                ICriteria criteriaStateful = session.CreateCriteria(typeof(T));

                                result = criteriaStateful.List<T>();
                            }
                        }
                        else
                        {
                            ICriteria criteriaStateful = session.CreateCriteria(typeof(T));

                            result = criteriaStateful.List<T>();
                        }

                        if (autoCommit)
                            CommitTransaction();

                        // Note: there are no changes, therefore we don't session.Flush().

                        break;

                    case true:
                        // Note: A stateless session does not feature a cache 1L.

                        IStatelessSession sessionStateless = this.OpenStatelessSession();

                        BeginTransaction(isStateless);

                        ICriteria criteriaStateless = sessionStateless.CreateCriteria(typeof(T));

                        if (autoCommit)
                            CommitTransaction();

                        result = criteriaStateless.List<T>();

                        break;
                }
            }
            catch (Exception ex)
            {
                throw new DALException(ex);
            }
            finally
            {
                if (autoCloseSession)
                    CloseSession();
            }

            return result;
        }
-----------------------------------------


On Tue, Apr 10, 2012 at 12:49 PM, John T <thornbor...@gmail.com> wrote:

Oskar Berggren

unread,
Apr 10, 2012, 12:15:16 PM4/10/12
to nhu...@googlegroups.com
Den 10 april 2012 16:27 skrev John Thornborrow <thornbor...@gmail.com>:
> Hi,
>
> We are using Automatic flush mode, but it is very explicitly and precisely
> not flushing when we query. The code example I posted in my previous post is
> factually correct, the second line (i.e. query) will/does throw an
> exception.
>
> Cannot viably use Get()/Load() because the Query is composed further up the
> chain, thus the Law of Demeter kicks in and well.. it's pretty ugly for the
> domain to know what is and isn't a concern of the DB anyway. :)
>
> Though I must ask.. why implement a Cache if it is not going to be
> Queryable?


The L1 cache is more like the "working data set" - it contains the
objects currently tracked by the session. This has multiple purposes:
avoiding object duplication, knowing which objects to persist when
flushing changes, and performance. NH relies on the database to
execute queries, returned objects may still come from the L1 cache if
they were already loaded.


Regarding the oversimplified code example you gave, have you executed
that code exactly as written?


/Oskar

John T

unread,
Apr 10, 2012, 1:37:31 PM4/10/12
to nhu...@googlegroups.com
Hi,

Yes, I have executed the code exactly as written - and it does not work. Sequence contains no elements on the Single(). :)

Regards,
John.

On Tuesday, April 10, 2012 5:15:16 PM UTC+1, Oskar Berggren wrote:
On Tuesday, April 10, 2012 5:15:16 PM UTC+1, Oskar Berggren wrote:

Gunnar Liljas

unread,
Apr 10, 2012, 3:12:48 PM4/10/12
to nhu...@googlegroups.com
You may need an explicit transaction (BeginTransaction) for autoflushing to work as expected.

/G

2012/4/10 John T <thornbor...@gmail.com>

--
You received this message because you are subscribed to the Google Groups "nhusers" group.
To view this discussion on the web visit https://groups.google.com/d/msg/nhusers/-/uraL59S5BY8J.

John T

unread,
Apr 10, 2012, 5:04:10 PM4/10/12
to nhu...@googlegroups.com
Is this necessary within a TransactionScope?

Thanks,
J.

On Tuesday, April 10, 2012 8:12:48 PM UTC+1, Gunnar Liljas wrote:
You may need an explicit transaction (BeginTransaction) for autoflushing to work as expected.

/G

2012/4/10 John T 
Hi,

Gunnar Liljas

unread,
Apr 11, 2012, 4:27:29 AM4/11/12
to nhu...@googlegroups.com
Yes, I believe it is.

/G

2012/4/10 John T <thornbor...@gmail.com>
Is this necessary within a TransactionScope?
To view this discussion on the web visit https://groups.google.com/d/msg/nhusers/-/jZYrwqvtmT0J.

John T

unread,
Apr 11, 2012, 5:35:56 AM4/11/12
to nhu...@googlegroups.com
I've just confirmed this in the test I've pasted into bottom of mail.

Without wanting to sound ignorant; Does anyone know what is the reason we must call BeginTransaction() ? I was of the understanding that NHibernate integrates with TransactionScope, so why the need to explicitly invoke an NHibernate transaction?

Regards,
John.


[Test]
public void TestTransaction()
{
   var session = /* fluffy stuff to get a session */;
   session.FlushMode = FlushMode.Auto;

   using (new TransactionScope())
   using (var trans = session.BeginTransaction()) // remove this, and it fails on the Query()
   {
     try
     {
       var marketSegment = session.Query<DataClasses.MarketSegment>().Single(each => each.Code == "BP");
       var id = session.Save(new DataClasses.ClientAccount { AccountCode = "XX123", MarketSegment = marketSegment });

       var clientAccount = session.Get<DataClasses.ClientAccount>(id);
       Assert.That(clientAccount, Is.Not.Null, "Get failed");

       clientAccount = session.Query<DataClasses.ClientAccount>().SingleOrDefault(each => each.AccountCode == "XX123");
       Assert.That(clientAccount, Is.Not.Null, "Query failed");
     }
     finally
     {
       trans.Rollback();
     }
   }
}

On Wednesday, April 11, 2012 9:27:29 AM UTC+1, Gunnar Liljas wrote:
Yes, I believe it is.

/G

2012/4/10 John T 
Is this necessary within a TransactionScope?

John T

unread,
Apr 11, 2012, 7:45:23 AM4/11/12
to nhu...@googlegroups.com
If anyone is still interested, I have discovered that configuring my SessionFactory to use the AdoNetWithDistributedTransactionFactory TransactionStrategy has resolved my issue(s) in this thread.

When explicitly using this strategy I do *not* need to call Session.BeginTransaction() and the earlier test passes. It also commits on TransactionScope.Complete();

I am not yet aware if there is still a connection leak as according to NH-2107 in the issue log, however.

Regards,
John.

John T

unread,
Apr 11, 2012, 7:04:15 PM4/11/12
to nhu...@googlegroups.com
Well much to my fury, after wasting a full day's work after discovering this, it appears it was a false positive. It no longer passes, and I cannot see why. So changing the transaction strategy does not solve the problem(s). :(

Regards,
John.
Reply all
Reply to author
Forward
0 new messages