session.persist() method confusion - java

I read this in doc:
persist() also guarantees that it will not execute an INSERT statement if it is called outside of transaction boundaries.
But when I try this code:
SessionFactory sessionFactory = new Configuration().configure("student.cfg.xml").buildSessionFactory();
Session session = sessionFactory.openSession();
Student student = new Student();
student.setFirstName("XXX");
student.setLastName("YYY");
student.setCity("ZZZ");
student.setState("PPP");
student.setCountry("XXX");
student.setId("NNN");
session.persist(student);
session.flush();
session.close();
The record is getting inserted. As you can see in the above code, I have not used any transaction. Then in that case according to the doc, the data should not be inserted in the DB right?
Transaction boundaries means any operation between Transaction tran = session.beginTransaction(); and tran.commit(); right?
Please let me know where am I making the mistake.
Regards,

If you remove session.flush() and session.close(), you'll observe that no insert statement was executed. The point of that guarantee is that the persist call itself won't execute any statements; it doesn't say anything about the implication on the behavior of flush and close. Indeed, flush flushes all persistent objects to the datastore.

This is old magic!
if (getTransactionIsolation()==Connection.TRANSACTION_NONE
|| getAutoCommit()==true)
The persist() ignores the transaction boundary!
Why? persist() does not know about support transactions or not. In second case its committed because he didnt expected to need a transaction.

Related

How to make Hibernate not to rollback when an exception occurs

The following SQL if run in MSSQL will insert the 1st and 3rd rows successfully:
BEGIN TRAN
INSERT ... -- valid data
INSERT ... -- invalid data (e.g. over column width)
INSERT ... -- valid data
COMMIT
Even though the second row fails within the transaction, you can still see the two rows with some valid data after the commit in the table.
However, when trying something similar in Hibernate, it rollbacks the whole transaction. Is there a way to tell Hibernate not to rollback on failed rows and commit the rest as same as how MSSQL does it?
e.g.
EntityTransaction transaction = em.getTransaction();
transaction.begin();
em.persist(new MyEntity("good"));
em.persist(new MyEntity("too long"));
em.persist(new MyEntity("good"));
transaction.commit();
This is not possible within the same transaction. Hibernate simply doesn't allow this. An error in a statement leads to an exception, which Hibernate cannot recover from. From the manual:
If the JPA EntityManager or the Hibernate-specific Session throws an exception, including any JDBC SQLException, you have to immediately rollback the database
transaction and close the current EntityManager or Session.
Certain methods of the JPA EntityManager or the Hibernate Session will not leave the Persistence Context in a consistent state. As a rule of thumb, no exception thrown by Hibernate can be treated as recoverable. Ensure that the Session will be closed by calling the close() method in a finally block.
Now this is a restriction (design decision) of Hibernate and not of the underlying JDBC or database stack. So what you want is perfectly possible using JDBC directly. If it is really important for you to get that behaviour, you might consider using JDBC calls for this section of the code. There you can do it exactly like in the SQL client: open transaction, issue statements, catching any exceptions manually and "ignoring" them, and at the end committing the transaction.
Example code:
Session session = em.unwrap(Session.class);
session.doWork(connection -> {
// manual commit mode
connection.setAutoCommit(false);
executeInsertIgnoringError(connection, new Object[]{123, null, "abc"});
executeInsertIgnoringError(connection, new Object[]{....});
...
connection.commit();
});
private void executeInsertIgnoringError(Connection connection, Object[] values) {
try (PreparedStatement stmt =
connection.prepareStatement("INSERT INTO MY_ENTITY VALUES (?, ?, ?, ...)")) {
for (int i = 0; i < values.length; i++) {
// PreparedStatement is indexed from 1
stmt.setObject(i+1, values[i]);
}
stmt.executeUpdate();
} catch (Exception e) {
log.warn("Error occurred, continuing.");
}
}
The way i did it is to divide your logic into diferent functions, and open the transaction inside the persisting function instead of the main one.
The main problem I see in your code is that you're defining a block transaction insead of opening a transaction for each operation.
Here's my snippet:
persistEntity(new MyEntity("good"));
persistEntity(new MyEntity("bad"));
persistEntity(new MyEntity("good"));
...
private void persistEntity(MyEntity entity){
EntityTransaction transaction = em.getTransaction();
transaction.begin();
em.persist(entity);
transaction.commit();
}
This way it will rollback just for the bad entity and keep going with the other. You can also add a try catch inside the persistEntity method, if you want to log the exception.
Fun fact, If you're using Spring you could create another #Component for the persist operations and only add #Transactional to the persisting method, this way you don't have to manage the transactions yourself.
Don't do so, that is idiomatically wrong, at first just review the real scope of your transactions.
You could write the code to run one statement at a time with autocommit on and not use #Transactional... Then perhaps catch any exceptions and throw them away as you go. But pretty much everything in that sentence is troublesome to even think about as a responsible developer and it would affect your entire app. Flavius's post would be a little more granular in doing something similar with explicitly smaller transactions and is a good way to go about it too.
As others have been commenting it's not a long term great plan and goes against so many ways to write programs correctly and the benefits and purpose of transactions. Perhaps if you plan to only use this as a one off data ingestion plan you could but again be very wary of using these patterns in a production grade app.
Having been sufficiently alarmed, you can read more about auto commit here and also be sure to read through the post links on why you probably shouldn't use it.
Spring JPA - No transaction set autocommit 'true'
You can do that by adding below property in hibernate config xml file
<property name="hibernate.connection.autocommit" value="true"/>
If you could use #Transactional annotation then
#Transactional(dontRollbackOn={SQLException.class, NOResultException.class})
Then I would suggest one some change in your code. It's better if you add your entities in a loop and catch exception on each transaction.

How FlushMode.COMMIT works

As per Session.setFlushMode(FlushMode) we can set FlushMode to the session. Now I am trying to test how the Flushmode.COMMIT mode works with a small example.
I have created an entity called Cat with just 2 properties id and name. Now here is the code that I am testing:
Session session = getSession();
session.setFlushMode(FlushMode.COMMIT);
Transaction tx = session.beginTransaction();
Cat cat = (Cat) session.get(Cat.class, 1);
cat.setName(name);
session.flush();
//tx.commit();
session.close();
From logs I can see that when the line session.flush() is executed then hibernate is issuing JDBC update call to database as:
Hibernate: update Cat set name=? where id=?
As I set the FlushMode to COMMIT, I am expecting that the update query will be executed only when I say tx.commit() but the flushing is happening at session.flush(). Can someone please explain why it is happening like this?
Note the Javadoc of Session#flush().
Force this session to flush. Must be called at the end of a unit of
work, before committing the transaction and closing the session
(depending on flush-mode, Transaction.commit() calls this method).
or the javadoc for FlushMode#MANUAL
The Session is only ever flushed when Session.flush() is explicitly
called by the application. This mode is very efficient for read only
transactions.
Setting a FlushMode simply defines when flush() will happen automatically (all but MANUAL). If you call flush() yourself, manually, you're overriding that behavior.

Hibernate persist() method

Is the below statement a valid one?
persist() also guarantees that it will not execute an INSERT statement if it is called outside of transaction boundaries
When I try the below code using persist; then the row is getting inserted without any transaction (It is commented out).
SessionFactory sessionFactory = new Configuration().configure("student.cfg.xml").buildSessionFactory();
Session session = sessionFactory.openSession();
//Transaction tran = session.beginTransaction();
/*
* Persist is working without transaction boundaries ===> why?
*/
Student student = new Student();
student.setFirstName("xxx");
student.setLastName("yyy");
student.setCity("zzz");
student.setState("ppp");
student.setCountry("###");
student.setId("123456");
session.persist(student);
//tran.commit();
session.flush();
session.close();
persist() also guarantees that it will not execute an INSERT statement if it is called outside of transaction boundaries
This statement is correct. When control returns from persist() back to your code, no INSERT statements have been executed. These statements are guaranteed to be deferred until session flushing. Note that persist() would be a pointless method if no insert happened ever.
AFAIK data is saving because of session.flush(), try after removing this, mostly you will get an error.
Hibernate persist
Diff. save & persist

Hibernate lock causes dirty collection exception

I'm trying to reattach to the session an object from the HTTP session that was originally retrieved from the DB, I do this by calling session.lock(object, LockMode.None) and even though lock does not cascade this works all right for me because it does not push updates to the DB like merge does (the lock is required to open a detail view in a pop-up and the actual saving would occur later on the main window). Now to my surprise I have found that if my entity has a one to many relation any changes on that collection would cause a HibernateException "Reassociated object has dirty collection".
How am I supposed to reattach objects to the session without updating the DB or discarding the changes on the object then?
Here is the situation as code
EntityA t = createAnEntityA();
Session sess = factory.openSession();
sess.beginTransaction();
sess.save(t);
sess.getTransaction().commit();
sess.close();
// t is now saved on the DB but in dettached state
// change a simple property
sess = factory.openSession();
sess.beginTransaction();
t.setPropertyB("B");
sess.lock(t, LockMode.NONE);
// t is attached again, you won't get LazyInitializationException
// by calling its properties, although you have to be careful
// because the reattachment does not cascade to children
sess.getTransaction().commit();
sess.close();
// no updates went to the DB because setPropertyB was called
// when t was still dettached
// now change a collection
EntityC c = createAnEntityC();
t.getCollectionPropertyC().add(c);
sess = factory.openSession();
sess.beginTransaction();
sess.lock(t, LockMode.NONE);
// Exception is thrown :-(
sess.getTransaction().commit();
sess.close();
I'm afraid currently it's not possible without hitting DB.
Apparently, the exception is happening because of lock command.
There's a Jira reporting this behavior.
https://hibernate.atlassian.net/browse/HHH-511
It has 2 patches to fix the problem. You could try those patches.
But if you problem is only Lazy loading of collection, you could consider using Open Session In View pattern. (maybe not the best pattern, but it can work for your case)

Does session.save() do an immediate insert?

...or does it wait until the associated transaction is committed?
I'm using an HQL query in a loop like this:
tx.begin()
for(...)
{
session.getNamedQuery(...).list()
...
session.save(new MyEntity())
}
tx.commit()
The named query needs to be able to see the entities that were added with the save call. Will it work that way?
It depends on the flush mode of the session.
You can also manually flush it with session.flush()
The flush mode can be set in multiple ways - session.setFlushMode(..), entityManager.setFlushMode(..), or via xml configuration (org.hibernate.FlushMode).
The default value is AUTO:
The Session is sometimes flushed before query execution in order to ensure that queries never return stale state. This is the default flush mode.
Try it, if it doesn't, then call
session.flush()
to send the SQL to the DB. Regardless, it won't be committed until the call to
tx.commit()

Categories

Resources