In my current project a simple question came up. “Is Hibernate’s first-level cache limited to a certain transaction?” Intuitively my answer was: “No, the first-level cache is also called session cache. So it should rather be bound to a Hibernate session. And since the same session can handle multiple transactions during its lifetime, entities cached by one transaction would be accessible by another transaction within this context.” Ok, that is the theory. A day later, I thought again about this small water-cooler chat. I used words like “should” and “were” to answer a very concrete, technical question. That left a bitter taste. But how could I prove that entities are cached inter-transactional, thus confirming my “premature” assumption?
We don’t have a choice, let’s try it out!
Since “keeping things simple!” is a valuable goal, we will create a small plain Hibernate project. It ought to contain something like a test case which tries to access the same entity within two different transactions trying not to talk to the database more than once. In other words, our approach to validating our theory is to count how often separate transactions within the same session have to execute SELECT
queries to work with a single entity. If our assumption was right and transactions can share entities in a session-wide cache, only one of these transactions had to read an entity from the database and the other transaction will have access to this entity through the cache without reading it from the database again.
Sounds “simple”, but how could we observe our ORM’s database access without much effort? Do we have to parse database logs or to write some smart interceptors? Fortunately, someone already did that for us. There is ttddyy’s DataSourceProxy project and this tiny library wraps your data source and allows you to collect some useful metrics regarding your ORM behavior. Leveraging such a DataSourceProxy, we can verify every database access on Java level. That makes it very easy to write a JUnit test.
What do we need?
To create a minimum viable test project, we only need a handful of dependencies and a database. The most important dependency is the ttddyy proxy.
1<dependencies> 2 ... 3 <groupId>net.ttddyy</groupId> 4 <artifactId>datasource-proxy</artifactId> 5 ... 6</dependencies>
Our only entity contains just an identifier and a creation date since we don’t need huge data for use case.
1@Entity
2public class SomeEntity {
3
4 ...
5
6 @Id
7 private Integer id;
8
9 private Date createdDate;
10
11 ...
12}
The datasource configuration is a crucial part. Here we have to wrap our real datasource with a DataSourceProxy.
1private static DataSource buildProxyDataSource() {
2 return ProxyDataSourceBuilder.create(buildDataSource())
3 .name("ProxyDataSource")
4 .countQuery()
5 .build();
6}
Well done. Now, what does our test flow look like?
Our test creates an entity (Transaction A). After that, we will immediately clear the first-level cache to force at least one database read on the first entity access (Transaction B). If we did not clear the cache, it would contain the entity right upon the entities’ creation time and we would not have to execute a single SELECT
query in our entire test.
1... session.beginTransaction(); 2... 3createEntity(session, entityId); 4 5transactionA.commit(); 6 7 8... session.beginTransaction(); 9 10// clear cache after entity creation, otherwise we would have no select at all 11session.clear(); 12 13// intended only select 14... readEntityCreationDate(session, entityId); 15 16transactionB.commit(); 17 18 19... session.beginTransaction(); 20 21// another read, but no further select expected although we opened a different transaction context 22... readEntityCreationDate(session, entityId); 23 24transactionC.commit();
Since we now start with an empty session and our test is loading the entity explicitly, one SELECT
query is intended. This operation also puts the entity right back into the first-level cache (session cache). After committing (Transaction B), another Transaction (Transaction C) is accessing the entity again by its identifier. This call should be answered by the first-level cache, so we expect no further SELECT
query although we are in another transaction context.
Drum roll … The results:
We verify our assumption by counting the sum of executed queries separated by type. The QueryCountHolder offers very convenient methods to do that.
1final QueryCount grandTotal = QueryCountHolder.getGrandTotal(); 2assertThat(grandTotal.getInsert()).isEqualTo(1); // (Transaction A) Inserts our entity 3assertThat(grandTotal.getSelect()).isEqualTo(1); // (Transaction B) Only one transaction reads the table
We see that there is only one database INSERT
to create our entity and one SELECT
to read it again.
The full example test project is available on GitHub.
Summary
Finally I’m able to replace “it should be bound to a Hibernate session” with “it is to be bound to a Hibernate session”. And finally I can sleep peacefully again. 🙂 Joking aside, although that simple example does not even begin to exhaust ttddyy’s DataSourceProxy projects capabilities, it shows how useful this library can be for purposes like ours. It will prevent you from making rash decisions due to assumptions we made in lack of proof. Next time you are in doubt regarding your ORM’s behavior, don’t ASS-U-ME! Maybe a DataSourceProxy could help take a look behind the curtain.
P.S.: If you need more tests, don’t forget
1QueryCountHolder.clear();
after each 😉
More articles
fromKevin Peters
Your job at codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
More articles in this subject area
Discover exciting further topics and let the codecentric world inspire you.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog author
Kevin Peters
Senior IT Software Engineer / Consultant
Do you still have questions? Just send me a message.
Do you still have questions? Just send me a message.