Simple App: Think of something you can generate by asking Grails to do the coding for you. Simple app is a CRUD application with these characteristics:
- Simple domain Objects without relationships
- No transactions/services
- Not more than one hibernate query per request
- Simple validation logic (contained in domain objects)
class User {
...
Office office
UserPreferences preferences
}
In a simple app this code:
User.findAllByOffice(office1) //Code Example (A)
will exhibit only one type of side-effects: it issues SELECT statement + some DB locks.
Complex App: In my last post I have listed several side effects that can be associated with (A). Clearly, complex apps are a different ball game! Here are the side effects listed again. Code (A)
- will issue a SELECT statement (with whatever locks)
- can save some changed objects to the database (part 5)
- can save some unchanged objects too (part 6)
- will impact some record types returned by queries that follow it (from proxies to actual objects). Some records returned from this query will use Hibernate Proxy to implement preferences and some will use the actual UserPreferences class. (Some developers may be surprised that the same goes for the office association). (part 4)
- will impact the data content of some records returned by queries that follow it. Similarly, the content of some records returned from this query may be different from the data returned by the underlying SELECT statement. (part 2)
Counting the number of side-effects in (A) that impact my code is just one possible measure of my app complexity. As my application becomes more complex, these side-effects become 'louder' (impacts become more frequent and more noticeable). In this post, I am less interested in how many side-effects impact my code, more in how 'loud' they are.
Following table shows a fictional CRUD application that started very simple and became more complex over time.
H5 - total number of side-effects in queries similar to (A)
H6 - frequency of problems related to queries similar to (A)
Complexity
|
Exposure to Problems
|
Explanation of impacts
|
H5
|
H6
| |
1
|
Simple App
|
Happy Days!
|
Happy Days!
|
1
| |
2
|
Add Domain Object Relationships
|
Hibernate Proxy Objects
|
2
|
low
| |
3
|
Add more complex validation - more than one query in the same session
|
Repeatable Finder
|
3
|
low
| |
4
|
Move validation logic outside of domain objects
|
Auto-flushing
|
4
|
low -mid
| |
5
|
Add Services and Transactions
|
LazyInitializationException
Auto-flashing becomes less loud (unwanted saves are rolled back)
Transactional integration tests diverge from reality
| Auto-flushing can still be a problem: saving can fail. |
mid
| |
6
|
More complex data passed from client
|
Unmarshalling of client data keeps changing between Grails versions
|
Small intro to version upgrade problems
|
mid
| |
7
|
Added logic in Filters
|
Higher probability of repeatable finder issue.
Need for more complex test infrastructure
Side Note: What is the before-after-afterView ordering if Controller does a forward? |
Potentially interesting filter cleanup or setup ordering issues
|
mid
| |
8
|
Application managed hibernate sessions
|
DuplicateKeyException and similar hard to troubleshoot errors.
Queries can save unmodified objects.
|
5
|
high
|
8 needs more explanation. Here is one example why I need to create small hibernate sessions:
Integration tests may need to include scenarios mimicking activity performed over several HTTP requests. Typically, I see such tests mashing all logic into one test method executing everything in the scope of the same hibernate session. In real life, each of the requests will be performed in a separate hibernate session. Tests have diverged from reality. Side-effects 2-5 listed above will have dramatically different impact on the code when ran in the test environment.
Audacious App:
Consider Grails/Hibernate implementing Type 2 Slowly Changing Dimension (the one using time slices). To do that, I may want to use something like a session variable (available in most databases, including Oracle or Postgresql) to define a time point and use read-only views to get a snapshot of all my data at the specified time point. I will configure my GORM domain objects against these views and see what happens.
To use session variables, I will need to wrestle with Spring framework DB connection management to make sure that the connection is not swapped under me, because that would change the definition of session variable. That is not trivial but doable.
How does that change my exposure to Hibernate side-effects? Consider this: moving the time-point one day forward applies a day worth of user activity in just a few milliseconds! Many concurrency problems in the application just became super loud. That includes repeatable finder (side-effect 5, my blog part 2). Because of that I need to wrap each use of a time-point session variable with a withNewSession() block (or something equivalent). That exposes my code to issues documented in part 3 and 6 (side-effect number 3!). All of these are now super loud.
Conclusions:
This is my best attempt to explain the discrepancy in perception of Hibernate and Grails. I think there is more going on that I do not understand, but this is my best answer to-date.
The 5 side-effects listed in this post are worth investigating more. I will refer to them again when talking about testing (next post).
Looking forward to the upcoming posts, this series has been entertaining and thorough so far :)
ReplyDeletevery-very interesting topic - wishing to have more traffic here. My experience: we are at version n++ of a large Grails application (~300 tables) and we hit the wall constantly with Hibernate/Grails. We have an unconditional love for Grails - but for us the only way to get things running is to use groovy.Sql. Will contribute here with some examples asap. Julio.
ReplyDeleteI feel very grateful that I read this. It is very helpful and very informative and I really learned a lot from it.
ReplyDelete