英文:
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="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<>();
}
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
INSERT
s 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 = "com.acme.FttYearMonthUserType")
@Columns(columns = {//
@Column(name = "REFERENCE_YEAR"),//
@Column(name = "REFERENCE_MONTH")//
})
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
, myAttributeOverride
s 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 = "index.year", column = @Column(name = "REFERENCE_YEAR")),//
@AttributeOverride(name = "index.month", column = @Column(name = "REFERENCE_MONTH")),//
})
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
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;
}
- 只有当
FttAdjustment
是一个实体时,才能使用@MapKey
注解。 - 在
@JoinColumn
中添加了updatable = false
,以避免在插入新的FttAdjustment
时产生额外的更新查询。 - 这个映射已经在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="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;
}
@MapKey
annotation can be used only if theFttAdjustment
is an entity.- The
updatable = false
was added to the@JoinColumn
to avoid extra update query during inserting newFttAdjustment
. - 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论