Nathan Johns Nathan Johns - 4 months ago 18
Java Question

Hibernate UserType nullSafeSet - how to know if called for insert/update or select

I have a custom UserType which stores a date/time value in a TIMESTAMP field and is updated to the current time 'in UTC' when the record is inserted or updated. (This field is not used for versioning or for id purposes.)

The problem is that this works very nicely but if you need to run a query where this field is one of the criteria when the prepared statement is built, the custom user types nullSafeSet is called which sets the value to current time so the query always has that condition set to the current time which doesn't produce the desired results.

Is is possible to make nullSafeSet aware of the context in which it is being called so it can adjust it's behaviour for insert/update or a select. Or perhaps there is another way to do this?

I have tried using a <timestamp ... /> but it doesn't write the value in UTC. (The backing database is Derby which doesn't deal with time zones by itself very well.) I have also tried just updating the value in the replace(...) method, but this only works if the entity is detached from the session and then brought back. From some of the code the entity is retrieved and updated all within a single session context so the field doesn't get updated.

Is it possible to still have a custom user type write the value in UTC but use some sort of generator to get the current date/time only on insert and update?


package example;

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Properties;
import java.util.TimeZone;

import org.hibernate.HibernateException;
import org.hibernate.usertype.ParameterizedType;
import org.hibernate.usertype.UserType;

/**
* The class
DateTimestampUserType
implements a Hibernate
*
UserType
to allow the persistence of Date
* instances as a TIMESTAMP in the database.
*
* All
Date
instances persisted are in UTC/GMT time.
*
* This
UserType
implementation also allows for a boolean parameter
* 'updateToNow' specified as part of Hibernate configuration. This parameter
* controls whether when a
Date
needs to be persisted it is updated
* to the current time. This is useful for fields like TimeModified as it
* requires no intervention by the calling code.
*/
public class DateTimestampUserType implements UserType, ParameterizedType {

/* Constants */
private static final String UTC_TZ = "GMT";

/* Member Variables */
private boolean m_updateToNow = false;

/* Methods */
/* (non-Javadoc)
* @see org.hibernate.usertype.ParameterizedType#setParameterValues(java.util.Properties)
*/
public void setParameterValues(Properties parameters) {
if (parameters != null && parameters.containsKey("updateToNow")) {
m_updateToNow = Boolean.parseBoolean(parameters.getProperty("updateToNow"));
}
}

/* (non-Javadoc)
* @see org.hibernate.usertype.UserType#assemble(java.io.Serializable, java.lang.Object)
*/
public Object assemble(Serializable cached, Object owner) throws HibernateException {
return cached;
}

/* (non-Javadoc)
* @see org.hibernate.usertype.UserType#deepCopy(java.lang.Object)
*/
public Object deepCopy(Object object) throws HibernateException {
if (object == null) return null;
return new Date(((Date)object).getTime());
}

/* (non-Javadoc)
* @see org.hibernate.usertype.UserType#disassemble(java.lang.Object)
*/
public Serializable disassemble(Object value) throws HibernateException {
return (Serializable) value;
}

/* (non-Javadoc)
* @see org.hibernate.usertype.UserType#equals(java.lang.Object, java.lang.Object)
*/
public boolean equals(Object x, Object y) throws HibernateException {
if (x == y) return true;
if (x == null || y == null) return false;
return x.equals(y);
}

/* (non-Javadoc)
* @see org.hibernate.usertype.UserType#hashCode(java.lang.Object)
*/
public int hashCode(Object object) throws HibernateException {
return object.hashCode();
}

/* (non-Javadoc)
* @see org.hibernate.usertype.UserType#isMutable()
*/
public boolean isMutable() {
return true;
}

/* (non-Javadoc)
* @see org.hibernate.usertype.UserType#nullSafeGet(java.sql.ResultSet, java.lang.String[], java.lang.Object)
*/
public Object nullSafeGet(ResultSet resultSet, String[] names, Object owner) throws HibernateException, SQLException {
Date result = null;
Calendar cal = new GregorianCalendar(TimeZone.getTimeZone(UTC_TZ));
Timestamp timeStamp = resultSet.getTimestamp(names[0], cal);

result = new Date(timeStamp.getTime());

return result;
}

/* (non-Javadoc)
* @see org.hibernate.usertype.UserType#nullSafeSet(java.sql.PreparedStatement, java.lang.Object, int)
*/
public void nullSafeSet(PreparedStatement statement, Object value, int index) throws HibernateException, SQLException {
if (m_updateToNow) {
value = new Date();
}

Calendar cal = new GregorianCalendar(TimeZone.getTimeZone(UTC_TZ));
Timestamp x = new Timestamp(((Date)value).getTime());
statement.setTimestamp(index, x, cal);
}

/* (non-Javadoc)
* @see org.hibernate.usertype.UserType#replace(java.lang.Object, java.lang.Object, java.lang.Object)
*/
public Object replace(Object original, Object target, Object owner) throws HibernateException {
if (m_updateToNow) {
return new Date();
} else {
return original;
}
}

/* (non-Javadoc)
* @see org.hibernate.usertype.UserType#returnedClass()
*/
public Class returnedClass() {
return Date.class;
}

/* (non-Javadoc)
* @see org.hibernate.usertype.UserType#sqlTypes()
*/
public int[] sqlTypes() {
return new int[] { java.sql.Types.TIMESTAMP };
}
}

Answer

nullSafeSet() is invoked both when the entity is being saved / updated and when query parameter has to be set.

Your problem lies with "updateToNow" flag; if it's set to true in your mapping for given entity, you'll always be overwriting the values with the current timestamp. Remove that and you'll be fine.

If you're always setting the timestamp to current (for given column), consider doing so in the database and mapping your property as "generated" instead.

Comments