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.
Somehow I have a bit of a problem with this approach.
ReplyDeleteServices should demarcate transactional boundaries of domain specific logic. With this approach you create a way to write logic in a layer above the service layer which might move logic to the 'wrong' place.
But I agree, used correctly it is a powerful solution.
I think I understand what you're saying, but I am not sure you are using the right words.
ReplyDeleteYou always have the option to write logic above the service layer. This is in fact quite normal and fine as long as you put that logic in the domain objects (which, in my view, the service layer is a part of).
So in other words, even though the service layer normally functions as the transaction demarcation, I see no reason that prohibits you from putting this function in another kind of domain object.
Now the latter may be difficult. If you look at use case 3 in the article for example, I see no clear way to rewrite it such that the transaction boundary is again within a domain object. It should be possible though..., just have to think some more about this.
How is this different then using Spring's old TransactionalTemplate?
ReplyDeleteI didn't know about TransactionalTemplate, it sounds the about the same though.
ReplyDeleteBtw, I can not find the API docs for TransactionalTemplate. Is it still supported?
I assume kristof josza means TransactionTemplate. Surely it's still supported; it's very useful.
ReplyDeleteIn the post Erik wrote the transactional logic still resides in the servicelayer; where it belongs IMHO.
If I understand correctly the spawned thread will not join the same transaction as the parent thread so to make the call transactional you add the @Transactional annotation to the Command class that will ultimately scope the Callable/Runnable methods with transactional boundaries. Does this mean that a new transaction is created when a new thread is spawned and the new thread will run within that new transaction scope?
ReplyDeleteFlorin, yes, that is correct.
ReplyDeleteThanks for the response. I'm trying to get the new transactions to join the existing transaction in progress. Do you know of any solution?
ReplyDeleteflorin
Sorry, I have never ventured in that direction.
ReplyDelete