Saturday, 21 September 2013

Transaction management with Spring Data JPA

Spring provides an easy way to manage transactions.
Let's see how to make our methods transactional without using any XML configuration.

We will start with Spring Java config for our application:

@EnableJpaRepositories
@EnableTransactionManagement
@ComponentScan("pl.mjedynak")
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL).build();
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean();
entityManagerFactory.setDataSource(dataSource());
entityManagerFactory.setJpaVendorAdapter(jpaVendorAdapter());
entityManagerFactory.setPackagesToScan("pl.mjedynak.model");
return entityManagerFactory;
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(true);
hibernateJpaVendorAdapter.setDatabase(Database.HSQL);
return hibernateJpaVendorAdapter;
}
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
view raw AppConfig.java hosted with ❤ by GitHub
We have an entity representing a bank account:

@Entity
public class Account {
@Id
@GeneratedValue
private Long id;
private BigDecimal balance;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public BigDecimal getBalance() {
return balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
@Override
public int hashCode() {
return Objects.hash(id, balance);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
final Account other = (Account) obj;
return Objects.equals(this.id, other.id) && Objects.equals(this.balance, other.balance);
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", balance=" + balance +
'}';
}
}
view raw Account.java hosted with ❤ by GitHub
We also have a repository from Spring Data JPA for Account objects:

public interface AccountRepository extends CrudRepository<Account, Long> {
}

The TransferService allows transferring money from one account to another:

@Component
public class TransferService {
@Autowired private AccountRepository accountRepository;
@Transactional
public void transfer(Account from, Account to, BigDecimal amount) {
BigDecimal currentFromBalance = from.getBalance();
BigDecimal currentToBalance = to.getBalance();
to.setBalance(currentToBalance.add(amount));
accountRepository.save(to);
if (currentFromBalance.compareTo(amount) < 0) {
throw new IllegalStateException("not enough money");
}
from.setBalance(currentFromBalance.subtract(amount));
accountRepository.save(from);
}
}
Just for example sake, we are adding money to one account and before subtracting from the second one, we check if it has enough funds.
If the method wasn't transactional, we would introduce a major bug.
However, @Transactional annotation makes it eligible for rollback if the exception is thrown.
It's also important to note that many methods from the repository are transactional with default propagation, so the transaction from our service will be reused.
Let's make sure that it works by writing an integration test:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class TransferServiceIntegrationTest {
@Autowired private AccountRepository accountRepository;
@Autowired private TransferService transferService;
@Test
public void shouldRollbackTransactionWhenExceptionIsThrown() {
// given
Account from = anAccount().withBalance(valueOf(100)).build();
Account to = anAccount().withBalance(valueOf(0)).build();
accountRepository.save(asList(from, to));
BigDecimal amount = valueOf(200);
// when
try {
transferService.transfer(from, to, amount);
} catch (IllegalStateException e) {
// then
Account returnedFrom = accountRepository.findOne(from.getId());
Account returnedTo = accountRepository.findOne(to.getId());
assertThat(returnedFrom.getBalance().doubleValue(), is(100d));
assertThat(returnedTo.getBalance().doubleValue(), is(0d));
return;
}
fail();
}
}
If we remove @Transactional annotation the test will fail.
Be aware that managing transactions with Spring have some traps that are described in this article.
The whole project can be found at github.