29 Aralık 2020 Salı

SpringData @Transactional - Rollback

Giriş
1. Rollback işlemi için unchecked bir exception fırlatmak yeterli. 
2. Eğer checked exception kullanmak istersek rollbackFor alanını kullanmak gerekir. Açıklaması şöyle
... by default in spring transactions are rolled back only for runtime exceptions. When a checked exception is thrown from your code and you don’t explicitly tell spring that it should rollback the transaction then it get’s committed.
3. Eğer bazı unchecked exception'lar için rollback olmasın istiyorsak noRollbackFor alanı kullanılır.

4. rollbackFor ve noRollbackFor alanları parametre olarak class alır. Bunların string yani class ismi alan türevleri ise rollbackForClassName ve noRollbackForClassName alanları. 

1. rollbackFor Alanı - Class Parametre
Örnek
Şu kod java.lang.ArithmeticException fırlattığı için otomatik rollback yapar
@Transactional
public Response<Void> updateMedium(UserVO userVO){

  UserDomain userDomain = this.getIfPresent(userVO.getId());
  userDomain.setDeleted(userVO.getDeleted());
  UserDomain saved = userRepository.save(userDomain);
  int a = 2 / 0;

  return Response.success();
}
Şu kod artık exception fırlatıyor. unchecked exception fırlatmadığı için artık otomatik rollback yapmaz.
@Transactional
public Response<Void> updateMedium(UserVO userVO) throws Exception {

  UserDomain userDomain = this.getIfPresent(userVO.getId());
  userDomain.setDeleted(userVO.getDeleted());
  userDomain.setPassword(userVO.getPassword());
  userDomain.setUsername(userVO.getUsername());
  userDomain.setCreateTime(new Date());
  UserDomain saved = userRepository.save(userDomain);
  try {
    int a = 2 / 0;
  } catch (Exception e) {
    throw new Exception();
  }

  return Response.success();
}
Kodu şöyle yapmak gerekir
@Transactional(rollbackFor = Exception.class)
Örnek - unchecked exception
Şu kod rollback yapar, çünkü unchecked exception fırlatıyor
@Transactional
public void rollbacksOnRuntimeException() {
  jdbcTemplate.execute("insert into test_table values('rollbacksOnRuntimeException')");
  throw new RuntimeException("Rollback!");
}
Örnek - checked exception
Şu kod rollback yapmaz çünkü checked exception fırlatıyor ancak rollbackOn alanı tanımlı değil
@Transactional
public void noRollbackOnCheckedException() throws Exception {
  jdbcTemplate.execute("insert into test_table values('noRollbackOnCheckedException')");
  throw new Exception("Simple exception");
}
Örnek
Şu iki kod da düzgün rollback yapar. Birincisi checked exception fırlatıyor ve rollBackOn alanı tanımlı. İkincisi ise rollbackOn tanımlı olsa bile zaten unchecked exception fırlatabilir.
@Transactional(rollbackFor = CustomCheckedException.class)
public void withRollbackOnAndDeclaredException() throws CustomCheckedException {
  jdbcTemplate.execute("insert into test_table
values('withRollbackForAndDeclaredException')"
);
  throw new CustomCheckedException("rollback me");
}

@Transactional(rollbackFor = CustomCheckedException.class)
public void withRollbackOnAndRuntimeException() throws CustomCheckedException {
  jdbcTemplate.execute("insert into test_table
values('withRollbackOnAndRuntimeException')"
);
  throw new RuntimeException("rollback me");
}
2. noRollbackFor Alanı - Class Parametre
Aynı transaction içinde bir yerde exception fırlatılıyorsa normalde transaction rollback edilir. Ancak bu exception'ı biz bilerek göz ardı etmek istersek iki tane temel çözüm var.

1. noRollbackFor kullanmak
2. Exception fırlatan kodu REQUIRES_NEW ile yeni bir transaction içine almak.

Örnek
Elimizde şöyle bir kod olsun.
@Transactional
public void addPeople(String name) {
  personRepository.saveAndFlush(new Person("Jack", "Brown"));
  personRepository.saveAndFlush(new Person("Julia", "Green"));
  String resultName = name;
  try {
    personValidateService.validateName(name);
  }
  catch (IllegalArgumentException e) {
    log.error("name is not allowed. Using default one");
    resultName = "DefaultName";
  }
  personRepository.saveAndFlush(new Person(resultName, "Purple"));
  }
}
validateName() şöyle olsun
@Service
public class PersonValidateService {
  @Autowired
  private PersonRepository personRepository;

  @Transactional
  public void validateName(String name) {
    if (name == null || name.isBlank() || personRepository.existsByFirstName(name)) {
      throw new IllegalArgumentException("name is forbidden");
    }
  }
}
Burada amaç eğer validateName() exception fırlatırsa bile default name ile bir kayıt yaratmak. Ancak bu kayıt yaratılmıyor. Çünkü tüm Spring kodları bir @Transaction ile işaretli olduğu için aynı transaction içinde çalışıyor. Spring her hangi bir yerde exception yakarlarsa geri kalan işlemleri de yapmaz. Bu durumda şöyle yaparız
@Service
public class PersonValidateService {
  @Autowired
  private PersonRepository personRepository;

  @Transactional(noRollbackFor = IllegalArgumentException.class)
  public void validateName(String name) {
    if (name == null || name.isBlank() || personRepository.existsByFirstName(name)) {
      throw new IllegalArgumentException("name is forbidden");
    }
  }
}
Açıklaması şöyle
The default @Transactional propagation is REQUIRED. It means that the new transaction is created if it’s missing. And if it’s present already, the current one is supported. So, the whole request is being executed within a single transaction.

Anyway, there is a caveat. If the RuntimeException throws out of the transactional proxy, Spring marks the current transaction as rollback only. That’s exactly what happened in our case. PersonValidateService.validateName throws IllegalArgumentException. Transactional proxy tracks it and sets on the rollback flag. Later executions during the transaction have no effect because they ought to be rolled back in the end.
Örnek
Şöyle yaparız
@Service
@Transactional(
  isolation = Isolation.READ_COMMITTED, 
  propagation = Propagation.SUPPORTS, 
  readOnly = false, 
  timeout = 30)
public class CarService {
 
  @Autowired
  private CarRepository carRepository;
 
  @Transactional(
    rollbackFor = IllegalArgumentException.class, 
    noRollbackFor = EntityExistsException.class,
    rollbackForClassName = "IllegalArgumentException", 
    noRollbackForClassName = "EntityExistsException")
  public Car save(Car car) {
    return carRepository.save(car);
  }
}

Hiç yorum yok:

Yorum Gönder