2

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();
user1589188
  • 5,316
  • 17
  • 67
  • 130
  • 5
    `The following SQL if run in MSSQL` ... that observation seems very strange to me, and I would have expected that if any part of the transaction fails, SQL Server would rollback the entire transaction. – Tim Biegeleisen Apr 20 '21 at 01:31
  • @TimBiegeleisen I guess it depends on what type of exception and in this case MSSQL decided not to rollback everything. – user1589188 Apr 22 '21 at 01:41
  • Even in that case, I would question the Hibernate session, not SQL Server. Maybe somehow Hibernate divided the work into multiple database transactions, hence your current observations. – Tim Biegeleisen Apr 22 '21 at 01:42
  • @TimBiegeleisen It seems some misunderstanding here. Hibernate does rollback (which I don't want); running the SQL statements directly in SSMS inserts those without exception and not rolling back (this is what I want and want to do the same with Hibernate, hence this question). – user1589188 Apr 22 '21 at 01:47
  • Just to be clear, I would like to add that the behavior of MSSQL is weird and unexpected. Hibernate is doing exactly the right thing with respect to the expected behavior of a transaction. – Sherif elKhatib May 01 '21 at 03:17

5 Answers5

2

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.");
    }
}
jhyot
  • 3,733
  • 1
  • 27
  • 44
  • Yup, I reckon the same. If you can write up some example code how this can be done using the entity manager (i.e. issues direct SQLs), I can pick yours as the answer. – user1589188 May 03 '21 at 01:50
  • Great! Thanks for your effort, picked yours as the answer. – user1589188 May 04 '21 at 01:34
1

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.

Flavius D.
  • 53
  • 4
  • Yes but isn't it many transactions vs 1 transaction? What do we get from using transaction with your way? – user1589188 Apr 26 '21 at 04:54
  • 3
    @user1589188 You do understand that the central concept of transaction is atomicity, right? Either all operations succeed or none. You're specifically asking to ditch this property, so reversing the question: what are you hoping to get from insisting on a single transaction? – crizzis Apr 26 '21 at 15:41
  • @crizzis On my testing, doing n separate INSERTs performed worse than having n INSERTs in a single transaction. So in my use case I do not care about atomicity but performance gain. And I am not being nonsense here as I mentioned in my question that MSSQL/SSMS is doing exactly what I want. – user1589188 Apr 27 '21 at 02:21
  • @user1589188 Transactions are made for bulk operations, you want to treat it as "a whole". If an error occours then the whole transaction will rollback. If you do not need this mechanism then i suppose the only way is to do multiple transactions. Databases like postgres for example( idk about mysql) treat every operation as a separate transaction if you don't specify it as a whole. – Flavius D. Apr 29 '21 at 13:36
  • @FlaviusD. Harsha below posted a potential solution (dontRollbackOn) but requires Spring. Do you know how to do the same in Hibernate? – user1589188 Apr 30 '21 at 02:34
  • @user1589188 I do not a way without Spring and also I do not know if that solution applies to SQL errors or just Java Exceptions. My guess here is that if you do not want a transation per insert your best bet is to validate in some way the query before executing it. You could create an Object TableXValidator or method tableXInsertValidator where you get the table structure, store it in an object then compare the query to it (columns, lenght, types etc). This may be a good solutions if the 3 inserts are on the same table, then, you could just ignore the ones that did not pass validation. – Flavius D. Apr 30 '21 at 08:17
0

Don't do so, that is idiomatically wrong, at first just review the real scope of your transactions.

0

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'

Atmas
  • 2,389
  • 5
  • 13
0

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.