Hibernate Object States/LifeCycle
Let's start off with the basics. Now imagine you have a JPA annotated User class, and you create an instance of it using the following code:
AdminUser u = new AdminUser ();
Will Hibernate automatically persist this object, given just this line of code alone? Of course not! When you create an object in your Java code, even if your application does use the Hibernate framework for persistence, the newly created instance will exist only in the memory of the Java program that created it, and Hibernate has nothing to do with it. In fact, there's a special term we use to describe objects in Java programs that have been instantiated, but have not been placed under the spell of the Hibernate Session; such instances are referred to as transient instances, as their state is never persisted to the database.
From Transient to Persistent
tran-sient [tran-shuhnt, -zhuhnt, -zee-uhnt] adjective 1. not lasting, enduring, or permanent; transitory. 2. lasting only a short time; existing briefly; temporary: transient authority. 3. staying only a short time
transient instance: a transient instance is one that is instantiated within a Java program, but is not managed by, or connected in any way to a Hibernate Session. Furthermore, transient objects do not have any existing representation in a database.
Now, if you create an instance of a class in your Java code, and you want Hibernate to manage the persistent state of that object, you have to associate you're newly created instance with the magical Hibernate Session. There are a number of ways to do this, with the easiest probably being to simply pass the Java instance to the saveOrUpdate method of the Hibernate Session.
AdminUser u = new AdminUser (); // transient at this point
session.saveOrUpdate(u); // no longer transient
Once an instance of a JavaBean has been associated with the Hibernate Session, it is no longer a transient instance, but instead, becomes what we call a persistent object, and the persistent state of the instance is then managed by the Hibernate Session for the duration of the transaction.
Now, that previous sentence was quite a mouthful, especially the part about the duration of the transaction. You see, whenever you want to perform any of the basic CRUD operations, you must first initiate a transaction, and when you are done, you must close the transaction. Don't let anyone tell you differently; that's the way database operations work.
The Transactional Context
As I said, any time you want Hibernate to manage the persistence of your JavaBeans, you must first initiate a transaction, which is just a simple method call on the Hibernate Session.
hibernateSession.beginTransaction();
Once you being a transaction, you can start associating your POJOs and JavaBeans with the Hibernate Session. If you don't initiate a transaction, you'll get a runtime exception telling you about no transactional context being in existence, and at all costs, we want to avoid runtime exceptions.
Now, the really cool thing about Hibernate is the fact that once a transaction has been started, all you have to do is associate an instance with the Session, and then Hibernate will take care of fully managing the persistent state of that instance, right up to and including the point where you commit the transaction. When you finally commit the transaction, the state of all of the POJO's that have been associated with the Hibernate Session will be saved to the database.
The following snippet of code demonstrates the art of beginning a transaction, creating an instance, associating that instance with the Hibernate Session, and then finally, committing the transaction:
hibernateSession.beginTransaction();
AdminUser user = new AdminUser ();
hibernateSession.saveOrUpdate(user);
hibernateSession.getTransaction().commit();
In this code snippet, a new transaction is created, after which a brand new, unassociated, transient instance of a User is created. This instance is then associated with the Hibernate Session when it is passed as an argument to the saveOrUpdate() method of the session, transitioning the instance from being a transient instance, to a persistent instance.
Affection for the saveOrUpdate Method
Now, I don't actually like the saveOrUpdate method. I mean, I love using it, but I don't really like the name, because I think the name is a little bit misleading as it implies that an instance that is passed to the method will be saved, or updated, immediately. You see, that's only partly true. Sure, when the saveOrUpdate method is invoked, the instance will eventually have its state persisted to the database, but more happens that just that. The instance in not only queued up for a database save, but the Hibernate Session keeps track of that instance, and if any further changes happen to the instance before the transaction is committed, then those changes to the instance's state will be persisted as well. I'd almost prefer it if the saveOrUpdate method was renamed to something more descriptive like saveOrUpdateAndAssociateInstanceWithSession, but then again, I guess there is an upper limit on how long good method names should be.
Take a look at the following code snippet:
Session session=HibernateUtil.beginTransaction();
User user = new User();
user.setPassword("abc123");
session.save(user);
user.setLoginName("mj");
user.setPassword("abc123");
user.setEncryptedPassword("zab012");
user.setEmailAddress("mj@scja.com");
user.setLastAccessTime(new java.util.Date());
user.setRegistrationDate(
new java.util.GregorianCalendar());
user.setVerified(Boolean.FALSE);
HibernateUtil.commitTransaction();
Notice that in this code snippet, an instance of the user class is created, the password is set to abc123, and then the save method of the Hibernate Session is passed the instance. The call to the save method triggers the following SQL statement to be executed against the database:
insert into examscam.user
(emailAddress, lastAccessTime, login_name, password, registrationDate, verified)
values (?, ?, ?, ?, ?, ?)
So, when the save method is invoked on the Hibernate Session, the state of the instance, which is really nothing more than a primary key and a password in this example, is persisted to the database. But look at the original code snippet, and see what happens after the save method is invoked. A bunch of other properties of the User instance are modified, and then the transaction is committed. The question is, will these changes to the state of the User instance be persisted to the database, or will the database only contain the primary key and the password once the transaction is committed? Well, in fact, all of the changes to the instance that took place even after the save method was invoked on the Hibernate Session will be persisted to the database. As the transaction is committed, Hibernate will issue the following SQL statement against the database:
update examscam.user set emailAddress=?, lastAccessTime=?, login_name=?, password=?, registrationDate=?, verified=? where id=?
You see, that's the great thing about Hibernate: as soon as an instance is associated with the Session, be it through a load, get, update or save invocation, the state of that instance will be managed by Hibernate right up to the point in which the current transaction is committed. Hibernate will keep track of any changes that happen to the instance throughout the course of the transaction, and update the corresponding record in the database accordingly.
Proper Hibernate Coding Practices
I often see developers that are new to Hibernate constantly calling the saveOrUpdate method whenever a set of changes have been made to a POJO. This isn't necessary. You only have to associate an instance with the Hibernate Session once within the scope of a transaction. From that point on, you can do whatever you want to your JavaBean instances. Hibernate will persist the final state of your instance when the current transaction is finally committed.
The following piece of code needlessly calls the saveOrUpdate method after instance variables have been updated. This is totally unnecessary, as the User instance was already associated with the Hibernate Session through the original call to saveOrUpdate.
hibernateSession.getTransaction();
User user = new User();
hibernateSession.saveOrUpdate(user);
user.setLoginName("mj");
user.setPassword("abc123");
hibernateSession.saveOrUpdate(user); /*This is BAD!*/
hibernateSession.getTransaction().commit();
With the first call to saveOrUpdate, the instance named u becomes associated with the Hibernate session. From that point on, you can mess around with the user instance as much as you want, and Hibernate will take care of the persistence. You can initialize, update, change, and modify any instance variable of the user instance that you want, and Hibernate will save the final state of the instance once the transaction has been committed..
hibernateSession.getTransaction();
User user = new User();
hibernateSession.saveOrUpdate(user);
user.setLoginName("Manish Jaiswal");
user.setPassword("abc123");
hibernateSession.getTransaction().commit();
Loading Instances and the Hibernate Session
Of course, the saveOrUpdate method, along with the save method, is great for associating brand new instances with the Hibernate Session, but more often than not, you'll want to pull a previously persisted instance from the database into your Java program, perhaps so you can update the instance and subsequently persist that new information to the database. For pulling existing instances out of the database, while at the same time, ensuring they are associated with the Hibernate Session for persistence management, we use either the load or the get method of the Hibernate Session.
Note that the load method is intended to be used when you know an instance actually exists in the database. This method actually returns proxy objects that alleviate database hits until transaction commit time, making it a little more efficient. The get method is better used when you don't know for sure if the instance you are loading, or getting, actually exists in the database.
Calling the load or get method on the Hibernate Session in order to obtain a persistent object from the database not only provides you access to the instance of interest, but it also associates that instance with the Hibernate Session. Take a look at the following code, which pulls an instance of a User out of the Hibernate Session.
Session hibernateSession = this.getCurrentSession();
hibernateSession.beginTransaction();
User u = (User)hibernateSession.get(User.class, 2);
u.setLoginName("manish");
u.setPassword("password");
hibernateSession.getTransaction().commit();
Notice that after updating the properties of the instance, namely the loginName and password, that we simply ask the Hibernate Session to commit the transaction, which will in turn, update the database. There is no need to call the saveOrUpdate method after changing the attributes of the instance, because the instance is already associated with the Hibernate Session, and as a result, any changes to the state of the persistent instance will be updated in the database.
Rushing the Update with a flush()
As you know, the Hibernate Session will be keeping track of all of the updates that happen to instances that are associated with it, and as you could imagine, the list of updates that may need to be committed might end up getting quite large. Using the default configuration of Hibernate, you can never be totally sure when the updates will be committed to the database; all you can be sure of is that once the transaction has been committed, the updates have happened. However, if you have some compelling reason to rush the updates, and have them sent immediately to the database, you can call the flush() method on the Hibernate Session.
FlushMode
You can override the default flushing behavior of the Hibernate Session if you so desire. To have more control over how and when Hibernate will flush the changes that the Session is marinating, you can set the Session's FlushMode. There are five flush modes, although one has been deprecated. They are:
- AUTO - the Session is typically flushed before query execution to ensure query results do not contain stale data
- ALWAYS - the Session is flushed before every query
- COMMIT - the Session is flushed when the transaction.commit() method is called
- MANUAL - the Session is only flushed when the flush() method is invoked on the Session
There is also a FlushMode of NEVER, although this has been deprecated, and the use of the MANUAL setting is promoted instead.
The opposite of flush()? refresh()?
So, if flush() takes the state of all of the instances in the Hibernate Session and forces a database update, I guess the Session's refresh() method could be considered the flush() method's inverse, as the refresh method is conversely passed a persistent instance, and Hibernate is asked to go to the database and update the already associated instance's properties with the data stored in the database.
For the most part, you won't likely run into too many circumstances where a persistent instance has fallen out of favor with the Hibernate Session, but if you believe such a thing has happened, perhaps due to a set of batch updates that may have been kicked off, or even due to some direct JDBC calls that may have sidestepped the Hibernate Session, calling the refresh method just might be required.
Here's the Hibernate API JavaDoc on the Session's refresh() method. Note the warning they give about the appropriate use of the method:
public void refresh(Object object) throws HibernateException
Parameters: object -a persistent or detached instance
Re-read the state of the given instance from the underlying database. It is inadvisable to use this to implement long-running sessions that span many business tasks.
This method is, however, useful in certain special circumstances:
-- where a database trigger alters the object state upon insert or update
-- after executing direct SQL (eg. a mass update) in the same session
-- after inserting a Blob or Clob
Terminology Review: Transient and Persistent
Transient instances are JavaBean instances that exist in your application, but do not have a representation in the database, and are not associated with a Hibernate Session.
A persistent instance is one that not only exists in your application code, but is also associated with a Hibernate Session within the scope of an active transaction, so that when the transaction is committed, the state of that instance will be persisted to the database.
Detached Instances
Okay, so we have a great appreciation for the fact that as soon as an instance has been associated with a Hibernate Session, Hibernate will take responsibility for the persistent state of that object until the current transaction is committed. But what happens after the transaction is committed? For example, take a look at the following code:
Session session=HibernateUtil.beginTransaction();
User user = new User();
user.setLoginName("manishjaiswal");
user.setPassword("111111111");
session.save(user);
user.setPassword("22222222");
HibernateUtil.commitTransaction();
user.setPassword("33333333")
So, if you peaked into the database after running this code snippet, what would the value of the password be for the associated database record? Would it be 111111111, 222222222 333333333? The answer is 2222222, since the instance is first persisted to the database with the value 11111111, then, as the transaction is committed, the password is updated to 2222222. But when the final Java based update to the password field is done, there is no open transaction, and Hibernate has no context with which it can update the user's password to cccccc.
After the transaction has been committed, the User instance is said to be a detached instance, because the instance is no longer associated with a Hibernate Session, and no mechanism exists to tie the state of the instance to the database.
Programmatically Detaching Instances
Sometimes you may have an instance whose persistent state is being managed by the Hibernate Session, but then, for some reason, you want to shake that instance free of Hibernate's grasp. If you can't wait for a transaction to commit and have the instance naturally become detached, you can explicitly detach an instance from the Hibernate Session by simply calling the evict method of the Session, just as I do in this following snippet of code:
Session session=HibernateUtil.beginTransaction();
User user = new User();
user.setLoginName("manish jaiswal");
user.setPassword("aaaaaa");
session.save(user);
session.evict(user);
user.setPassword("bbbbbb");
HibernateUtil.commitTransaction();
So, after the transaction is committed in this snippet of code, what value would the password column for the user's corresponding database record be? Would it be null, aaaaaa or bbbbbb? Well, the correct answer is aaaaaa.
You see, the instance has its password initialized to aaaaaa, after which it is passed to the save method of the Hibernate Session, and the Hibernate Session does just that - it saves the state of the instance. However, we programmatically evict the instance from the Session, at which point, Hibernate wipes its hands clean of any further changes to the User instance. The instance indeed has a representation in the database, but the evict method has detached the instance from its underlying representation, so further changes to the state of the User instance, such as the changing of the password to bbbbbb, are no longer the concern of the Hibernate Session, and such changes are not persisted to the database.
Object Comparisons
Once you start mixing and matching persistent and detached objects within your code, which pretty much any J2EE application will do at some point in time, you will find some not-so-funny, and potentially non-intuitive, problems that come up when you start doing comparisons of instances that you would think would be the same.
For example, take a look at the following code that creates two instances, user1 and user2, based on the same, identical, database record. What do you think the output of the code snippet would be?
Session session=HibernateUtil.beginTransaction();
User user1 = new User();
user1.setPassword("aaaaaa");
Long id = (Long)session.save(user1);
session.evict(user1);
User user2 = (User)session.get(User.class, id);
System.out.print("The instances are the same: ");
System.out.println( user1.equals(user2));
HibernateUtil.commitTransaction();
Since both instances of the User class are based on the same database record, they will have all of their properties set to the same values, which means the two objects are essentially the same, but the comparison of the two objects returns false. It's somewhat non-intuitive, but if you know what's going on under the covers of the Java Virtual Machine, it actually makes sense.
By default, when you compare two instances using .equals(), the compiler simply compares the memory locations of the two instances, as opposed to comparing their actual property values. Since we have two separate instances, we end up having two separate memory locations, and a .equals() comparison returns false. To overcome such situations, a Hibernate best practice is to have all of your JPA annotated classes properly override the inherited .hashcode() and .equals() methods, providing an implementation that makes sense for the class. That way, when two instances with exactly the same state are compared, the actual properties the object contains will be compared, and the compiler will not simply look at the memory locations of objects when performing an equality comparison.
The org.hibernate.NonUniqueObjectException
So, as we have seen, the following code snippet creates two instances, user1 and user2, both of which share the same set of properties, but with the main difference being the fact that user1 becomes a detached object after the evict(user1); method is called, whereas the instance user2 is a persistent object right up until the point that the transaction gets committed. Here's the code:
Session session=HibernateUtil.beginTransaction();
User user1 = new User();
user1.setPassword("aaaaaa");
Long id = (Long)session.save(user1);
session.evict(user1);
User user2 = (User)session.get(User.class, id);
HibernateUtil.commitTransaction();
Now, what do you think would happen if you changed some values in the detached instance, user1, and then tried to use the Hibernate Session to update that instance, considering the fact that user2, an instance that shares its id with user1, is already associated with the Hibernate Session? What would happen if you tried to run the following code:
Session session=HibernateUtil.beginTransaction();
User user1 = new User();
user1.setPassword("aaaaaa");
Long id = (Long)session.save(user1);
session.evict(user1);
User user2 = (User)session.get(User.class, id);
user1.setVerified(true);
session.saveOrUpdate(user1);
HibernateUtil.commitTransaction();
Well, here's another rule that Hibernate strictly enforces: only one instance of a class with a given unique primary key can be associated with the Session at a time. In this case, if we try to re-associate the evicted user1 with the Hibernate Session, Hibernate will kick out to us a NonUniqueObjectException, indicating that it is already managing an instance that represents that particular database record. So, Hibernate will gladly manage your unique instances, but fundamentally, it is that primary key that makes an object unique, and the developer must be careful not to add a second, non-unique instance of a class to an active Hibernate Session.
A Little Bit About Transactions
One of the things that you should know about a transaction is that it is an all or nothing type of thing. When you start a transaction, you can associate as many instances with your Hibernate Session as you like. Furthermore, you can perform an unlimited number of loads, gets, saves, updates, evictions or refreshes while a transaction is in progress. Hibernate, and the Hibernate Session, will keep track of all of your various method calls and state changes, and finally, when the transaction is committed, Hibernate will persist all of your changes to the database.
However, sometimes, when you are committing a transaction, something can go wrong. There are a multitude of reasons why a database write might fail, be it database deadlocking, or simply a connection timeout. But regardless of why a transaction fails, if a transaction does fail, all of the changes or updates that have taken place within that transaction are completely rolled back, taking the database back to the state it was in before the transaction was even started. Furthermore, if you have any instances in scope while the transaction failed, those instances will become detached instances, because the transaction and the Hibernate Session with which they were associated are junked, and no longer capable of persisting the state of your data. Furthermore, while data in the database will be rolled back to the state it was in before the transaction was committed, any JavaBeans that are still in scope will not have their internal state rolled back, making them out of sync with the database if they did have their state updated during the course of the failed transaction.
Any time we commit a transaction, the possibility of an exception being thrown looms large. As a result, if an exception does occur during the committing of a transaction, it is a best practice to catch the exception, explicitly rollback the transaction, and finally, close the session. Furthermore, since all of the POJO instances have become detached from the database, and are no longer under the Hibernate Session's spell, those instances should be allowed to simply go out of scope and be garbage collected, as they are no longer in sync with the database.
Finally, you can either re-throw the HibernateException, or potentially, throw a custom application exception that will be appropriately handled by an upper application layer, providing an appropriately formatted error message to the end user.
try {
Session hibernateSession = this.getCurrentSession();
hibernateSession.beginTransaction();
User u = (User)hibernateSession.get(User.class, 2);
hibernateSession.evict(u); /* u is now detached */
u.setLoginName("Joey");
u.setPassword("shabidew");
hibernateSession.getTransaction().commit();
} catch (HibernateException e) {
e.printStackTrace();
hibernateSession.getTransaction().rollback();
hibernateSession.close();
throw e;
}
In Summary
So, when we create instances in our Java code, the instance is considered to be a transient instance, which means there is no mechanism in place to manage the persistent state of that instance. However, once we pass a transient instance to the save, update, or saveOrUpdate method of the Hibernate Session, we consider the transient instance to have transitioned into a persistent instance, as Hibernate will begin to manage the persistent state of that instance. Any instance associated with the Hibernate Session is said to be a persistent instance.
Saving or updating a JavaBean isn't the only way to get your hands on a persistent instance. JavaBeans that have been loaded into the Hibernate Session, either by a get or load method call, or even an HQL or criteria query, are considered to be persistent instances, and as such, any changes or updates to the state of those instances will be persisted to the database by the Hibernate Session as well.
If you do have an instance that you want to release from Hibernate's control, you can always call the evict method of the Hibernate Session, passing in the name of the instance you want freed from Hibernate's control. When an instance is no longer having its state managed by the Hibernate Session, we call that a detached instance, because while it does have a representation in the database, Hibernate isn't doing anything to keep the instance in sync with the underlying persistence store. In effect, the instance is detached from its corresponding representation in the database.
Of course, when we work with Hibernate, all of our interactions with the database must occur within the scope of a transaction. By default, when we call methods like save or update, we can never be totally sure when the corresponding record in the database is updated - all we know for sure is that once the transaction is committed, all of the changes to any of the persistent instances associated with the Hibernate Session will be saved to the database. Of course, there is always the potential that the act of saving all of the data to the database will fail for some reason, and if that does happen, Hibernate will throw a runtime exception. At this point, there's really not too much you can do, other than roll back the current transaction, and close the Hibernate Session. At this point, all of the instances that were previously under the control of the Hibernate Session become detached objects, and quite likely, are no longer in sync with the database. In such a case, you can always start a new transaction, and try to turn your detached instances back into persistent instances by re-associating them with the Hibernate Session through save or update calls, but in the end, you're probably better off just sending a friendly error message to your client application, and start any request-response cycle over again from scratch.
And that's about it; a quick description of how hibernate works, along with a simple description of what we mean when we talk about transient, persistent and detached objects.
No comments:
Post a Comment