题 如何强制Hibernate以java.util.Date而不是Timestamp的形式返回日期?


情况

我有一个带有java.util.Date类型变量的persistable类:

import java.util.Date;

@Entity
@Table(name = "prd_period")
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class Period extends ManagedEntity implements Interval {

   @Column(name = "startdate_", nullable = false)
   private Date startDate;

}

DB中的对应表:

CREATE TABLE 'prd_period' (
  'id_' bigint(20) NOT NULL AUTO_INCREMENT,
 ...
  'startdate_' datetime NOT NULL
)

然后我将Period对象保存到DB:

Period p = new Period();
Date d = new Date();
p.setStartDate();
myDao.save(p);

之后如果我试图从DB中提取我的对象,则返回时间戳类型的变量startDate - 我尝试使用的所有地方等于(...)返回false。

:有没有办法强制Hibernate返回日期作为java.util.Date类型的对象而不是Timestamp而不对每个这样的变量进行显式修改(例如,它必须能够正常工作,而不需要对java.util的现有变量进行显式修改。日期类型)?

注意:

我找到了许多显式解决方案,其中使用了注释或修改了setter - 但我有许多带有Date变量的类 - 所以我需要一些集中式解决方案,所有下面描述的都不够好:

  1. 使用注释@Type:  - 将返回java.sql.Date

    @Column
    @Type(type="date")
    private Date startDate;
    
  2. 使用注释@Temporal(TemporalType.DATE)  - 将返回java.sql.Date

    @Temporal(TemporalType.DATE)
    @Column(name=”CREATION_DATE”)
    private Date startDate;
    
  3. 通过修改setter(深拷贝)  - 将返回java.util.Date

    public void setStartDate(Date startDate) {
        if (startDate != null) {
            this.startDate = new Date(startDate.getTime());
        } else {
            this.startDate = null;
        }
    }
    
  4. 通过创建我自己的类型:  - 将返回java.util.Date

详情如下: http://blogs.sourceallies.com/2012/02/hibernate-date-vs-timestamp/


49
2018-03-02 13:16


起源


我认为你应该使用你刚刚展示的一个明确答案。主要用于未来的代码维护。它将允许新开发人员准确地查看您正在做的事情,而不是花费大量时间来弄清楚为什么系统中的日期与其他所有hibernate系统的工作方式不同。 - Zoidberg
第二种解决方案对我来说最好。此外,从您的博客帖子看来,您依靠hibernate来创建数据库结构。如果你把它放在你的控制中并且只是使用它会怎么样 date 代替 timestamp 到处? - Denis Tulskiy
+1这确实是一个令人讨厌的问题。我正在使用Hibernate多年,我不知道 任何 关于此的官方Hibernate文档。我正在使用 修改后的二传手 做法。我也在getter中做了深刻的复制,因为 java.util.Date 不是不可变的,所以无论如何都应该首选深层副本。 - tscho
请注意,如果您使用字段访问而不是属性访问,则setter方法将不会执行任何操作。 - JB Nizet
如果更改Java类是一个选项,那么最好的解决方案就是没有 java.util.Date 完全和使用 乔达时间 代替。还有一个特别的 Hibernate支持项目 或者更一般化 Usertype项目 可与Joda-Time一起使用。 - tscho


答案:


所以,我花了一些时间来解决这个问题并找到了解决方案。它不是一个漂亮的,但至少是一个起点 - 也许有人会补充一些有用的评论。

有关我在进程中找到的映射的一些信息:

  • 包含Hibernate类型到属性类型的基本映射的类是org.hibernate.type.TypeFactory。所有这些映射都存储在不可修改的映射中

    private static final Map BASIC_TYPES;
    ...
    basics.put( java.util.Date.class.getName(), Hibernate.TIMESTAMP );
    ...
    BASIC_TYPES = Collections.unmodifiableMap( basics );
    

正如您所见,java.util.Date类型与Hibernate类型相关联org.hibernate.type.TimestampType

  • 下一个有趣的时刻 - 创建Hibernate org.hibernate.cfg.Configuration - 包含有关映射类的所有信息的对象。可以像这样提取这些类及其属性:

    Iterator clsMappings = cfg.getClassMappings();
    while(clsMappings.hasNext()){
        PersistentClass mapping = (PersistentClass) clsMappings.next();
        handleProperties(mapping.getPropertyIterator(), map);
    }
    
  • 绝大多数属性是org.hibernate.mapping.SimpleValue类型的对象。我们感兴趣的是SimpleValue.getType()方法 - 在此方法中定义了在使用DB时将使用哪种类型来回转换属性值

    Type result = TypeFactory.heuristicType(typeName, typeParameters);
    

在这一点上,我理解我无法修改BASIC_TYPES - 所以唯一的方法 - 将SimpleValue对象替换为java.util.Date类型的属性到我的自定义对象,它将能够知道要转换的确切类型。

解决方案:

  • 通过扩展HibernatePersistence类并重写其方法createContainerEntityManagerFactory来创建自定义容器实体管理器工厂:

    public class HibernatePersistenceExtensions extends HibernatePersistence {
    
        @Override
        public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info, Map map) {
    
            if ("true".equals(map.get("hibernate.use.custom.entity.manager.factory"))) {
                return CustomeEntityManagerFactoryFactory.createCustomEntityManagerFactory(info, map);
            } else {
                return super.createContainerEntityManagerFactory(info, map);
            }
        }
    }
    
  • 创建Hibernate配置对象,修改java.util.Date属性的值ojects,然后创建自定义实体管理器工厂。

    public class ReattachingEntityManagerFactoryFactory {
    
    
        @SuppressWarnings("rawtypes")
        public static EntityManagerFactory createContainerEntityManagerFactory(
        PersistenceUnitInfo info, Map map) {
            Ejb3Configuration cfg = new Ejb3Configuration();
    
            Ejb3Configuration configured = cfg.configure( info, map );
    
            handleClassMappings(cfg, map);
    
            return configured != null ? configured.buildEntityManagerFactory() : null;
        }
    
        @SuppressWarnings("rawtypes")
        private static void handleClassMappings(Ejb3Configuration cfg, Map map) {
            Iterator clsMappings = cfg.getClassMappings();
            while(clsMappings.hasNext()){
                 PersistentClass mapping = (PersistentClass) clsMappings.next();
                 handleProperties(mapping.getPropertyIterator(), map);
            }
        } 
    
    
    
        private static void handleProperties(Iterator props, Map map) {
    
            while(props.hasNext()){
                 Property prop = (Property) props.next();
                 Value value = prop.getValue();
                 if (value instanceof Component) {
                     Component c = (Component) value;
                     handleProperties(c.getPropertyIterator(), map);
                 } else {
    
                     handleReturnUtilDateInsteadOfTimestamp(prop, map);
    
                 }
             }
    
        private static void handleReturnUtilDateInsteadOfTimestamp(Property prop, Map map) {
            if ("true".equals(map.get("hibernate.return.date.instead.of.timestamp"))) {
                Value value = prop.getValue();
    
                if (value instanceof SimpleValue) {
                    SimpleValue simpleValue = (SimpleValue) value;
                    String typeName = simpleValue.getTypeName();
                    if ("java.util.Date".equals(typeName)) {
                        UtilDateSimpleValue udsv = new UtilDateSimpleValue(simpleValue);
                        prop.setValue(udsv);
                    }
                }
            }
        }
    
    }
    

正如您所看到的,我只是迭代每个属性,并将SimpleDalue-object替换为UtilDateSimpleValue,以获取java.util.Date类型的属性。这是一个非常简单的类 - 它实现了与SimpleValue对象相同的接口,例如org.hibernate.mapping.KeyValue。在构造函数中传递原始SimpleValue对象 - 因此每次调用UtilDateSimpleValue都会被重定向到原始对象,但有一个例外 - 方法getType(...)返回我的自定义类型。

public class UtilDateSimpleValue implements KeyValue{

    private SimpleValue value;

    public UtilDateSimpleValue(SimpleValue value) {
        this.value = value;
    }

    public SimpleValue getValue() {
        return value;
    }

    @Override
    public int getColumnSpan() {
        return value.getColumnSpan();
    }

    ...

    @Override
    public Type getType() throws MappingException {
        final String typeName = value.getTypeName();

        if (typeName == null) {
                throw new MappingException("No type name");
        }

        Type result = new UtilDateUserType();

        return result;
    }
    ...
}
  • 最后一步是UtilDateUserType的实现。我只是扩展原始的org.hibernate.type.TimestampType并覆盖它的方法get(),如下所示:

    public class UtilDateUserType extends TimestampType{
    
        @Override
        public Object get(ResultSet rs, String name) throws SQLException {
            Timestamp ts = rs.getTimestamp(name);
    
            Date result = null;
            if(ts != null){
                result = new Date(ts.getTime());
            }
    
            return result;
        }
    }
    

就这些。有点棘手,但现在每个java.util.Date属性都作为java.util.Date返回,而不对现有代码(注释或修改setter)进行任何其他修改。正如我在Hibernate 4或更高版本中发现的那样,有一种更简单的方法来替换您自己的类型(请参阅此处的详细信息: Hibernate TypeResolver)。欢迎任何建议或批评。


9
2018-03-06 11:13





使用自定义UserType的一个简单替代方法是在setter中为持久化bean中的date属性构造一个新的java.util.Date,例如:

import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.Column;

@Entity
public class Purchase {

    private Date date;

    @Column
    public Date getDate() {
        return this.date;
    }

    public void setDate(Date date) {
        // force java.sql.Timestamp to be set as a java.util.Date
        this.date = new Date(date.getTime());
    }

}

9
2017-12-02 22:17



public void getDate() 无法返回类型Date的引用。 - Wrench


方法1和2显然不起作用,因为你得到了 java.sql.Date 每个JPA / Hibernate规范的对象,而不是 java.util.Date。从方法3和方法4开始,我宁愿选择后者,因为它更具说明性,并且可以同时使用字段和getter注释。

您已经在引用的博客文章中列出了解决方案4,因为@tscho非常友好地指出。也许 defaultForType (见下文)应该给你 集中解决方案 你在找。当然,仍然需要区分日期(没有时间)和时间戳字段。

为了将来参考,我将留下使用您自己的Hibernate的摘要 用户类型 这里:

让Hibernate给你 java.util.Date 实例,你可以使用 @类型 和 @TypeDef 注释,用于定义java.util.Date java类型与数据库的不同映射。

请参阅核心参考手册中的示例 这里

  1. 实施一个 用户类型 做实际的管道(转换为/从java.util.Date转换),例如,命名为 TimestampAsJavaUtilDateType
  2. 在一个实体或package-info.java中添加@TypeDef注释 - 两者都将在会话工厂中全局可用(请参阅上面的手动链接)。您可以使用 defaultForType 在所有类型的映射字段上应用类型转换 java.util.Date

    @TypeDef
      name = "timestampAsJavaUtilDate",
      defaultForType = java.util.Date.class, /* applied globally */
      typeClass = TimestampAsJavaUtilDateType.class
    )
    
  3. 可选地,而不是 defaultForType,您可以单独使用@Type注释字段/ getter:

    @Entity
    public class MyEntity {
       [...]
       @Type(type="timestampAsJavaUtilDate")
       private java.util.Date myDate;
       [...]
    }
    

附:建议一个完全不同的方法:我们通常只是不使用equals()来比较Date对象。相反,我们使用实用程序类与方法进行比较,例如只有两个Date实例的日历日期(或其他分辨率,如秒),无论确切的实现类型如何。这对我们来说效果很好。


5
2018-03-02 23:17



OP在可能解决方案的第4点提出了这种方法。该 链接博客文章 还提到了这种方法的微妙副作用:转换自 java.sql.Timestamp 至 java.util.Date 降低纳秒精度。对于这个也是如此 修改后的二传手 做法。这就是为什么我也只用毫秒精度定义数据库中的时间戳列。 - tscho
@tscho你是对的,我完全错过了博文。我将编辑我的答案以反映这一点,虽然我仍然认为它有用,因为它在stackoverflow而不是外部博客。 - Michael Paesold


这是Hibernate 4.3.7.Final的解决方案。

pacakge-info.java包含

@TypeDefs(
    {
        @TypeDef(
                name = "javaUtilDateType",
                defaultForType = java.util.Date.class,
                typeClass = JavaUtilDateType.class
        )
    })
package some.pack;
import org.hibernate.annotations.TypeDef;
import org.hibernate.annotations.TypeDefs;

和JavaUtilDateType:

package some.other.or.same.pack;

import java.sql.Timestamp;
import java.util.Comparator;
import java.util.Date;
import org.hibernate.HibernateException;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.type.AbstractSingleColumnStandardBasicType;
import org.hibernate.type.LiteralType;
import org.hibernate.type.StringType;
import org.hibernate.type.TimestampType;
import org.hibernate.type.VersionType;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.java.JdbcTimestampTypeDescriptor;
import org.hibernate.type.descriptor.sql.TimestampTypeDescriptor;

/**
 * Note: Depends on hibernate implementation details hibernate-core-4.3.7.Final.
 *
 * @see
 * <a href="http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html/ch06.html#types-custom">Hibernate
 * Documentation</a>
 * @see TimestampType
 */
public class JavaUtilDateType
        extends AbstractSingleColumnStandardBasicType<Date>
        implements VersionType<Date>, LiteralType<Date> {

    public static final TimestampType INSTANCE = new TimestampType();

    public JavaUtilDateType() {
        super(
                TimestampTypeDescriptor.INSTANCE,
                new JdbcTimestampTypeDescriptor() {

                    @Override
                    public Date fromString(String string) {
                        return new Date(super.fromString(string).getTime());
                    }

                    @Override
                    public <X> Date wrap(X value, WrapperOptions options) {
                        return new Date(super.wrap(value, options).getTime());
                    }

                }
        );
    }

    @Override
    public String getName() {
        return "timestamp";
    }

    @Override
    public String[] getRegistrationKeys() {
        return new String[]{getName(), Timestamp.class.getName(), java.util.Date.class.getName()};
    }

    @Override
    public Date next(Date current, SessionImplementor session) {
        return seed(session);
    }

    @Override
    public Date seed(SessionImplementor session) {
        return new Timestamp(System.currentTimeMillis());
    }

    @Override
    public Comparator<Date> getComparator() {
        return getJavaTypeDescriptor().getComparator();
    }

    @Override
    public String objectToSQLString(Date value, Dialect dialect) throws Exception {
        final Timestamp ts = Timestamp.class.isInstance(value)
                ? (Timestamp) value
                : new Timestamp(value.getTime());
        // TODO : use JDBC date literal escape syntax? -> {d 'date-string'} in yyyy-mm-dd hh:mm:ss[.f...] format
        return StringType.INSTANCE.objectToSQLString(ts.toString(), dialect);
    }

    @Override
    public Date fromStringValue(String xml) throws HibernateException {
        return fromString(xml);
    }
}

此解决方案主要依赖于TimestampType实现,并通过JdbcTimestampTypeDescriptor类型的匿名类添加其他行为。


2
2018-02-18 12:47



工作很好!但是需要在wrap方法中进行null检查。 - Vasile Bors


只需添加此注释即可 @Temporal(TemporalType.DATE) 为一个 java.util.Date 实体类中的字段。

更多信息,请参阅 这个 stackoverflow的答案。


2
2018-02-15 16:20





Java平台库中有一些类可以扩展实例化 class并添加一个值组件。例如,java.sql.Timestamp 扩展java.util.Date并添加纳秒字段。等于实施 对于时间戳,确实违反了对称性,并且如果可能导致不稳定的行为 Timestamp和Date对象在同一个集合中使用,或以其他方式混合使用。 Timestamp类有一个免责声明警告程序员 混合日期和时间戳。虽然你不会遇到麻烦,只要你 保持它们分开,没有什么可以阻止你混合它们,而且 产生的错误可能很难调试。 Timestamp类的这种行为是一个 错误,不应该被模仿。

看看这个链接

http://blogs.sourceallies.com/2012/02/hibernate-date-vs-timestamp/


1
2018-03-03 23:14





我遇到了这个问题以及我的JUnit assertEquals未能将Dates与Hibernate发出的'java.util.Date'类型进行比较(问题中描述的实际上是时间戳)。事实证明,通过将映射更改为'date'而不是'java.util.Date',Hibernate会生成java.util.Date成员。我正在使用Hibernate版本4.1.12的XML映射文件。

此版本发出'java.util.Timestamp':

<property name="date" column="DAY" type="java.util.Date" unique-key="KONSTRAINT_DATE_IDX" unique="false" not-null="true" />

这个版本发出'java.util.Date':

<property name="date" column="DAY" type="date" unique-key="KONSTRAINT_DATE_IDX" unique="false" not-null="true" />

但是,请注意,如果使用Hibernate生成DDL,那么这些将生成不同的SQL类型('date'的日期和'java.util.Date'的Timestamp)。


0
2017-07-30 17:24