Sharing Our Passion for Technology
& Continuous Learning
Hibernate Date vs Timestamp
I encountered a subtle hibernate mapping issue involving Dates and Timestamps. The following test recreates this issue.
package com.sourceallies.logging;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import javax.annotation.Resource;
import org.apache.commons.lang.time.DateUtils;
import org.hibernate.Criteria;
import org.hibernate.SessionFactory;
import org.hibernate.classic.Session;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;
import com.sourceallies.Person;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@TransactionConfiguration(defaultRollback=false)
public class HibernateDateTimeTest {
@Resource
private SessionFactory sessionFactory;
private Session currentSession;
private Date birthDateWithTime;
private Date birthDateWithoutTime;
private Date createDate;
private Date modifyDate;
@Before
public void setUp() throws Exception{
currentSession = sessionFactory.getCurrentSession();
birthDateWithTime = new Date();
birthDateWithoutTime = DateUtils.truncate(birthDateWithTime, Calendar.DATE);
createDate = new Date();
modifyDate = new Date();
}
@Test
@Transactional
public void testSave(){
Person person = new Person("testFirst", "testLast",
birthDateWithTime, createDate, modifyDate);
assertFalse(person.getBirthDate() instanceof Timestamp);
assertFalse(person.getCreateDate() instanceof Timestamp);
assertFalse(person.getModifyDate() instanceof Timestamp);
saveOrUpdate(person);
}
@Test
@Transactional
public void testFind(){
List<Person> people = findAll();
assertEquals(1, people.size());
Person foundPerson = people.get(0);
assertTrue(foundPerson.getBirthDate() instanceof Date);
assertTrue(foundPerson.getCreateDate() instanceof Timestamp);
assertTrue(foundPerson.getModifyDate() instanceof Timestamp);
assertFalse(foundPerson.getBirthDate().equals(birthDateWithTime));
assertTrue(foundPerson.getBirthDate().equals(birthDateWithoutTime));
assertFalse(foundPerson.getCreateDate().equals(createDate));
assertFalse(foundPerson.getModifyDate().equals(modifyDate));
}
public void saveOrUpdate(Person person) {
currentSession.saveOrUpdate(person);
currentSession.flush();
}
@SuppressWarnings("unchecked")
private List<Person> findAll() {
Criteria criteria = currentSession.createCriteria(Person.class);
List<Person> people = criteria.list();
return people;
}
}
Here is the mapping for person.
...
@Entity
public class Person {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id = -1L;
@Column
private String firstName;
@Column
private String lastName;
@Column
@Type(type="date")
private Date birthDate;
@Column
@Type(type="timestamp")
private Date createDate;
@Column
private Date modifyDate;
...
}
There are three Date fields: 'birthdate', 'createDate', and 'modifyDate'. The fields 'birthDate' and 'createDate' have a specified Type. The field 'modifyDate' however, does not have a specified Type. The generated SQL from Hibernate is as follows.
create table Person (id bigint generated by default as identity (start with 1),
birthDate date, createDate timestamp, firstName varchar(255),
lastName varchar(255), modifyDate timestamp, primary key (id))
We see that Hibernate created 'birthDate' as a 'Date' and the other two were created as a 'Timestamp'. The primary difference between 'Date' and 'Timestamp' in SQL is that 'Timestamp' holds the date and time while the 'Date' only holds the date.
At first glance this is not a big deal, but let's take a closer look at the test.
@Test
@Transactional
public void testSave(){
Person person = new Person("testFirst", "testLast",
birthDateWithTime, createDate, modifyDate);
assertFalse(person.getBirthDate() instanceof Timestamp);
assertFalse(person.getCreateDate() instanceof Timestamp);
assertFalse(person.getModifyDate() instanceof Timestamp);
saveOrUpdate(person);
}
Person is created with three Dates that all include date and time values.
@Test
@Transactional
public void testFind(){
List<Person> people = findAll();
assertEquals(1, people.size());
Person foundPerson = people.get(0);
assertTrue(foundPerson.getBirthDate() instanceof Date);
assertTrue(foundPerson.getCreateDate() instanceof Timestamp);
assertTrue(foundPerson.getModifyDate() instanceof Timestamp);
assertFalse(foundPerson.getBirthDate().equals(birthDateWithTime));
assertTrue(foundPerson.getBirthDate().equals(birthDateWithoutTime));
assertFalse(foundPerson.getCreateDate().equals(createDate));
assertFalse(foundPerson.getModifyDate().equals(modifyDate));
}
When Hibernate retrieves a Person from the database the 'createDate' and 'modifyDate' have been converted to Timestamps. At first glance this doesn't appear to be a big deal. java.sql.Timestamp extends java.util.Date. But why are they not equal. I found a helpful answer to this question in Effective Java (2nd Edition) page 41.
There are some classes in the Java platform libraries that do extend an instantiable class and add a value component. For example, java.sql.Timestamp extends java.util.Date and adds a nanoseconds field. The equals implementation for Timestamp does violate symmetry and can cause erratic behavior if Timestamp and Date objects are used in the same collection or are otherwise intermixed. The Timestamp class has a disclaimer cautioning programmers against mixing dates and timestamps. While you won’t get into trouble as long as you keep them separate, there’s nothing to prevent you from mixing them, and the resulting errors can be hard to debug. This behavior of the Timestamp class was a mistake and should not be emulated. (Bloch, Effective Java, 2nd Ed.)If you want to read more about 'equals' read chapter 3, Item 8 in Effective Java (2nd Edition). Apart from a hack there are two primary ways to solve this issue. The first way involves typing 'createDate' and 'modifyDate' to Timestamp.
...
@Column
@Type(type="date")
private Date birthDate;
@Column
private Timestamp createDate;
@Column
private Timestamp modifyDate;
...
The second approach uses a custom UserType. Here is a custom UserType that converts Timestamp to Date and vise versa. There is a side effect with this solution. Converting Timestamp to Date drops the nanosecond precision. If this is not acceptable then the previous solution should be used.
package com.sourceallies;
import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.Date;
import org.hibernate.HibernateException;
import org.hibernate.usertype.UserType;
public class DateTimeUserType implements UserType {
@Override
public int[] sqlTypes() {
return new int[]{Types.TIMESTAMP};
}
@SuppressWarnings("rawtypes")
@Override
public Class returnedClass() {
return Date.class;
}
@Override
public boolean equals(Object x, Object y) throws HibernateException {
return x == y || !(x == null || y == null) && x.equals(y);
}
@Override
public int hashCode(Object x) throws HibernateException {
return x.hashCode();
}
@Override
public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
throws HibernateException, SQLException {
Timestamp timestamp = rs.getTimestamp(names[0]);
if (rs.wasNull()) {
return null;
}
return new Date(timestamp.getTime());
}
@Override
public void nullSafeSet(PreparedStatement st, Object value, int index)
throws HibernateException, SQLException {
if (value == null) {
st.setNull(index, Types.TIMESTAMP);
}
else {
Date date = (Date) value;
Timestamp timestamp = new Timestamp(date.getTime());
st.setTimestamp(index, timestamp);
}
}
@Override
public Object deepCopy(Object value) throws HibernateException {
return value;
}
@Override
public boolean isMutable() {
return false;
}
@Override
public Serializable disassemble(Object value) throws HibernateException {
return (Serializable) value;
}
@Override
public Object assemble(Serializable cached, Object owner)
throws HibernateException {
return cached;
}
@Override
public Object replace(Object original, Object target, Object owner)
throws HibernateException {
return original;
}
}
Here is the way to use it in the Person class.
@Entity
@TypeDefs({
@TypeDef(name="dateTimeUserType", typeClass=DateTimeUserType.class)
})
public class Person {
...
@Column
@Type(type="date")
private Date birthDate;
@Column
@Type(type="dateTimeUserType")
private Date createDate;
@Column
@Type(type="dateTimeUserType")
private Date modifyDate;
...
Either of these solutions will prevent the 'equals' issue that we encountered before. This issue is not a Hibernate issue it is truly a Java issue as is stated in Effective Java (2nd Edition). Hibernate can be challenging enough without these subtle issues that are easy to miss until a bug is reported.
Here is the complete code. Hibernate Logging Zip .