Thursday, December 21, 2006
Through the looking glass
Project Looking Glass, Sun's Java 3D desktop (Vista Killer) has finally released 1.0.0, and of course I had to check it out. Here's a screenshot:
Wednesday, December 20, 2006
Transactions and exceptions revisited
We had a long discussion which I'm going to try and sum up here. Obviously you (almost) never want an exception to leave the transactional layer without rolling back the transaction, since that means that something went wrong, but the transaction commited anyway. Suppose you have something like this:
@Transactional
public void registerNewUser(User user) throws LDAPException {
log.info("Registering user: " + user);
userDao.insertUser(user);
ldapService.registerEntry(user.getFirstName(), user.getLastName()); // Let's assume this throws a (checked) LDAPException
}
in the service layer, and then you call this method from outside the service layer, for example in an MVC controller. Now if the call to create the LDAP entry fails (throws a checked exception), the user data is stored even though an exception is thrown out to the calling controller, which can't do anything about it.
This scenario could be handled in two different ways, depending on business requirements: either a failed LDAP registration is fatal, and we need to roll back the transaction to avoid storing user data, or an LDAP exception is not fatal and we should catch and gracefully handle the failure inside the registerNewUser() service method.
If we decide to roll back, we have a few options:
- Catch LDAPException and throw a RuntimeException
- Access the current transaction and call setRollbackOnly (for example via TransactionAspectSupport.currentTransactionStatus().setRollbackOnly())
- Modify the transaction semantics in the configuration, by setting rollbackFor=LDAPException on the annotation
@Transactional
public void registerNewUser(User user) {
log.info("Registering user: " + user);
userDao.insertUser(user);
try {
ldapService.registerEntry(user.getFirstName(), user.getLastName()); // Let's assume this throws a (checked) LDAPException
} catch (LDAPException e) {
// Log and move on
log.error("LDAP registration failed: " + e.getMessage());
}
}
No checked exception is thrown anymore. Furthermore, assume that the LDAP service call is also transactional, and that we're using the default propagation behaviour, REQUIRED, so that the LDAP service call participates in the same transaction as the registerNewUser() started. If the rule is to always roll back on any transaction, the exception thrown in the LDAP service call will mark the current transaction for rollback, even though we catch it in the user service method! That means that we have lost the ability to pass on failure messages between horizontal transactional calls (service-to-service).
So to conclude:
- We need to keep a way to pass on failure messages between calls inside the same transaction, and checked exceptions are a good candidate for that (certainly better than returning a negative integer).
- You should not expose service methods that throw checked exceptions to layers above/outside the transactional layer.
Thursday, December 14, 2006
This you need to know
However, please note that the Spring Framework's transaction infrastructure code will, by default, only mark a transaction for rollback in the case of runtime, unchecked exceptions; that is, when the thrown exception an instance or subclass of RuntimeException. (Errors will also - by default - result in a rollback.) Checked exceptions that are thrown from a transactional method will not result in the transaction being rolled back.
Note the very last sentence (my bold). Checked exceptions do not trigger a transaction rollback!There is of course a way change this behaviour by listing an array of Exception classes for which to roll back for, the details are in section 9.5.3 and below.
Personally I find this quite counter-intuitive, but I suppose there is good reason for this default behaviour. In either case, it's very important to be aware of how it works.
Wednesday, December 13, 2006
Faster DAO testing
First of all, don't add a (Annotation)LocalSessionFactoryBean to your test context, just a DataSource and a hibernate.cfg.xml, and don't add any mapped/annotated classes to the configuration file either. Then create a base DAO test class that looks something like this:
public class AbstractDaoTest {
SessionFactory sessionFactory;
public AbstractDaoTest(Class... classes)
{
// Read hibernate.cfg.xml from root of classpath, for example src/test/resources
Configuration configuration = new Configuration();
for (Class clazz : classes) {
configuration.addClass(clazz);
}
sessionFactory = configuration.buildSessionFactory();
}
}
Then place all your DAOs and the DataSource in the test application context, and start writing your DAO tests like this:
public class ParentDaoTest extends AbstractDaoTest {
ParentDao dao;
public ParentDaoTest() {
// Parent has a relation to the Child class, so we need two classes mapped
super(Parent.class, Child.class);
}
public void setParentDao(ParentDao dao)
this.dao = dao;
}
public void testSomething() {
// Stuff that involves Parent and Child
}
}
Now there's only two classes that Hibernate needs to parse metadata for and create CRUD SQL for and so on, which can speed up application context startup considerably compared to mapping all domain classes, if you have 10-20 classes in you domain model.
Sunday, December 10, 2006
Thursday, December 07, 2006
En vän har gått ur tiden
Min katt Morrissey dog idag. Han hade en medfödd, obotlig njursjukdom som till slut blev för svår att uthärda, och han somnade in på sin lurviga fäll hemma i soffan efter en överdos sömnmedel.
Tack Yvonne på Djurdoktorn för all hjälp.
Lillmosse, 2002-07-04 - 2006-12-07