JPA 2.1: 引入Java 8的日期/时间API

11

我想为我的启用了JPA的应用程序 添加对Java 8日期/时间API(JSR-310)的支持。

很明显,JPA 2.1不支持Java 8日期/时间API
作为一种解决方法,最常见的建议是使用AttributeConverter

在我的现有应用程序中,我更改了实体以使用LocalDate/LocalDateTime类型进行列映射,并为它们添加了用于java.util.Date的旧setter/getters。
我创建了相应的AttributeConverter类。

我的应用程序在使用Query.setParameter()时,传入java.util.Date实例后失败了(在转换到新API之前是可以工作的)。看起来JPA期望新的日期类型,并且不会即时转换。我原本期望如果将一个已注册AttributeConverter的类型作为参数传递给setParameter(),它会被转换器自动转换。但这似乎并非如此,至少在使用EclipseLink 2.6.2时不是这样的。
java.lang.IllegalArgumentException: You have attempted to set a value of type class java.util.Date for parameter closeDate with expected type of class java.time.LocalDate from query string SELECT obj FROM [...]
    at org.eclipse.persistence.internal.jpa.QueryImpl.setParameterInternal(QueryImpl.java:937) ~[eclipselink-2.6.2.jar:2.6.2.v20151217-774c696]
    at org.eclipse.persistence.internal.jpa.EJBQueryImpl.setParameter(EJBQueryImpl.java:593) ~[eclipselink-2.6.2.jar:2.6.2.v20151217-774c696]
    at org.eclipse.persistence.internal.jpa.EJBQueryImpl.setParameter(EJBQueryImpl.java:1) ~[eclipselink-2.6.2.jar:2.6.2.v20151217-774c696]
  [...]


问题:

  1. 这种行为是否是预期的?我有遗漏什么吗?
  2. 有没有一种方法可以使用新日期类型作为字段,而不会破坏现有代码?
  3. 您如何处理在JPA中转换到新的日期/时间API?


更新:

然而,至少使用EclipseLink时,存在自定义类型的AttributeConverter,并不完全受支持:

在JPQL查询中,实际字段类型和转换后的数据库类型都不能用作参数。 当使用转换后的数据库类型时,会出现上述异常。 当使用实际字段类型(例如LocalDate)时,它会直接传递给不知道此类型的jdbc驱动程序:

Caused by: java.sql.SQLException: Invalid column type
    at oracle.jdbc.driver.OraclePreparedStatement.setObjectCritical(OraclePreparedStatement.java:10495) 
    at oracle.jdbc.driver.OraclePreparedStatement.setObjectInternal(OraclePreparedStatement.java:9974) 
    at oracle.jdbc.driver.OraclePreparedStatement.setObjectInternal(OraclePreparedStatement.java:10799) 
    at oracle.jdbc.driver.OraclePreparedStatement.setObject(OraclePreparedStatement.java:10776) 
    at oracle.jdbc.driver.OraclePreparedStatementWrapper.setObject(OraclePreparedStatementWrapper.java:241)
    at org.eclipse.persistence.internal.databaseaccess.DatabasePlatform.setParameterValueInDatabaseCall(DatabasePlatform.java:2506)

我希望EclipseLink使用AttributeConverter将字段类型转换为java.sql类型。(请参见此错误报告: https://bugs.eclipse.org/bugs/show_bug.cgi?id=494999)
这导致了最重要的问题#4:
4.是否有解决方案来支持使用EclipseLink的java 8日期字段,包括在此类字段上使用查询参数的可能性附加信息

我认为现在你需要添加另一个依赖项以获得Hibernate中的Java 8支持。这将消除您在Data / Time类周围指定AttributeConverters的需要。请参见:http://stackoverflow.com/a/37098160/1356423 - Alan Hay
@SteffenHarbich 这使我更进了一步 - 谢谢。似乎 AttributeConverter 的目标类型需要是一个 java.sql 类型(尽管 java.util.Date 作为实体字段完美地工作)。现在简单的查询可以工作了。但是,当在查询中多次使用同一个参数时,EclipseLink 似乎不会在第一次出现后转换该值,从而导致上述异常。将您的查询调整为 "SELECT p FROM MeasuringPoint p WHERE p.when = :custDate AND p.when = :custDate" 可以重现这个问题。 - MRalwasser
@SteffenHarbich,你能否尝试查询语句 SELECT p FROM MeasuringPoint p WHERE p.when = :custDate AND p.when -1 = :custDate - MRalwasser
“-1”在查询中的意图是什么?我猜测这不是有效的JPQL。 - Steffen Harbich
在与参数进行比较之前,它应该从p.when中减去一天。但是-1并不重要。任何使用:custDate的JPQL(在第一次使用的不同表达式中)都应该重现异常。 - MRalwasser
显示剩余7条评论
4个回答

4

不久前,我将一个Java EE 7 Web应用程序从Java 7转换为Java 8,并在实体中将java.util.Date替换为LocalDateLocalDateTime

  1. 是的,这种行为是可以预期的,因为AttributeConverter只能在实体字段类型和数据库类型(即java.sql.Date等)之间进行转换;它不能在实体字段类型和查询参数中使用的java.util.Date之间进行转换。
  2. 据我所知,在引入java.time类型到JPA实体后,没有继续使用现有代码中的java.util.Date的方法。
  3. 除了创建必要的AttributeConverter实现外,我还将所有java.util.Date的出现都更改为适当的java.time类型,不仅包括实体中,还包括JPA-QL查询和业务方法。

对于第2项,当然您可以通过使用实用程序方法和getter/setter在java.utiljava.time之间进行转换,但这并不能完全解决问题。更重要的是,如果您不愿意转换使用这些属性的其余代码,那么我不太明白为什么要将java.time类型引入JPA实体属性。在我对该Java EE应用程序进行的转换工作之后,没有任何地方还使用了java.util.Date(尽管我还必须为JSF创建转换器)。


关于1:我已经更新了问题,引用了JPA 2.1规范。 - MRalwasser
我所说的是一个相当大的代码库。我希望能够一次性将其迁移到新的API上,但不幸的是,预算和时间目前不允许这样做。我想在实体中支持新的API,这将鼓励我们的团队在未来的任务和现有代码重构中使用它。在某个盈亏平衡点上,我们将完成完整的过渡。 - MRalwasser
1
JDBC 驱动程序仅了解 java.sql 类型,而不了解 java.util.Date。因此,JPA 转换器仅在 java.timejava.sql 类型之间进行转换(至少在我的情况下是这样的);它们不会将 java.time.LocalDate 转换为 java.util.Date,再转换为 java.sql.Date(然后被映射到本机数据库 DATE 类型)。 - Rogério
我认为JDBC在我的问题中根本没有发挥作用。多年来,我们一直在使用java.util.Date字段作为JPA实体的一部分,EclipseLink负责与JDBC驱动程序进行通信(并将其转换为java.sql.Date)。我的问题只涉及AttributeConverter,JPQL中的(预期)转换以及我的方法的替代方案。 - MRalwasser
关于2:至少在使用eclipselink时,将java.time类型用作查询参数也会失败(请参见https://bugs.eclipse.org/bugs/show_bug.cgi?id=494999)。因此,我既不能使用java.util.Date也不能使用新类型作为查询参数。您是如何解决这个问题的? - MRalwasser

2
由于提供程序本身存在如此多的错误,我认为你没有太多选择,只能在映射级别上使用java.util.Date,在 API 级别上使用 Java 8 日期。
假设你编写了一个用于转换到/从java.util日期的实用类叫做DateUtils,你可以定义你的映射如下:
@Entity
public class MyEntity {

  @Column("DATE")
  private Date date; // java.util.Date

  public void setDate(LocalDateTime date) {
    this.date = DateUtils.convertToDate(date);
  }

  public LocalDateTime getDate() {
    return DateUtils.convertFromDate(date);
  }
}

然后在JPQL中按日期筛选:
public List<MyEntity> readByDateGreaterThan(LocalDateTime date) {
  Query query = em.createQuery("select e from MyEntity e where e.date > :date");
  query.setParameter("date", DateTuils.convertToDate(date));
  return query.getResultList();
}

因此,java.util 日期将在实体和 DAO(存储库)内部使用,而实体和 DAO 公开的 API 将采用/返回 java 8 日期,从而使应用程序的其余部分仅使用 java 8 日期。

这大致就是我们今天要做的事情。这种方法的一个缺点是 JPQL 参数需要事先转换为 java.util 类型。 - MRalwasser

2

我有以下设置:

  • EclipseLink v2.6.2
  • h2 Database v1.4.191
  • Java 8

实体类如下:

@Entity
public class MeasuringPoint extends BaseEntity {

    @Column(nullable = false)
    private LocalDateTime when;

    public void setWhen(LocalDateTime when) {
        this.when = when;
    }

    public LocalDateTime getWhen() {
        return when;
    }

}

所需的 JPA 2.1 转换器为:

@Converter(autoApply = true)
public class LocalDateTimeConverter implements AttributeConverter<LocalDateTime, Timestamp> {

    @Override
    public Timestamp convertToDatabaseColumn(LocalDateTime attribute) {
        return attribute == null ? null : Timestamp.valueOf(attribute);
    }

    @Override
    public LocalDateTime convertToEntityAttribute(Timestamp dbData) {
        return dbData == null ? null : dbData.toLocalDateTime();
    }

}

现在我可以进行查询。
List<?> result = em.createQuery(
         "SELECT p FROM MeasuringPoint p WHERE p.when = :custDate")
         .setParameter("custDate", LocalDateTime.now())
         .getResultList();

并且它的运行非常顺利。结果包含了预期的实体。自动完成到时间戳的转换。当您使用Criteria API查询时,请查看这个答案,其中展示了如何在Criteria API查询中使用LocalDateTime
我想知道为什么它在您的代码中不起作用。可能是H2 JDBC驱动程序支持一些您的Oracle不支持的功能。

0

AttributeConverter的设计是正常工作的,因为转换器旨在处理实体类型和数据库类型之间的来回转换。验证正在验证参数类型是否与实体内部的类型不匹配 - 仅因为您的属性转换器可以处理它,这并不意味着它符合属性转换器类型的契约。JPA只说它会在进入数据库之前通过转换器进行转换,在这种情况下它不会进入数据库。

如果您不喜欢Rogério的建议,您可以 1)修改EclipseLink代码以放宽验证,以允许其通过到您的转换器,或 2)将属性类型更改为“Object”,以便您可能传递的所有参数类型都将进入您的转换器。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接