JPA/Hibernate使用键java.time.YearMonth进行映射。

huangapple go评论75阅读模式
英文:

JPA/Hibernate Map using key java.time.YearMonth

问题

以下是翻译好的部分:

我正在开发一个金融应用程序,其中的时间单位是月份,被解释为(年,月)。FTT代表金融交易税,为了提供一些背景信息。

我们公司推崇先建立干净数据库的方法。我们使用Hibernate 5.1。我知道它已经终止支持了,但是不能更改。

我正在重构一个实体:

@Entity
@Table(name="FTT_REPORT")
public class FttReport {

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

    @Column(name="REFERENCE_YEAR")
    private int referenceYear; //旧代码,我可以直接使用YearMonth
    @Column(name="REFERENCE_MONTH")
    private int referenceMonth; //旧代码,我可以直接使用YearMonth

    private BigDecimal [很多税务数据];

    @ElementCollection(fetch = FetchType.EAGER)
    @MapKeyClass(YearMonth.class)
    @OrderBy("REFERENCE_YEAR, REFERENCE_MONTH")
    // @MapKeyType(@Type(type = "com.acme.FttYearMonthUserType")) #稍后我会解释为什么注释掉
    @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只是一个带有数值列的可嵌入实体。

简要解释一下。我们的应用程序每个计算税务报告,但有时会有延迟交易,并且客户必须向政府支付罚款费用/附加税。我们不会将这个数字混合到总税中,而是需要向用户显示所有“调整”月份的全部附加税报告。

至此,我知道如果我使用LocalDate作为映射键并假设日期总是1号,那么我就可以结束了。但是我需要首先尝试一种更清晰的方法。

通过上述映射,以下操作是成功的:

  • Hibernate 正确地创建了包含我所喜欢的正确列的数据库创建脚本(不使用 @MapKeyType
  • Hibernate 成功添加了预期的主键和外键约束(不使用 @MapKeyType
  • Hibernate 成功地从单元测试中进行了第一个税务调整的 INSERT 操作

不起作用的是检索。当 Hibernate 检索第一个用于调整的 FttReport 时,此时 MapKeyType 仍被注释掉。

引发的错误是:

org.hibernate.InstantiationException: No default constructor for entity: java.time.YearMonth

因此,要求的内容如下:

  • 如何正确使用非常规的复杂类型作为映射中的键?在这里,非常规意味着这种类型不是与 Hibernate(也不是 JSR-310 扩展)原生注册的类型,也没有 POJO 格式。
  • 为什么启用 MapKeyType 后,我的 AttributeOverrides 注解被完全忽略了?我本来希望可以覆盖这些列。
  • 是否有更好的方法告诉 Hibernate,我希望这个多键映射使用我喜欢的列名?

如果需要更多翻译或其他帮助,请告诉我。

英文:

I am working on a financial application where the unit of time is the month, intended as (year, month). FTT stands for Financial Transaction Tax, to give a bit of a context

Our company promotes a clean-database-first approach. We use Hibernate 5.1. I know it's EOL, but can't be changed.

I am currently refactoring an entity

@Entity
@Table(name=&quot;FTT_REPORT&quot;)
public class FttReport {

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

    @Column(name=&quot;REFERENCE_YEAR&quot;)
    private int referenceYear; //Old code, I could use YearMonth directly
    @Column(name=&quot;REFERENCE_MONTH&quot;)
    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(&quot;REFERENCE_YEAR, REFERENCE_MONTH&quot;)
    // @MapKeyType(@Type(type = &quot;com.acme.FttYearMonthUserType&quot;)) #I&#39;ll explain soon why it&#39;s commented out
    @AttributeOverrides({//
        @AttributeOverride(name = &quot;key.year&quot;, column = @Column(name = &quot;REFERENCE_YEAR&quot;)),//
        @AttributeOverride(name = &quot;key.month&quot;, column = @Column(name = &quot;REFERENCE_MONTH&quot;)),//
    })
    @CollectionTable(//
        name = &quot;FTT_REPORT_ADJUSTMENTS&quot;,//
        foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = &quot;FK_FTT_ADJUSTMENT_REPORT&quot;),//
        joinColumns = @JoinColumn(name = &quot;REPORT_ID&quot;)//
    )
    private final SortedMap&lt;YearMonth, FttAdjustment&gt; adjustments = new TreeMap&lt;&gt;();

}

The FttAdjustment is just an embeddable with only numerical columns

A brief explanation. Our application computes tax reports every month, but sometimes late trades come in and Customer has to pay a penalty fee /surcharge tax to Government. We don't blend the figure into total tax but need to show the user a full report of all surcharge taxes for all "adjusted" months.

At this point, I know that I could call it a day by using LocalDate as map key and assume day always 1st.

But I am required to try a cleaner approach first.

With the above mapping, the following is successful:

  • Hibernate correctly creates the database creation script with the correct columns that I like (without @MapKeyType)
  • Hibernate successfully adds the expected primary key and foreign key constraints (without @MapKeyType)
  • Hibernate successfully INSERTs the very first tax adjustment from the unit test 🎉

What doesn't work, is retrieval. When Hibernate retrieves the first FttReport tested for adjustments. At this moment, MapKeyType is still commented out.

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]

So far so bad. Other entities, like a Trade use YearMonth columns flat-ly by using a custom type

@MappedSuperclass
public abstract class FttAbstractTrade {

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

}

At this point, please note that @MapKeyType was commented out, so I had tried to enable that annotation with custom type. At least, I thought, the custom type handler knows that a YearMonth must be instantiated via constructor with year and month.

Cool, but now Hibernate won't map the REFERENCE_YEAR and REFERENCE_MONTH columns, trying to find basic year and month columns.

I know the application will work if I accept Hibernate's mandated column names, but I have a little more time to investigate and learn before this becomes a total showstopper.

After enabling the MapKeyType annotation, startup error occurs (because hbm2ddl.auto is validate)

org.hibernate.tool.schema.spi.SchemaManagementException: Schema-validation: missing column [month] in table [FTT_REPORT_ADJUSTMENTS]

Question

Well... basically I could ask "how can I make this thing work?" but a good phrasing is...

  • What is the correct way to use an unconventional complex type as key in a map? By unconventional, I mean a type that is not natively registered with Hibernate (neither in JSR-310 extensions) and has no POJO format
  • Why, when I enable MapKeyType, my AttributeOverrides annotations get totally ignored? I expected to override the columns
  • Is there a better way to tell Hibernate that I want this multi-key map to name its columns my preferred way?

Edit

I have fixed my startup problem by changing AttributeOverrides.

It took me debugging into Hibernate's own source code to find how it liked the attribute to be named.

By setting a breakpoint in Hibernate's source code (AbstractPropertyHolder line 264) I found out that Hibernate uses keyword index in this case to identify map key.

I changed

@AttributeOverrides({//
     @AttributeOverride(name = &quot;index.year&quot;, column = @Column(name = &quot;REFERENCE_YEAR&quot;)),//
    @AttributeOverride(name = &quot;index.month&quot;, column = @Column(name = &quot;REFERENCE_MONTH&quot;)),//
})

Now the application starts but I still get the instantiation exception

Edit

Soure code for the user type, FttYearMonthUserType

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

}

Edit

After more debugging into Hibernate's core, I discovered what follows

On AbstractCollectionPersister constructor I debugged the path indexedCollection.getIndex().getType() and it is null. It must be a Type. UserType does not extend Type while CompositeType does, and that's not even CompositeUserType.

The statement returns a default ComponentType which I am examining

JPA/Hibernate使用键java.time.YearMonth进行映射。

For now I have understood that during the SessionFactory init phase, the property of that key should change. Or, judging by the type of persisters that Hibernate offers out of the box, maybe it's not possible to use a non-POJO type as map key, and perhaps one might need to use the value as store for the key's columns?

答案1

得分: 1

在Hibernate文档中,我看到了非常相似的映射,但有一个重要的区别:映射键映射到单个列。我在文档中找不到相关证据,但我担心对于@ElementCollection关联,不支持将映射键映射到多个列。

但是,我成功地在@OneToMany关联中实现了这个映射。下面我将提供一个示例。

假设我们有以下数据库模式:

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

我们可以使用以下映射:

@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. 只有当FttAdjustment是一个实体时,才能使用@MapKey注解。
  2. @JoinColumn中添加了updatable = false,以避免在插入新的FttAdjustment时产生额外的更新查询。
  3. 这个映射已经在Hibernate 5.4.10.Final版本中进行了测试。

P.S. 还有一个额外的注意事项。@AttributeOverride是JPA定义的用于重写可嵌入类型列名的注解,但您尝试将其用于设置hibernate自定义基本类型的列名,我担心这是一个错误。

英文:

I see very similar mapping in the hibernate documentation, but with one important difference: map key mapped to a single column. I can not find proof in the documentation, but I am afraid the similar mapping with map key mapped to the several columns is not possible for the @ElementCollection association.

However, I was able to do it for the @OneToMany association. So, below I will provide an example.

Assuming that we have the following schema:

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

We can use the following mapping:

@Entity
@Table(name=&quot;FTT_REPORT&quot;)
public class FttReport
{
   @Id
   @Column(name=&quot;REPORT_ID&quot;)
   private Long id;

   @OneToMany(cascade = CascadeType.ALL)
   @OrderBy(&quot;REPADJ_YEAR, REPADJ_MONTH&quot;)
   @JoinColumn(name = &quot;REPADJ_REPORT_ID&quot;, updatable = false)
   @MapKey(name = &quot;adjYearMonth&quot;)
   private SortedMap&lt;YearMonth, FttAdjustment&gt; adjustments;
}


@Entity
@Table(name = &quot;FTT_REPORT_ADJUSTMENTS&quot;)
public class FttAdjustment
{
   @Id
   @Column(name = &quot;REPADJ_ID&quot;)
   private Long id;
   
   // this needs only for adding new FttAdjustment
   @Column(name = &quot;REPADJ_REPORT_ID&quot;)
   private Long reportId;
   
   @Type(type = &quot;com.acme.FttYearMonthUserType&quot;)
   @Columns(columns = {
      @Column(name = &quot;REPADJ_YEAR&quot;),
      @Column(name = &quot;REPADJ_MONTH&quot;)
   })
   private YearMonth adjYearMonth;
}
  1. @MapKey annotation can be used only if the FttAdjustment is an entity.
  2. The updatable = false was added to the @JoinColumn to avoid extra update query during inserting new FttAdjustment.
  3. This mapping was tested with hibernate 5.4.10.Final.

P.S. And one more additional note. The @AttributeOverride is the JPA defined annotation for the overriding embeddable types column names, but you try to use it for setting column names for the hibernate custom basic types. I am afraid that this is a mistake.

huangapple
  • 本文由 发表于 2020年9月2日 00:03:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/63691367.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定