Как использовать java.sql.Timestamp как реальный java.util.Date с JPA

У меня проблема с управлением датами с миллисекундами. Я понимаю необходимость использования TIMESTAMP для хранения миллисекунд:

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

Но если я не могу сравнить эту дату с другим экземпляром java.util.Date, если я не обращу внимание на порядок вызова equals(), потому что экземпляр this.date - это java.sql.Timestamp. Как получить java.util.Date из JPA? Поскольку дата, которая исходит из JPA, даже если подпись метода является java.util.Date, фактически является экземпляром java.sql.Timestamp.

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

Я пытаюсь изменить свой метод в классе persistence:

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

Он работает, но он не эффективен с большим количеством данных.

Существуют и другие варианты:

  • Я мог бы изменить дизайн моего класса persistence, используя @PostLoad, чтобы создать java.util.Date из оставшейся даты после его получения.

  • Интересно, не могу ли я получить результат с помощью ClassTransformer?

Вы когда-нибудь сталкивались с этой проблемой? Что я не правильно? Каков наилучший способ справиться с этой проблемой?

Ответы

Ответ 1

java.sql.Timestamp переопределяет метод compareTo(Date), поэтому без проблем следует использовать compareTo(..)

Короче - java.util.Date и java.sql.Timestamp взаимно сопоставимы.

Кроме того, вы всегда можете сравнить date.getTime(), а не сами объекты.

И еще дальше - вы можете использовать поле long для хранения даты. Или даже a DateTime (от joda-time)

Ответ 2

TBH, я не уверен в точном статусе этого, но действительно может возникнуть проблема с тем, как Hibernate (который является вашим провайдером JPA, правильно?) обрабатывает столбцы TIMESTAMP.

Чтобы сопоставить SQL TIMESTAMP с java.util.Date, Hibernate использует TimestampType, который фактически назначит java.sql.Timestamp, чтобы ваш атрибут java.util.Date. И хотя это "законно", проблема в том, что Timestamp.equals(Object) несимметричный (почему на земле?!), и это нарушает семантику Date.equals(Object).

Как следствие, вы не можете "слепо" использовать myDate.equals(someRealJavaUtilDate), если myDate сопоставляется с SQL TIMESTAMP, что, конечно, не очень приемлемо.

Но хотя это широко обсуждалось на форумах Hibernate, например. в этот поток и этот (прочитайте все страницы), кажется, что пользователи и разработчики Hibernate никогда не соглашались с проблемой (см. проблемы, такие как HB-681), и я просто не понимаю, почему.

Возможно, это только я, может быть, я просто пропустил что-то простое для других, но проблема выглядит очевидной для меня, и хотя я считаю эту глупость java.sql.Timestamp Чтобы быть виновником, я все еще думаю, что Hibernate должен защищать пользователей от этой проблемы. Я не понимаю, почему Гэвин не согласился с этим.

Мое предложение было бы создать тестовый пример, демонстрирующий проблему (должно быть довольно просто), и сообщить о проблеме (снова), чтобы узнать, получаете ли вы более положительные отзывы от текущей команды.

Между тем, вы можете использовать собственный тип, чтобы "исправить" проблему самостоятельно, используя что-то вроде этого (взятое с форума и вставленное как есть):

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);
   }

}

Ответ 3

По моему опыту вы не хотите, чтобы java.sql.Timestamp выходил из вашей логики - он создает множество странных ошибок, как вы указали, и не становится лучше, если ваше приложение выполняет сериализацию.

Если он работает с переопределением, которое возвращает новый java.util.Date, то перейдите к нему. Или еще лучше, отправляйтесь на JodaTime. Вы найдете множество примеров в сети, которые делают это. Я бы не стал беспокоиться о производительности здесь, так как ваша база данных по величине медленнее, чем создание нового объекта java.util.Date.

EDIT: Я вижу, что вы используете Hibernate. Если вы используете аннотации, вы можете сделать:

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

Затем вы получите красивые объекты DateTime из Jodatime в ваших постоянных объектах. Если вы хотите иметь только дату, вы можете использовать LocalDate следующим образом:

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

ЕСЛИ вы используете maven, следующие настройки должны быть настроены для вас (вам может потребоваться обновить версии спящего режима)

    <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>

Ответ 4

Проблема имеет решающее значение для тестов DAO:

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 !!!

Итак, результат действительно удивителен, и писать тесты стало сложно.

Но в реальной бизнес-логике вы никогда не будете использовать объект как ключ набора/карты, потому что он огромен и он изменен. И вы никогда не сравните значения времени при сравнении equal. Кроме того, следует избегать сравнения целых объектов.

Обычный сценарий использует неизменяемый идентификатор объекта для ключа map/set и сравнивает значения времени с методом compareTo() или просто использует значения getTime().

Но делать тесты - это боль, поэтому я реализовал свои собственные обработчики типов.

http://pastebin.com/7TgtEd3x

http://pastebin.com/DMrxzUEV

И я переопределил диалект, который я использую:

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));
    }
}

Я еще не решил - использовал бы этот подход как для тестов, так и для производства или только для тестов.

Ответ 5

JPA должна вернуть java.util.Date для атрибута типа java.util.Date, аннотация @Temporal (TIMESTAMP) должна влиять только на то, как дата сохраняется. Вам не нужно возвращать java.sql.Timestamp.

Какой поставщик JPA вы используете? Вы пробовали это в EclipseLink, эталонной реализации JPA?