With Spring, controlling where a transaction is started can be as simple as adding the @Transactional annotation (and one extra line in the Spring configuration). The @Transactional is typically placed on the class that implements the interface of your service layer. For example you may have a MemberService:
public interface MemberService {
List findMembers(MemberCriteria criteria);
Member getMember(long id);
Member updateMember(Member member);
}
@Transactional
public class DefaultMemberService implements MemberService {
public List findMembers(MemberCriteria criteria) { ... }
public Member getMember(long id) { ... }
public Member updateMember(Member member) { ... }
}
Here is a kind of weird service that I came up with a couple of years ago, named after the Command pattern, the CommandService:
@Transactional
public class DefaultCommandService implements CommandService {
public void inTransaction(Runnable command) {
command.run();
}
public <T> T inTransaction(Callable<T> command) {
try {
return command.call();
} catch (Exception e) {
throw new RuntimeException(
"CommandService#inTransaction("
+ command.toString() + ")", e);
}
}
}
Suppose you have a webpage that will update only one property of an entity, e.g. the email address of a member, and another page to update the password. Now to prevent concurrent update problems you want to get and update the member within the same transaction. You could make a new method on the service interface for each property you want to change. However, it is a good practice to keep interfaces concise. So instead we do something like this:
final long memberId = ...;
final String newEmail = ...;
Member freshMe =
commandService.inTransaction(new Callable<Member>() {
public Member call() throws Exception {
Member freshMe = memberService.getMember(memberId);
freshMe.setEmail(email.getNewEmail());
return memberService.updateMember(freshMe);
}
});
I once wrote some code that would keep thousands of LDAP records in sync with a relational database. In my test environment, a dodgy laptop, it would take 50 minutes to do the initial import of 15,000 records. Each record was persisted to the database in its own transaction. After a small code change, I used the CommandService to persist the records in groups of 20. What do you think happened? The total time to import these 15000 records dropped from 50 to 2 minutes! Not bad, 25 times faster through 4 lines of extra code. Careful positioning of transaction boundaries can have a dramatic effect on performance. CommandService can help you do that. Use case 3: transactions in unusual places
Let us look at a little bit more detailed example of the PeriodicRetriever from my previous article (changes in italic).
public class CachingPostalCodeService {
private final Object postalCacheLock = new Object();
private List<PostalCode> postalCache;
private PeriodicExecutor postalCacheReloader;
private HibernatePostalCodeDao hibernatePostalCodeDao;
public CachingPostalCodeService() {
postalCacheReloader = new PeriodicExecutor(
TimeUnit.MINUTES.toMillis(10), new Runnable() {
public void run() {
refreshPostalCache();
}
public String toString() {
return "Postal code cache reloader";
}
});
}
public List<PostalCode> getPostalCodes() {
postalCacheReloader.requestStart();
synchronized (postalCacheLock) {
return postalCache;
}
}
private void refreshPostalCache() {
List<PostalCode> newPostalCodes =
Collections.unmodifiableList(
hibernatePostalCodeDao.getAll()
);
synchronized (postalCacheLock) {
postalCache = newPostalCodes;
}
}
// ... setter for hibernatePostalCodeDao ...
}
public class CachingPostalCodeService {
private final Object postalCacheLock = new Object();
private List<PostalCode> postalCache;
private PeriodicExecutor postalCacheReloader;
private HibernatePostalCodeDao hibernatePostalCodeDao;
private CommandService commandService;
public CachingPostalCodeService() {
postalCacheReloader = new PeriodicExecutor(
TimeUnit.MINUTES.toMillis(10), new Runnable() {
public void run() {
commandService.inTransaction(new Runnable() {
public void run() {
refreshPostalCache();
}
});
}
public String toString() {
return "Postal code cache reloader";
}
});
}
// ... same as above ...
// ... setters for hibernatePostalCodeDao and commandService
}
Feel free to use the complete version of CommandService in any way you see fit. Conclusions
Watching your transaction boundaries can be very rewarding, both in code size as in performance. A command service, such as the one presented in this article can help you do that.