I'd like to present and gain feedback on an alternate approach that we've come up with to the whole equals/hashCode problem. We haven't done extensive testing, and I'd like to hear about gotchas from those in the know prior to making a significant investment.
After reading what's available (forums, Hib In Action, etc.), one thing's certainly clear - there's simply no perfect solution. In it's absence, however, we've attempted to come up with one that's optimized to our situation and constraints, but it's not without it's limitations. In fact, I admit it's complete hackery in some respects.
Our goals were the following:
1. Simplify the creation of new entities by eliminating the need for an immutable logical (business) key and the construction of customized eq/hc methods. Many of our objects simply don't naturally fit this model.
2. Maintain equality across session boundaries which mandates deviation from the standard Java model of object identity.
Our constraints include the following:
1. All entities will have surrogate keys
2. The database will either be PostgreSQL or Oracle and all surrogate keys will be supplied from sequences. Note that this enables the retrieval of new identifiers without previously inserting a table row.
3. We're building a web app and a session will typically be available when performing manipulations of the domain objects (creation, adding to collections, and what not). We expect rich manipulation of detached objects to be virtually non-existent.
Our approach:
1. We use the standard DAO pattern, but extend it with an additional generate() method that returns a fresh object with an id allocated from the database. Note that it's not persistent yet, but does contain a unique id. Logically makes sense, but getting it to work is the hackery you'll see below.
2. All entities extend an AbstractEntity class that contains final implementations of the equals and hashCode methods along with an abstract getEntityClass() method.
3. Entities that will be added to a collection must be generated rather than simply created. Other than that, all works as usual.
Comments:
- Of course, we could use the save/flush approach, but it's more awkward and error prone for some of our more junior developers not to mention polluting code with session dependencies and the performance implications.
- Unit testing is an issue we need to address with this approach. Ideas include simply having a dependency injected that generates ids using a simple counter during testing.
- I know this is a hack and it stands the hairs on the back of my neck up too, but I'll sacrifice elegance for a real world solution with known shortcomings that works in our environment and reduces code bloat.
Client Code:
Code:
Set<User> users = new HashSet<User>();
UserDao userDao = new UserDao();
User newUser = userDao.generate();
// Set some properties, etc.
// Might want to have the create automatically scheduled upon generate?
userDao.create(newUser);
users.add(newUser);
Entity Code:
Yes, I know I'm dangerously casting objects in the getIdentifierGenerator() method, but it's the only way I know of to get to the generators.
Code:
public abstract class AbstractEntityDao {
private static Map<Class, IdentifierGenerator> entityIdentifierGenerators = new HashMap<Class, IdentifierGenerator>();
public AbstractEntityDao() {
}
/**
* The concrete persistent class for which this DAO manages persistence.
* Each DAO subclass must override this method to specify the persistent
* class it persists.
* The entity class is used in obtaining the id generator for the entity.
*/
public abstract Class getEntityClass();
protected Long generateEntityIdentifier() {
// IMPORTANT - Note the null object used in id generation. This only works with specific identifier
// generators such as sequence and the like that don't rely on a row first being created in the database
return (Long) getIdentifierGenerator().generate((SessionImpl)HibernateUtil.getCurrentSession(), null);
}
protected IdentifierGenerator getIdentifierGenerator() {
IdentifierGenerator idGenerator = entityIdentifierGenerators.get(getEntityClass());
if (idGenerator == null) {
synchronized (entityIdentifierGenerators) {
idGenerator = entityIdentifierGenerators.get(getEntityClass());
if (idGenerator == null) {
EntityPersister persister = (AbstractEntityPersister) HibernateUtil.getSessionFactory().getClassMetadata(getEntityClass());
idGenerator = persister.getIdentifierGenerator();
entityIdentifierGenerators.put(getEntityClass(), idGenerator);
}
}
}
return idGenerator;
}
}
Code:
public class UserDao extends AbstractEntityDao {
public UserDao() {
super();
}
public User generate() {
return new User(generateEntityIdentifier());
}
@Override
public Class getEntityClass() {
return User.class;
}
}
The eq/hc implementations need more work, but you get the idea.
Code:
/**
* The base class for all domain entities.
* All entities are required to have a surrogate primary key of type Long.
*/
public abstract class AbstractEntity {
private Long id;
public AbstractEntity() {}
public AbstractEntity(Long id) {
this.id = id;
}
/**
* The concrete persistent class that this entity represents.
* Each concrete entity subclass must override this method to specify
* the persistent class it represents. A persistent class is one that's
* mapped in Hibernate to a table containing a surrogate identifier.
* The entity class is used for implementing equals.
*/
public abstract Class getEntityClass();
public Long getId() {
return id;
}
private void setId(Long id) {
this.id = id;
}
@Override
public final boolean equals(Object that) {
if (this == that) return true;
if (!(getEntityClass().isInstance(that))) return false;
// If you get the following exception, you're likely trying to add a transient entity to a collection
// Use the generate() method of the entity's respective DAO instead to generate an entity with a valid id
if (id == null) throw new IllegalStateException("id not set; use DAO generation instead of creation to obtain an entity with a valid id");
return id.equals(((AbstractEntity) that).getId());
}
@Override
public final int hashCode() {
// If you get the following exception, you're likely trying to add a transient entity to a collection
// Use the generate() method of the entity's respective DAO instead to generate an entity with a valid id
if (id == null) throw new IllegalStateException("id not set; use DAO generation instead of creation to obtain an entity with a valid id");
return id.hashCode();
}
}
Details omitted for brevity, but creation of new entities is straightforward and reasonably constrained.
Code:
/**
* A persistent entity class that represents a user of the system
*/
public class User extends AbstractEntity {
public User() {
super();
}
public User(Long id) {
super(id);
}
@Override
public Class getEntityClass() {
return User.class;
}
}