2

I have a Java, Spring Data application with a PostgreSQL DB and I have a code similar to this in my service layer:

public void method1() {
    PersonDto dto = method2();
    method3(dto);
}

@Transactional
public PersonDto method2() {
    Person p1 = personRepository.saveAndFlush(new Person());
    return createPersonDto(p1);
}

@Transactional
public void method3(PersonDto p1) {
    Person p2 = personRepository.findById(p1.getId());
    if (p2 == null) {
        System.out.println("Not in the DB");
    } else {
        System.out.println("In the DB");
    }
}

Sometimes this code prints "Not in the DB", when I was expecting that it would always print "In the DB". My questions are:

  1. This saveAndFlush is even necessary? Since I'm not reusing that Entity in the same transaction, I guess it would have the same effect as a save, right?
  2. How to guarantee that the changes in method2 will be committed before method 3 calls it? So it will always print "In the DB"?
  3. The fact that I'm converting my Entity to a DTO is what is messing this up? Maybe if I was passing the Entity itself it would work properly?

I'm thinking about @Transactional only in method1, or replacing @Transactional in method3 with @Transactional(isolation = Isolation.READ_UNCOMMITTED). Any advice?

E-Riz
  • 31,431
  • 9
  • 97
  • 134
Serr
  • 303
  • 1
  • 10
  • 1
    1. Are you sure the ID in `PersonDto` corresponds to the same ID in the returned `Person` object? Are you sure it is not null? 2. Are you sure you are not calling `method2` from another Transactional method, thus the transaction wouldn't have ended yet, and maybe `method3()` is called from another transaction before the first one is committed? – jbx Jan 18 '23 at 18:14
  • Yes, I'm sure, I have a log for it before retrieving it in method3. Also, it works most of the time, about 1% of the time it fails, generally, when it executes super fast. About the Transaction, in the StackTrace there is no other method with a @Transactional annotation. Thanks! – Serr Jan 18 '23 at 19:28

1 Answers1

1

I can see a couple of reason why you're getting the results you describe.

One possibility is that @Transactional doesn't necessarily mean "execute this method in a totally separate transaction." The default propagation is REQUIRED, which will use an existing transaction if one is already active. So if method2() is called in a stack where a transaction was opened earlier, it won't create a new one (and thus the new entity won't be committed) until that original transaction is committed. If this is what's happening, one solution would be to change the propagation to REQUIRES_NEW:

@Transactional(propagation = REQUIRES_NEW)

Another possibility, which seems more likely, is that these methods are all in the same class. To understand why that fails, you have to understand how Spring implements handling of @Transactional (and most other behavioral annotations): using proxies. That means that only method calls that come through the proxy get the transactional behavior. That's the typical scenario when one object calls a reference it has to another; the reference is actually to the proxy, which intercepts the call and wraps the behavior (in this case, transactionality) around it. However, when calling methods within the same instance, there is no proxy in play (Spring doesn't/can't replace this with a reference to the proxy), and thus no transactional behavior.

There are a few ways to work around that, but first I would consider how to restructure my classes and methods to avoid it. If that's not reasonable, things like AspectJ, TransactionTemplate, and maybe some other ideas are discussed in this SO Q&A.


Addendum: This question and its answers also talks about options to work around the proxying problem.

E-Riz
  • 31,431
  • 9
  • 97
  • 134
  • 1
    You are correct, the issue was that all the methods were in the same class, so the @Transactional annotation was not even being used. We ended up passing the Entity to the method we needed instead of the DTO. It was decided that we would go with a very small change instead of a slightly bigger refactoring. But the reason it was failing was exactly what you said, so I'm marking your answer as correct. Thanks for your time. – Serr Jan 20 '23 at 11:05