0

I am trying to test transactional behaviors. I have made a small application to test, and there is something weird to me.

When I call method add(), the transactional does not work and I get in DB the object myObj, while I expected the transaction to be rollbacked.

If I call methdo add2(), the transaction is rollbacked correctly.

(Just a small precision, MyService is not transactional)

Could you please help me understand why this behavior?

Thanks a lot

Julien

@RestController
@RequestMapping("/test")
@RequiredArgsConstructor
public class Test {
    private final IMyService myService;

    @GetMapping(path = "/1")
    public void add() {
        this.transactionalError();
    }

    @GetMapping(path = "/2")
    @Transactional
    public void add2() {
        this.transactionalError();
    }

    @Transactional
    public void transactionalError() {
        MyObj myObj = new MyObj();
        myService.save(myObj);

        "".substring(2);

        MyObj myObj2 = new MyObj();
        myService.save(myObj2);
    }
}
Chouch
  • 31
  • 1
  • 7
  • `add` isn't transactional, so each call to `save` is a single transaction, instead of one large transaction. That being said, making your web layer the transactional boundary is a bad idea as you don't want your transaction to be dependend on the outcome of your web response. – M. Deinum Apr 04 '23 at 11:55

2 Answers2

2

Solution: The method allocated with @Transactional must be in a different class.

Mar-Z
  • 2,660
  • 2
  • 4
  • 16
1

Since the "correct" answer is already here but has little to no explanation I am going to add the same answer but with explanation.

First of all you have to understand how most Spring annotations work. When spring creates a bean with a @transactional annotation to Autowire it (or in the case of the controller simply to expose the endpoint) what really happens is that Spring creates a proxy around your class. Thus spring is able to add behavior before or after method calls.

E.g. you have an @Service class called DummyService you actually don't get a DummyService instance when doing this:

@Autowired
private DummyService dummyService;

The actual instance is a Proxy around your DummyService.

Now inside your DummyService you do not (and cannot) see that the instance is actually a Proxy. This means if you call another method inside DummyService you not calling the Proxy but rather the actual instance and you don't get the additional behavior of the proxy, e.g. you have no Transaction.

So let's look at a few examples:

First our service:

@Service
public class DummyService {

   
   public void doStuff() {
       // do stuff
       transaction() //call to DummyService itself. No transaction!
       // do more stuff
   }

   @Transactional
   public void transaction() {
       // do stuff
   }

}

now a Controller

@Service
public class DummyController {

   @Autowired
   private DummyService dummyService; // Will hold the proxy instance

   
   @GetMapping(path = "/stuff")
   public void doStuff() {
       // do stuff
       dummyService.transaction() //call to the autowired proxy. We got a transaction!
       transaction() //again no proxy = no transaction!
       // do more stuff
   }

   @GetMapping(path = "/transaction") //Calls to this url get handled by the proxy instance = transaction!
   @Transactional
   public void transaction() {
       // do stuff
   }

   @GetMapping(path = "/stuff-without-transaction")
   public void doStuffWithoutTransaction() {
       DummyService dummy2 = new DummyService(); //Creating the service yourself means no proxy
       // do stuff
       dummy2.transaction() //no proxy = no transaction!
       // do more stuff
   }

}

So as others said: simple answer is to move the @Transactional annotation to a different class, autowire the instance, get the proxy and thus get a transaction. You have to make calls through the Spring proxy instances to make this additional behavior work.

Rule of the thumb: never call @Transactional Methods from inside your class to avoid this problem! And never create Instances of classes with @Transactional methods yourself.

berse2212
  • 464
  • 5