Let's see how to make our methods transactional without using any XML configuration.
We will start with Spring Java config for our application:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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 + | |
'}'; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface AccountRepository extends CrudRepository<Account, Long> { | |
} |
The TransferService allows transferring money from one account to another:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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); | |
} | |
} |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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(); | |
} | |
} |
Be aware that managing transactions with Spring have some traps that are described in this article.
The whole project can be found at github.