16

I have a problem about the management of dates with milliseconds. I understand the need to use the TIMESTAMP to store milliseconds:

@Temporal(TIMESTAMP)
@Column(name="DATE_COLUMN", nullable = false)
@Override public java.util.Date getDate() { return this.date; }

But if I can't compare this date to another instance of java.util.Date, unless I pay attention to the order of equals() call, because this.date instance is a java.sql.Timestamp. How to get a java.util.Date from JPA ? Because the date that comes from JPA, even if the method signature is a java.util.Date is actually an instance of java.sql.Timestamp.

java.util.Date newDate = new Date(this.date.getTime());
this.date.equals(newDate) == false
newDate.equals(this.date) == true

I've try to modify my method in the persistence class:

@Override
public Date getDate() {
  return this.date == null ? null : new Date(this.date.getTime());
}

It's working, but it's not efficient with lots of data.

There are other options :

  • I could modify the design of my persistence class, using @PostLoad in order to create a java.util.Date from the persited date after I retrieve it.

  • I wonder if I can not get a result using a ClassTransformer?

Have you ever been confronted with this problem? What I do not correctly? What is the best way to handle this problem?

Michael Eakins
  • 4,149
  • 3
  • 35
  • 54
chepseskaf
  • 664
  • 2
  • 12
  • 41
  • I just had a very nasty version of this problem where this screwed up Spring Security's salt comparison. Apparently using a sql.timestamp date version for salt renders a different result than using a util.Date – Marc Aug 01 '13 at 12:17

5 Answers5

14

TBH, I'm not sure of the exact status of this but there might indeed be a problem with the way Hibernate (which is your JPA provider, right?) handles TIMESTAMP columns.

To map a SQL TIMESTAMP to a java.util.Date, Hibernate uses the TimestampType which will actually assign a java.sql.Timestamp to your java.util.Date attribute. And while this is "legal", the problem is that Timestamp.equals(Object) is not symmetric (why on earth?!) and this breaks the semantics of Date.equals(Object).

As a consequence, you can't "blindly" use myDate.equals(someRealJavaUtilDate) if myDate is mapped to a SQL TIMESTAMP, which is of course not really acceptable.

But although this has been extensively discussed on the Hibernate forums, e.g. in this thread and this one (read all pages), it seems that Hibernate users and developers never agreed on the problem (see issues like HB-681) and I just don't understand why.

Maybe it's just me, maybe I just missing something simple for others, but the problem looks obvious to me and while I consider this stupid java.sql.Timestamp to be the culprit, I still think that Hibernate should shield users from this issue. I don't understand why Gavin didn't agree on this.

My suggestion would be to create a test case demonstrating the issue (should be pretty simple) and to report the problem (again) to see if you get more positive feedback from the current team.

Meanwhile, you could use a custom type to "fix" the problem yourself, using something like this (taken from the forum and pasted as is):

public class TimeMillisType extends org.hibernate.type.TimestampType {

    public Date get(ResultSet rs, String name) throws SQLException {
        Timestamp timestamp = rs.getTimestamp(name);
        if (timestamp == null) return null;
        return
            new Date(timestamp.getTime()+timestamp.getNanos()/1000000);
   }

}
Pascal Thivent
  • 562,542
  • 136
  • 1,062
  • 1,124
8

java.sql.Timestamp overrides the compareTo(Date) method, so it should be no problem using compareTo(..)

In short - java.util.Date and java.sql.Timestamp are mutually comparable.

Furthermore, you can always compare the date.getTime(), rather than the objects themselves.

And even further - you can use a long field to store the date. Or even a DateTime (from joda-time)

Bozho
  • 588,226
  • 146
  • 1,060
  • 1,140
  • Date that comes from persistence is used by another framework that cannot be modified (too many works). – chepseskaf Oct 25 '10 at 12:30
  • Because I can't modify the comparison process, I need to provide a java.util.Date to the main system. I would like to know how to convert a date effectively ? – chepseskaf Oct 25 '10 at 12:55
  • why do you need to? java.util.Date and java.sql.Timestamp are mutually comparable. – Bozho Oct 25 '10 at 13:16
  • Look at my test, the result of the method depends on the order of call, and I did not touch this order. I need only a real java.util.Date. – chepseskaf Oct 25 '10 at 13:29
  • using `compareTo(..)` should work. Apart from that, you can always compare their `getTime()` rather than the objects. – Bozho Oct 25 '10 at 13:32
  • I understand your point of view, I think now we should not use the method equals() workjing with Date when it comes to comparing the concept of Time. – chepseskaf Oct 27 '10 at 11:56
4

In my experience you don't want the java.sql.Timestamp out into your logic - it creates a lot of strange errors just as you pointed out, and it does not get any better if your application does serialization.

If it works with the override that returns a new java.util.Date then go for that one. Or even better, go for JodaTime. You'll find lots of examples out on the net doing that. I would not worry about performance here as your database is in magnitude more slow than the creation of a new java.util.Date object.

EDIT: I see that you are using Hibernate. If you use annotations you can do:

@Type(type = "org.joda.time.contrib.hibernate.PersistentDateTime")
public DateTime getProvisionByTime() {
    return provisionByTime;
}

Then you will get nice DateTime objects from Jodatime in your persistent objects. If you want to only have a date, you can use LocalDate like this:

@Type(type = "org.joda.time.contrib.hibernate.PersistentLocalDate")
public LocalDate getCloudExpireDate() {
    return cloudExpireDate;
}

IF you use maven, the following dependencies should get this set up for you (you might need to update the hibernate versions)

    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate</artifactId>
        <version>3.2.6.ga</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-annotations</artifactId>
        <version>3.3.1.GA</version>
    </dependency>

    <dependency>
        <groupId>joda-time</groupId>
        <artifactId>joda-time-hibernate</artifactId>
        <version>1.2</version>
    </dependency>
    <dependency>
        <groupId>joda-time</groupId>
        <artifactId>joda-time</artifactId>
            <version>1.6.1</version>
    </dependency>
Knubo
  • 8,333
  • 4
  • 19
  • 25
  • Precisely, the java.sql.Timestamp is imposed by JPA. I wish I could do without, but the mapping is provided by JPA... – chepseskaf Oct 25 '10 at 12:34
  • As you are using hibernate, I guess you could try the solution I provided above. – Knubo Oct 26 '10 at 18:43
3

The problem is critical for DAO tests:

Employer employer1 = new Employer();
employer1.setName("namenamenamenamenamename");
employer1.setRegistered(new Date(1111111111)); // <- Date field

entityManager.persist(employer1);
assertNotNull(employer1.getId());

entityManager.flush();
entityManager.clear();

Employer employer2 = entityManager.find(Employer.class, employer1.getId());
assertNotNull(employer2);
assertEquals(employer1, employer2); // <- works
assertEquals(employer2, employer1); // <- fails !!!

So the result is really surprising and writing tests became tricky.

But in the real business logic you will never use entity as a set/map key because it is huge and it is mutable. And you will never compare time values by equal comparison. And comparing whole entities should be avoided too.

The usual scenario uses immutable entity ID for map/set key and compares time values with compareTo() method or just using getTime() values.

But making tests is a pain so I implemented my own type handlers

http://pastebin.com/7TgtEd3x

http://pastebin.com/DMrxzUEV

And I have overridden the dialect I use:

package xxx;

import org.hibernate.dialect.HSQLDialect;
import org.hibernate.type.AdaptedImmutableType;
import xxx.DateTimestampType;

import java.util.Date;

public class CustomHSQLDialect extends HSQLDialect {

    public CustomHSQLDialect() {
        addTypeOverride(DateTimestampType.INSTANCE);
        addTypeOverride(new AdaptedImmutableType<Date>(DateTimestampType.INSTANCE));
    }
}

I haven't decided yet - would I use this approach both for tests and production or for tests only.

Lokesh Mehra
  • 545
  • 6
  • 16
Nick Mazurkin
  • 731
  • 5
  • 7
1

JPA should return a java.util.Date for an attribute of type java.util.Date, the @Temporal(TIMESTAMP) annotation should only affect how the date is stored. You should not get a java.sql.Timestamp back.

What JPA provider are you using? Have you tried this in EclipseLink, the JPA reference implementation?

James
  • 17,965
  • 11
  • 91
  • 146