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 .

7 comments

  1. Nice topic David.

    I faced this problem before, but I was searching for a solutions instead of understanding the cause or the issue deeply.

    My solution was just to add the following annotation:

    @Temporal(TemporalType.DATE)
    @Column(name=”CREATION_DATE”)
    private java.util.Date date;

    Temporal is a JPA annotation that convert back and forth between time-stamp and java util date

  2. Great point. ‘@Temporal(TemporalType.DATE)’ is equivalent to ‘@Type(type=”date”)’. See the test below.

    	@Test
    	@Transactional
    	public void testSave(){
    		TemporalPerson person = new TemporalPerson("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<TemporalPerson> people = findAll();
     
    		assertEquals(1, people.size());
    		TemporalPerson foundPerson = people.get(0);
     
    		assertTrue(foundPerson.getBirthDate() instanceof Date);
    		assertTrue(foundPerson.getCreateDate() instanceof Date);
    		assertTrue(foundPerson.getModifyDate() instanceof Date);
     
    		assertFalse(foundPerson.getBirthDate().equals(birthDateWithTime));
    		assertTrue(foundPerson.getBirthDate().equals(birthDateWithoutTime));
    		assertFalse(foundPerson.getCreateDate().equals(createDate));
    		assertFalse(foundPerson.getModifyDate().equals(modifyDate));
    	}

    ‘@Temporal(TemporalType.DATE)’ drops the time value and only preserves the date. This makes the last two equals() evaluations false. In the case of Person we want the time preserved for the create and modify date. @Temporal is very useful just not in the case of values that need to preserve time information.

Comments are closed.