@Transactional in Spring and Exceptions

  • RuntimeExceptions cause the transaction to be rolled back, checked exceptions do not;

  • RuntimeExceptions cause the transaction to be rolled back the moment the exception crosses boundaries @Transactional-method. Even if you catch this exception higher up the stack, the transaction will still be rolled back;

  • This behavior can be controlled through attributes rollbackFor / noRollbackFor at the annotation @Transcational.

Example 1

@Slf4j
@Component
public class Bean {
  private final BeanRepository repository;

  public Bean (BeanRepository repository) {
    this.repository = repository;
  }

  @Transactional
  public void saveItems(List<Item> items) {
    for(Item item: items) {
      try {
        saveItem(item);
      } catch(RuntimeException e) {
        log.error("Could not save item {}", item, e);
      }
    }
  }

  @Transactional
  public void saveItem(Item item) {
    if(item.foo().equals("BAD_ITEM")) 
      throw new RuntimeException();

    repository.save(item);
  }
}

What happens if we pass a list to save Item‘s whose 0th and 2nd elements are NOT BAD_ITEMs, but the 1st is?

  1. Nothing will be saved because RuntimeException in the saveItem method will roll back the transaction;

  2. Only the 0th element will be saved;

  3. The 0th and 2nd element will be stored.

Answer

The correct answer is 3 (well, as a rule, more details below). When we call a method of the same component from a component in this way, it is just a call to an internal method, not a method of a proxy object. It simply ignores the annotation @Transactional at saveItem and will not create a new transactional context. Why this is happening can be found in the articles I linked to at the beginning. This means that RuntimeException will not cross the boundary of the @Transactional method.

If we use compile-time weaving, or if we rewrite the code so that the call occurs through inner.saveItemthen the correct answer is 1.

Example 2

@Component
public class Bean {
  private final BeanRepository repository;

  public Bean(BeanRepository repository) {
    this.repository = repository;
  }

  @Transactional
  public void saveItem(Item item) {
    if(item.foo().equals("BAD_ITEM")) 
      throw new RuntimeException();

    repository.save(item);
  }
}

@Slf4j
@Controller
public class Controller {
  private final Bean bean;

  public Controller(Bean bean) {
    this.bean = bean;
  }
  
  public void saveItems(List<Item> items) {
    for(Item item: items) {
      try {
        bean.saveItem(item);
      } catch(RuntimeException e) {
        log.error("Could not save item {}", item, e);
      }
    }
  }
}

The same question: what happens if we pass a list to save Item‘s whose 0th and 2nd elements are NOT BAD_ITEMs, but the 1st is?

  1. Nothing will be saved;

  2. Only the 0th element will be saved;

  3. The 0th and 2nd element will be stored.

Answer

The correct answer is still 3. Now the transaction with RuntimeException‘om will be rolled back, but this code creates 3 transactions. The remaining 2 will be committed.

Example 3

Everything is the same, but now the method saveItems Same @Transactional:

@Slf4j
@Controller
public class Controller {
  ...
  @Transactional
  public void saveItems(List<Item> items) {
    for(Item item: items) {
      try {
        bean.saveItem(item);
      } catch(RuntimeException e) {
        log.error("Could not save item {}", item, e);
      }
    }
  }
}
  1. Nothing will be saved;

  2. Only the 0th element will be saved;

  3. The 0th and 2nd element will be stored.

Answer

And now the correct answer is 1. By default, @Transactional uses Propagation.REQUIREDwhich will lead to saveItem will use the transaction opened for saveItems. If an error occurs, it will be marked as rollbackOnly.

Example 4

And now let’s add a constraint to the controller that RuntimeException‘s should not be rolled back.

@Slf4j
@Controller
public class Controller {
  ...
  @Transactional(noRollbackFor = RuntimeException.class)
  public void saveItems(List<Item> items) {
      try {
        bean.saveItem(item);
      } catch(RuntimeException e) {
        log.error("Could not save item {}", item, e);
      }
  }
}
  1. Nothing will be saved;

  2. Only the 0th element will be saved;

  3. The 0th and 2nd element will be stored.

Answer

The correct answer is still 1. noRollbackFor only affects the annotated method, its behavior is not “inherited” by components down the call stack, even if they use the same transaction. That’s why saveItem will mark the transaction as rollbackOnly.

Example 5

reschedule noRollbackFor on saveItem. And we will remove the interception code from the controller RuntimeException.

@Component
public class Bean {
  private final BeanRepository repository;

  public Bean(BeanRepository repository) {
    this.repository = repository;
  }

  @Transactional(noRollbackFor = RuntimeException.class)
  public void saveItem(Item item) {
    if(item.foo().equals("BAD_ITEM")) 
      throw new RuntimeException();

    repository.save(item);
  }
}

@Slf4j
@Controller
public class Controller {
  private final Bean bean;

  public Controller(Bean bean) {
    this.bean = bean;
  }

  @Transactional  
  public void saveItems(List<Item> items) {
    for(Item item: items) {
      bean.saveItem(item);
    }
  }
}
  1. Nothing will be saved

  2. Only the 0th element will be saved;

  3. The 0th and 2nd element will be stored.

Answer

The correct answer is 1. Yes, now saveItem does not roll back the transaction, but with the RuntimeException‘oh he doesn’t do anything. Exception will fly through the controller, and already at it @Transactional-method will cause the transaction to be rolled back.

Example 6

Let’s mark both @Transactional-method so that they don’t rollback runtime exceptions:

@Component
public class Bean {
  private final BeanRepository repository;

  public Bean(BeanRepository repository) {
    this.repository = repository;
  }

  @Transactional(noRollbackFor = RuntimeException.class)
  public void saveItem(Item item) {
    if(item.foo().equals("BAD_ITEM")) 
      throw new RuntimeException();

    repository.save(item);
  }
}

@Slf4j
@Controller
public class Controller {
  private final Bean bean;

  public Controller(Bean bean) {
    this.bean = bean;
  }

  @Transactional(noRollbackFor = RuntimeException.class)
  public void saveItems(List<Item> items) {
    for(Item item: items) {
      bean.saveItem(item);
    }
  }
}
  1. Nothing will be saved

  2. Only the 0th element will be saved;

  3. The 0th and 2nd element will be stored.

Answer

The correct answer is 2. The transaction will not be rolled back, but the list processing will stop due to an exception. The same effect will be if you remove the annotations altogether @Transactionalor if this annotation is removed only from the controller.

Example 7

At Spring Data in the interface CrudRepository there is this method:

void deleteAllById(Iterable<? extends ID> ids)

What if one non-existent one among the existing identifiers is passed to it as input? I’d expect it to remove the ones that are there and ignore the ones that aren’t (well, just like the regular operator delete V SQL). And yes Spring Datawill do just that, but only if you use Spring Data 3. And he’s about six months old now. If you are using an earlier version you will get an error EmptyResultDataAccessException (Spring Data strange people wrote, and yes, she went to hell with this backward compatibility of yours).

@Component
public class Bean {
  private final BeanRepository repository;

  public Bean(BeanRepository repository) {
    this.repository = repository;
  }

  @Transactional(noRollbackFor = EmptyResultDataAccessException.class)
  public void deleteItems(List<Item> items) {
    repostory.deleteAllById(items.stream().map(Item::id).toList());
  }
}

What happens if there is a non-existent ID in the middle of the list in Spring Data 2.x?

Answer

The transaction will be completely rolled back. At Repository modifying methods are themselves labeled as @Transactionalso this example is effectively equivalent to example number 4.

You can override the method in your repository deleteAllByIdhaving already indicated to him noRollbackFor = EmptyResultDataAccessException.classbut that’s a bad idea. deleteAllById just calls in a loop deleteById. It is also marked as @Transactionalbut it doesn’t matter, as we found out in example number 1. When deleteById will throw an exception, the transaction will not be rolled back, but the loop will break. As a result, you will find yourself in the situation of example number 6, when half Item‘ov retired, and half – no.

Exit – either instead deleteAllById check on the service side that Item exists, and then call deleteByIdor removed by modifying JPQL-request.

Example 8

public interface BeanRepository extends JpaRepository<Item, Long> { 
  @Override
  @Transactional(noRollbackForClassName = "org.springframework.dao.EmptyResultDataAccessException", rollbackFor = DataAccessException.class, noRollbackFor = RuntimeException.class)
  void deleteAllById(Iterable<? extends Long> longs);
}

What happens if one of the IDs is missing?

Answer

Well, firstly, the author of such a pull request will most likely be beaten on a code review. Possibly feet.

Answer – the rule that contains the nearest parent of the abandoned will work. Exception‘A. In our case – noRollbackForClassNameso part Item‘ov will be removed, some will not.

If the parent is not among the rules, the default behavior will be used (RuntimeException And Error rollback, checked – do not touch). There is also a completely stubborn option, when something like is written rollbackFor = RuntimeException.class, noRollbackFor = RuntimeException.class. Spring let me do that too, unfortunately. Judging by the source code, noRollback rules will always work.

Results

  1. Attributes rollbackFor / noRollbackFor control only the behavior of the transaction in case of exceptions. The exceptions themselves are still pushed higher up the stack;

  2. rollbackFor / noRollbackFor attributes are not inherited @Transactional-methods down the call stack, even if used Propagation.REQUIRED;

  3. If you seriously have to understand and actively use all this, then think about whether transactions in general and container-managed transactions in particular are needed in your particular case. Switching to manual work with transactions can simplify the code, compared to the logic built on different types of exceptions.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *