Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
2.1k views
in Technique[技术] by (71.8m points)

Spring + Hibernate manually creating transactions, PROPAGATION_REQUIRED fails. BUG?

PLEASE DO NOT RECOMMEND THAT I USE TRANSACTION ANNOTAIONS FOR THIS.

I have run into what appears to be a bug, related to Spring handling of transactions.

Please have a look at these two test cases, comments are in the code:

Entity class for example:

@Entity
public class Person{
    @Id
    String name;
}

Some methods used:

public TransactionStatus requireTransaction() {
        TransactionTemplate template = new TransactionTemplate();
        template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        return getTransactionManager().getTransaction(template);
}

public Session session() {
        return getRepository().session();
}

public PlatformTransactionManager getTransactionManager() {
        return getRepository().getTransactionManager();
}

Here is the first test, testA();

@Test
public void testA() throws InterruptedException {
        // We create the first transaction
        TransactionStatus statusOne = requireTransaction();

        // Create person one
        Person pOne = new Person();
        pOne.name = "PersonOne";
        session().persist(pOne);

        // ---> 111) NOTE! We do not commit! Intentionally!

        // We requireTransaction again. We should be getting the same transaction status.

        TransactionStatus statusTwo = requireTransaction();
        if ( !statusTwo.isNewTransaction() ) {
                System.out.println("isNewTransaction: false! As expected! Meaning we are getting the original transaction status!");
        }


        // Create person two
        Person pTwo = new Person();
        pTwo.name = "PersonTwo";
        session().persist(pTwo);

        // We will now be committing statusTwo which should actually be the first one, statusOne,
        // since we are using propagation required and the previous transaction was never committed
        // or rolledback or completed in any other fashion!

        getTransactionManager().commit(statusTwo);

        // !!!!!!! However !!!!!! Nothing is actually written to the database here!

        // This must be a bug. It existed on Spring 4.0.4 and I have upgraded to 4.2.0 and still the same thing happens!

        // Lets go on to the next test. testB() below.

        // If we now, at 111) instead do, let me repeat the entire logic:
}

Here is the second test, testA();

@Test
public void testB() throws InterruptedException {
        // We create the first transaction
        TransactionStatus statusOne = requireTransaction();

        Person pOne = new Person();
        pOne.name = "PersonOne";
        session().persist(pOne);

        // -----> 111) NOW WE ARE COMMITTING INSTEAD, SINCE WE ARE ALMOST FORCED TO BUT DO NOT WANT TO
        getTransactionManager().commit(statusOne);

        // ----> 222) HOWEVER, NOW WE WILL NOT BE ABLE TO ROLLBACK THIS AT A LATER POINT

        // We requireTransaction again. We should be getting A NEW transaction status.

        TransactionStatus statusTwo = requireTransaction();
        if ( statusTwo.isNewTransaction() ) {
                System.out.println("isNewTransaction: true! As expected! Meaning we are getting a new transaction status!");
        }

        Person pTwo = new Person();
        pTwo.name = "PersonTwo";
        session().persist(pTwo);

        getTransactionManager().commit(statusTwo);

        // Now we will have two instances in the database, as expected.

        // If we instead of committing statusTwo would have done:
        // getTransactionManager().rollback(statusTwo)
        // then only the last one will be rolledback which is not desired!

        // Why are we forced to commit the first one to have any effect on future transactions!
        // Delegation will not work like this!
}

Was that clear?

This is obviously a bug, is it not?

Why purpose would requireTransaction with PROPAGATION_REQUIRED have other than destroy future commits by the same thread?

Why is the commit on statusTwo in testA() not sufficient to commit the work on the first one as well?

Should this be done some other way? I think not, right? Bug!

EDIT For those that suggest that I use execute method, fine:

public PlatformTransactionManager getTransactionManager() {
            return /** implement this **/ ; 
}

@Test
public void testAA() throws InterruptedException {
        insertPerson1();
        insertPerson2();
}

public void requireTransaction(TransactionCallback<Object> action) {
        TransactionTemplate template = new TransactionTemplate(getTransactionManager());
        template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);        
        template.execute(action);
}


public void insertPerson1() {
        requireTransaction(new TransactionCallback<Object>() {
                @Override
                public Object doInTransaction(TransactionStatus status) {
                        Person pOne = new Person();
                        pOne.name = "PersonOne";
                        session().persist(pOne);

                        return null;
                }
        });
}

public void insertPerson2() {
        requireTransaction(new TransactionCallback<Object>() {
                @Override
                public Object doInTransaction(TransactionStatus status) {
                        Person pTwo = new Person();
                        pTwo.name = "PersonTwo";
                        session().persist(pTwo);

                        if ( true ) {
                                status.setRollbackOnly();
                                // throw new RuntimeException("aaaaaaa");
                        }

                        return null;
                }
        });
}

On insertPerson2, even if I set to rollback, or throw an exception the first person is still inserted!

That means, not one shared transaction, but actually two separate ones.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

You basically don't use the transaction API as its intended to. The reference documentation is pretty precise about how to use programatic transactions:

PlatformTransactionManager manager = … // obtain a transaction manager (DI)
TransactionTemplate template = new TransactionTemplate(manager);

template.execute(status -> {
  // Code to be execute in a transaction goes here.
});

The important point to realize here is that if you want to have code participating in a single transaction, that code needs to go inside the callback. That basically means where you actually put this template code depends on how big you want your transaction boundaries be. If you'd like to run an entire request inside a transaction, place this code inside a ServletFilter and invoke FilterChain.doFilter(…). That will basically cause all the code exceuted the to participate in the same transaction.

Only executing the code in the callback makes sure exceptions trigger a rollback correctly, something that your suggested usage of API totally ignores.

The TransactionTemplate can be instantiated using a TransactionDefinition object which basically defines the characteristics of the transaction to be created.

A bit of general advice

That said, without further knowledge about all the helper methods you introduced (What PlatformTransactionManager implementation to you use? What does getRepository().getTransactionManager() do? Why not inject the PlatformTransactionManager into the test directly?) it's hard to do any further diagnosis.

If you run into behavior that seems broken at a quick glance, make sure you reduce your code to the essentials and make sure you follow the recommendations of the reference docs. In 90% of the cases the perceived "bug" is just a mistake in using things, often buried in layers of indirection.

If you still think you found a bug, ask in a single canonical place first. If you don't get an answer in relatively short time, think about whether you might be able to improve the question (reduce the code to it's essentials etc.). Poorly asked (vage, verbose) questions usually don't create incentives to answer.

Don't copy & paste cross-posts into multiple locations. That usually just causes frustration amongst the people you're actually trying to get help from, as they have to hunt down all of the places you posted to - time, they could've used to actually help you.

StackOverflow is a great start usually. If the (mis)behavior can really be confirmed being a bug, a ticket can still be created. Alternatively, create a ticket (and then a ticket only) in the first place but be even more prepared to be required to prepare an executable test case, be precise and - most of all - have read the reference documentation :).


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...