首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >使用密钥java.time.YearMonth的JPA/Hibernate映射

使用密钥java.time.YearMonth的JPA/Hibernate映射
EN

Stack Overflow用户
提问于 2020-09-01 16:03:04
回答 1查看 283关注 0票数 3

我正在做一个财务应用程序,其中的时间单位是月份,打算作为(年,月)。FTT代表金融交易税,给出一些上下文

我们公司提倡一种干净的数据库优先方法.我们使用Hibernate 5.1。我知道这是EOL,但不能改变。

我目前正在重构一个实体

代码语言:javascript
复制
@Entity
@Table(name="FTT_REPORT")
public class FttReport {

    @Column(name="REPORT_ID")
    private final Long id = null;

    @Column(name="REFERENCE_YEAR")
    private int referenceYear; //Old code, I could use YearMonth directly
    @Column(name="REFERENCE_MONTH")
    private int referenceMonth; //Old code, I could use YearMonth directly


    private BigDecimal [a lot of tax figures];

    @ElementCollection(fetch = FetchType.EAGER)
    @MapKeyClass(YearMonth.class)
    @OrderBy("REFERENCE_YEAR, REFERENCE_MONTH")
    // @MapKeyType(@Type(type = "com.acme.FttYearMonthUserType")) #I'll explain soon why it's commented out
    @AttributeOverrides({//
        @AttributeOverride(name = "key.year", column = @Column(name = "REFERENCE_YEAR")),//
        @AttributeOverride(name = "key.month", column = @Column(name = "REFERENCE_MONTH")),//
    })
    @CollectionTable(//
        name = "FTT_REPORT_ADJUSTMENTS",//
        foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "FK_FTT_ADJUSTMENT_REPORT"),//
        joinColumns = @JoinColumn(name = "REPORT_ID")//
    )
    private final SortedMap<YearMonth, FttAdjustment> adjustments = new TreeMap<>();

}

FttAdjustment只是一个只有数值列的可嵌入的

一个简短的解释。我们的应用程序每个月都会计算纳税报告,但有时会出现延迟交易,客户不得不向政府缴纳罚款/surcharge税。我们不把这个数字合并到总税收中,但需要向用户显示所有“调整”月份的所有附加税的完整报告。

在这一点上,我知道我可以使用LocalDate作为映射键将其称为一天,并假定日期总是第一天。

但我必须先尝试一种更清洁的方法。

通过上述映射,以下内容是成功的:

  • Hibernate使用我喜欢的正确列正确地创建数据库创建脚本(没有@MapKeyType)
  • Hibernate成功地添加了预期的主键和外键约束(没有@MapKeyType)
  • Hibernate成功地实现了INSERT在单元测试中的第一次税收调整。

不起作用的是检索。当Hibernate检索到用于调整的第一个FttReport时。此时,MapKeyType仍被排除在外。

代码语言:javascript
复制
Caused by: org.hibernate.InstantiationException: No default constructor for entity:  : java.time.YearMonth
    at org.hibernate.tuple.PojoInstantiator.instantiate(PojoInstantiator.java:81) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.tuple.component.AbstractComponentTuplizer.instantiate(AbstractComponentTuplizer.java:83) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.type.ComponentType.instantiate(ComponentType.java:577) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.type.ComponentType.instantiate(ComponentType.java:583) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.type.ComponentType.resolve(ComponentType.java:681) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.type.ComponentType.nullSafeGet(ComponentType.java:325) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.persister.collection.AbstractCollectionPersister.readIndex(AbstractCollectionPersister.java:845) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.collection.internal.PersistentMap.readFrom(PersistentMap.java:265) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.loader.plan.exec.process.internal.CollectionReferenceInitializerImpl.finishUpRow(CollectionReferenceInitializerImpl.java:77) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.loader.plan.exec.process.internal.AbstractRowReader.readRow(AbstractRowReader.java:121) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.loader.plan.exec.process.internal.ResultSetProcessorImpl.extractResults(ResultSetProcessorImpl.java:122) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeLoad(AbstractLoadPlanBasedLoader.java:122) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeLoad(AbstractLoadPlanBasedLoader.java:86) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.loader.collection.plan.AbstractLoadPlanBasedCollectionInitializer.initialize(AbstractLoadPlanBasedCollectionInitializer.java:88) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.persister.collection.AbstractCollectionPersister.initialize(AbstractCollectionPersister.java:688) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.event.internal.DefaultInitializeCollectionEventListener.onInitializeCollection(DefaultInitializeCollectionEventListener.java:75) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.internal.SessionImpl.initializeCollection(SessionImpl.java:2004) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.collection.internal.AbstractPersistentCollection$4.doWork(AbstractPersistentCollection.java:567) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:249) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:563) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.collection.internal.AbstractPersistentCollection.forceInitialization(AbstractPersistentCollection.java:731) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.engine.internal.StatefulPersistenceContext.initializeNonLazyCollections(StatefulPersistenceContext.java:918) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:347) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.loader.Loader.doList(Loader.java:2622) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.loader.Loader.doList(Loader.java:2605) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.loader.Loader.listIgnoreQueryCache(Loader.java:2434) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.loader.Loader.list(Loader.java:2429) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.loader.hql.QueryLoader.list(QueryLoader.java:501) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.hql.internal.ast.QueryTranslatorImpl.list(QueryTranslatorImpl.java:370) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.engine.query.spi.HQLQueryPlan.performList(HQLQueryPlan.java:216) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.internal.SessionImpl.list(SessionImpl.java:1339) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at org.hibernate.internal.QueryImpl.list(QueryImpl.java:87) ~[hibernate-core-5.1.17.Final.jar:5.1.17.Final]
    at com.acme.core.data.dao.hibernate.BaseDaoImpl.findAll(BaseDaoImpl.java:927) ~[acme-3.10.5-BETA-15.jar:47cac235aaafde97a5117a64431433f405db8026]

到目前为止很糟糕。其他实体,如Trade,则通过使用自定义类型来平缓地使用YearMonth列。

代码语言:javascript
复制
@MappedSuperclass
public abstract class FttAbstractTrade {

    @Type(type = "com.acme.FttYearMonthUserType")
    @Columns(columns = {//
        @Column(name = "REFERENCE_YEAR"),//
        @Column(name = "REFERENCE_MONTH")//
    })
    protected YearMonth referencePeriod;

}

此时,请注意@MapKeyType已被注释掉,因此我尝试使用自定义类型启用该注释。至少,我认为自定义类型处理程序知道必须通过带有年份和月份的构造函数实例化YearMonth。

很酷,但是Hibernate现在不会映射REFERENCE_YEARREFERENCE_MONTH列,而是试图找到基本的yearmonth列。

知道,如果我接受Hibernate的强制列名,应用程序就可以工作,但在这成为一个完全的展示障碍之前,我还有更多的时间去研究和学习。

启用MapKeyType注释后,会发生启动错误(因为hbm2ddl.autovalidate)。

代码语言:javascript
复制
org.hibernate.tool.schema.spi.SchemaManagementException: Schema-validation: missing column [month] in table [FTT_REPORT_ADJUSTMENTS]

问题

好吧..。基本上,我可以问:“我怎样才能让这个东西工作?”但好的措辞是..。

  • 在地图中使用非常规复杂类型作为关键的正确方法是什么?我指的是一种非常规的类型,它不是在Hibernate中本地注册的(也不是在JSR-310扩展中),并且没有POJO格式。
  • 为什么,当我启用MapKeyType时,AttributeOverride的注释会被完全忽略?我希望覆盖这些列
  • 是否有更好的方法告诉Hibernate,我希望这个多键映射将其列命名为我喜欢的方式?

编辑

我已经通过改变AttributeOverrides来解决我的启动问题。

我调试了Hibernate自己的源代码,找到了,它喜欢这个要命名的属性。

通过在Hibernate的源代码(AbstractPropertyHolder行264)中设置一个断点,我发现Hibernate在本例中使用关键字index来标识映射键。

我改变了

代码语言:javascript
复制
@AttributeOverrides({//
     @AttributeOverride(name = "index.year", column = @Column(name = "REFERENCE_YEAR")),//
    @AttributeOverride(name = "index.month", column = @Column(name = "REFERENCE_MONTH")),//
})

现在,应用程序启动了,但我仍然得到实例化异常。

编辑

用户类型FttYearMonthUserType的源代码

代码语言:javascript
复制
public class FttYearMonthUserType implements UserType
{

    @Override
    public int[] sqlTypes()
    {
        return new int[] { Types.INTEGER, Types.INTEGER };
    }

    @Override
    public Class returnedClass()
    {
        return YearMonth.class;
    }

    @Override
    public boolean equals(Object x, Object y) throws HibernateException
    {
        return Objects.equals(x, y);
    }

    @Override
    public int hashCode(Object x) throws HibernateException
    {
        return Objects.hashCode(x);
    }

    @Override
    public YearMonth nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException
    {

        int year = rs.getInt(names[0]);
        if (rs.wasNull())
            return null;
        int month = rs.getInt(names[1]);
        if (rs.wasNull())
            return null;

        return YearMonth.of(year, month);
    }

    @Override
    public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException
    {
        if (Objects.isNull(value))
        {
            st.setNull(index, Types.INTEGER);
            st.setNull(index + 1, Types.INTEGER);
        }
        else
        {
            YearMonth yearMonth = (YearMonth) value;
            Integer year = yearMonth.getYear();
            Integer month = yearMonth.getMonthValue();

            st.setObject(index, year, Types.INTEGER);
            st.setObject(index + 1, month, Types.INTEGER);
        }
    }

    @Override
    public YearMonth deepCopy(Object value) throws HibernateException
    {
        if (value == null)
            return null;
        YearMonth yearMonth = (YearMonth) value;
        return YearMonth.of(yearMonth.getYear(), yearMonth.getMonthValue());
    }

    @Override
    public boolean isMutable()
    {
        return false; //Fixed after conversation
    }

    @Override
    public Serializable disassemble(Object value) throws HibernateException
    {
        return deepCopy(value);
    }

    @Override
    public Object assemble(Serializable cached, Object owner) throws HibernateException
    {
        return deepCopy(cached);
    }

    @Override
    public Object replace(Object original, Object target, Object owner) throws HibernateException
    {
        return deepCopy(original);
    }

}

编辑

在对Hibernate的核心进行了更多的调试之后,我发现了以下内容

AbstractCollectionPersister构造函数上,我调试了路径indexedCollection.getIndex().getType(),它是空的。一定是TypeUserType不扩展Type,而CompositeType则扩展,这甚至不是CompositeUserType

该语句返回我正在检查的默认ComponentType

现在,我已经理解在SessionFactory init阶段,该键的属性应该改变。或者,从Hibernate提供的现成的持久化器类型判断,可能不可能使用非POJO类型作为映射键,或者可能需要将值用作键列的存储库?

EN

回答 1

Stack Overflow用户

发布于 2020-09-13 11:37:06

我在hibernate 文档中看到了非常类似的映射,但是有一个重要的区别:映射键映射到单个列。我在文档中找不到证据,但恐怕@ElementCollection关联不可能使用映射键映射到几个列的类似映射。

然而,我能够为@OneToMany协会做这件事。因此,下面我将提供一个例子。

假设我们有以下模式:

代码语言:javascript
复制
create table FTT_REPORT
(
   REPORT_ID int not null,
   primary key (REPORT_ID)
);

create table FTT_REPORT_ADJUSTMENTS
(
   REPADJ_ID int not null,
   REPADJ_REPORT_ID int not null,
   REPADJ_YEAR int,
   REPADJ_MONTH int,
   
   primary key (REPADJ_ID),
   foreign key (REPADJ_REPORT_ID) references FTT_REPORT(REPORT_ID)
);

我们可以使用以下映射:

代码语言:javascript
复制
@Entity
@Table(name="FTT_REPORT")
public class FttReport
{
   @Id
   @Column(name="REPORT_ID")
   private Long id;

   @OneToMany(cascade = CascadeType.ALL)
   @OrderBy("REPADJ_YEAR, REPADJ_MONTH")
   @JoinColumn(name = "REPADJ_REPORT_ID", updatable = false)
   @MapKey(name = "adjYearMonth")
   private SortedMap<YearMonth, FttAdjustment> adjustments;
}


@Entity
@Table(name = "FTT_REPORT_ADJUSTMENTS")
public class FttAdjustment
{
   @Id
   @Column(name = "REPADJ_ID")
   private Long id;
   
   // this needs only for adding new FttAdjustment
   @Column(name = "REPADJ_REPORT_ID")
   private Long reportId;
   
   @Type(type = "com.acme.FttYearMonthUserType")
   @Columns(columns = {
      @Column(name = "REPADJ_YEAR"),
      @Column(name = "REPADJ_MONTH")
   })
   private YearMonth adjYearMonth;
}
  1. 只有当@MapKey是一个实体时,才能使用FttAdjustment注释。
  2. updatable = false被添加到@JoinColumn中,以避免在插入新FttAdjustment时进行额外的更新查询。
  3. 这个映射是用hibernate 5.4.10.Final测试的。

还有另外一个音符。@AttributeOverride是JPA为覆盖可嵌入类型列名定义的注释,但是您尝试使用它来设置hibernate自定义基本类型的列名。恐怕这是个错误。

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/63691367

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档