Real-World Experiences With Hibernate - Best Practices
'Best Practices'
Real-World Experiences With Hibernate says that we won't be advocating such practices?". Well, the Hibernate team has it's own "Best Practices" - and we've found that you can get into a mess if you apply them prescriptively without thinking about it. This section aims to examine some of those practices.
Components and Queries
Components provide an excellent technique for refactoring. However, one nuisance that we've encountered is that you cannot drop a component into a query. Consider the following component definition:
@javax.persistence.Embeddable public class MembershipId { private int memberPrefix; private int memberNumber; int getMemberPrefix() { return memberPrefix; } int getMemberNumber() { return memberNumber; } }
Say that this component is used in another entity:
@javax.persistence.Entity public class MemberNote { @javax.persistence.Id private int id; private MembershipId membershipId; private String text; }
and that you wanted to write a DAO method that returned all of the member notes for a particular membership ID. You can't just drop the component into a HQL query:
public List getMemberNotes(MembershipId membershipId) { return session.createQuery( "from MemberNote where membershipId=:membershipId") .setParameter("membershipId", membershipId).list(); }
Because it'll generate SQL that looks like this:
select membernote0_.id as id2_, membernote0_.isocode as isocode2_, membernote0_.memberNumber as memberNu3_2_, membernote0_.text as text2_ from MemberNote membernote0_ where (membernote0_.isocode, membernote0_.memberNumber)=?
This doesn't work with Oracle or HSQLDB. Instead, you have to fully qualify each part of the key:
public List getMemberNotes(MembershipId membershipId) { return session.createQuery( "from MemberNote where " + "membershipId.memberNumber=:memberNumber and " + "membershipId.memberPrefix=:memberPrefix") .setInteger("memberNumber", membershipId.getMemberNumber()) .setInteger("memberPrefix", membershipId.getMemberPrefix()) .list(); }
The documentation says "(n)ever try to use a path-expression that ends in a property of component type"(see here) . Frankly we're a little unsure as to why Hibernate behaves this way with simple equality checks. It'd be understandable if we tried to do a more complex comparison like a greater-than or less-than.
Incidentally, if you repeatedly find yourself doing foreign-key lookups like this, it might be worth mapping a relationship instead and have Hibernate do the work.
Generated Keys
The Hibernate documentation recommends the use of synthetic primary keys (see here) and in our experience, Hibernate works best with them.
However, it can be difficult to test business logic code that is dependent on the value of generated keys. This is because you have reduced control over what keys are generated. For example, say that you had a synthetic primary key on a class and decided that you would sort results by this ID. Because this ID is generated, it can be difficult to ensure that objects will have IDs generated in a particular order and to then check that these objects are sorted correctly.
To summarise, if you're using generated synthetic primary keys, it's best to just set them up so that Hibernate can use them, and then forget about them. Consider them a requirement of Hibernate's internal operation. If possible, don't use them yourself - not if you want to test them anyway.
The equals() Method
A prime example of a Hibernate "best practice" that has caused us problems is the equals() method. However, let me emphasis that the question is not if you should implement the equals() and hashCode() methods - the testing benefits alone make it worthwhile - but how you should implement them.
The Hibernate documentation recommends using "Business key equality" (see here to see what this means). Whilst this technique is certainly the most appropriate from a Hibernate point-of-view, we ran into problems when other users of the database (who were not even using Hibernate) inserted rows with duplicated 'business keys'. This caused our code to behave in unexpected ways.
Given that the columns we had dictated to be the "business key" did not actually constitute the real primary key, it rapidly became apparent that it was unreasonable to expect other users to not insert whatever they want into them. Consequently, we had to revert back to use of the primary key in the equals() method. Unfortunately, a large number of tests that had been written to set up "business key" data had to be modified.
Whilst usage of primary keys in equals() has its downsides (for example, unsaved objects that use a key generator cannot be put into sets), the sad fact in this case was that from the perspective of all users of the system, a primary key was a primary key - and a 'business key' wasn't!
For those cases where you have a composite primary key, we highly recommend implementing your equals() and hashCode() methods with the Jakarta Commons libraries - in particular the EqualsBuilder and HashCodeBuilder classes.
Custom Types
The Hibernate team recommends the use of custom types where possible. These can be extremely useful, but have one significant limitation: instances of custom types that map to multiple columns can't be put into HQL queries - at least not if your database doesn't support tuples (which Oracle doesn't).
For example, we had a database where dates were stored in separate day, month and year columns. This occurred in many tables, so we wrote a custom type and used it when mapping to these tables. Here is an example of its usage:
import javax.persistence.Column; @javax.persistence.Entity public class Membership { @javax.persistence.Id private int id; @org.hibernate.annotations.Type(type = "DateUserType") @org.hibernate.annotations.Columns(columns = { @Column(name = "BIRTH_DD"), @Column(name = "BIRTH_MM"), @Column(name = "BIRTH_YYYY") }) private java.util.Date birthDate; }
However, attempts to pass this date directly into a query failed:
public List getMemberships(Date birthDate) { return session.createQuery( "from Membership m where m.birthDate=:birthDate").setParameter( "birthDate", birthDate, org.hibernate.Hibernate.custom(DateUserType.class)).list(); }
Because Hibernate will generate the following SQL:
select membership0_.id as id3_, membership0_.MBR_BIRTH_DD as MBR2_3_, membership0_.MBR_BIRTH_MM as MBR3_3_, membership0_.MBR_BIRTH_YYYY as MBR4_3_ from Membership membership0_ where (membership0_.MBR_BIRTH_DD, membership0_.MBR_BIRTH_MM, membership0_.MBR_BIRTH_YYYY)=?
(Note that this is similar to the problem described in Components and Queries).
In our case, the only thing that we were able to do was to define a component that wraps the date:
import java.util.Calendar; import java.util.Date; @javax.persistence.Embeddable public class DateWrapper { private int dayOfMonth; private int month; private int year; @javax.persistence.Transient private final Calendar calendar = Calendar.getInstance(); DateWrapper(Date date) { calendar.setTime(date); dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); month = calendar.get(Calendar.MONTH); year = calendar.get(Calendar.YEAR); } Date getDate() { calendar.clear(); calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth); calendar.set(Calendar.MONTH, month); calendar.set(Calendar.YEAR, year); return calendar.getTime(); } int getDayOfMonth() { return dayOfMonth; } int getMonth() { return month; } int getYear() { return year; } }
and use it to map properties that were going to be used in HQL queries:
import javax.persistence.AttributeOverride; import javax.persistence.Column; @javax.persistence.Entity public class Membership { @javax.persistence.Id private int id; @javax.persistence.AttributeOverrides({ @AttributeOverride(name = "dayOfMonth", column = @Column(name = "BIRTH_DD")), @AttributeOverride(name = "month", column = @Column(name = "BIRTH_MM")), @AttributeOverride(name = "year", column = @Column(name = "BIRTH_YYYY")) }) private DateWrapper birthDate; }
Because the type of dateOfBirth was changed from Date to DateWrapper, any code that was using dateOfBirth directly needed to be modified to use DateWrapper.getDate(). We also needed to modify our DAO to explicitly qualify each part of the component:
public List getMemberships(Date birthDate) { DateWrapper birthDateWrapper = new DateWrapper(birthDate); String dayOfMonthParameterName = "dayOfMonth"; String monthParameterName = "month"; String yearParameterName = "year"; return session.createQuery( "from Membership where birthDate.dayOfMonth=:" + dayOfMonthParameterName + " and birthDate.month=:" + monthParameterName + " and birthDate.year=:" + yearParameterName).setInteger( dayOfMonthParameterName, birthDateWrapper.getDayOfMonth()) .setInteger(monthParameterName, birthDateWrapper.getMonth()) .setInteger(yearParameterName, birthDateWrapper.getYear()) .list(); }
Note that as far as we can tell, this problem only occurs when you have a custom type that maps to multiple columns. In short, custom types are still very useful, but may not be worth using for when you are mapping to multiple columns and need to use the mapped property in a HQL query.