有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

JPA/Hibernate使用java键映射。时间年月

我正在开发一个财务应用程序,其中时间单位为月,计划为(年,月)。FTT代表Financial Transaction Tax,给出一点上下文

我们公司首先推广清洁数据库的方法。我们使用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; //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只是一个只有数字列的可嵌入函数

一个简短的解释。我们的应用程序每个月计算一次税务报告,但有时出现延迟交易,客户必须向政府支付罚款/附加税。我们不会将该数字与总税额混合,但需要向用户显示所有“调整”月份的所有附加税的完整报告

在这一点上,我知道我可以通过使用LocalDate作为映射键来命名这一天,并假设这一天总是第一天

但我必须先尝试更干净的方法

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

  • Hibernate使用我喜欢的正确列正确地创建数据库创建脚本(没有@MapKeyType
  • Hibernate成功地添加了预期的主键和外键约束(没有@MapKeyType
  • Hibernate成功{}这是单元测试中的第一次税务调整🎉

不起作用的是检索。当Hibernate检索第一个经过调整测试的FTT端口时。此时,MapKeyType仍被注释掉

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列

@MappedSuperclass
public abstract class FttAbstractTrade {

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

}

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

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

知道如果我接受Hibernate强制使用的列名,应用程序就会工作,但在这成为一个彻底的阻碍之前,我还有一点时间来调查和学习

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

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 line 264)中设置断点,我发现Hibernate在本例中使用关键字index来标识映射键

我变了

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

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

编辑

用户类型的源代码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);
    }

}

编辑

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

AbstractCollectionPersister constructor上,我调试了路径indexedCollection.getIndex().getType(),它是空的。它必须是一个TypeUserType不扩展Type,而CompositeType扩展,这甚至不是CompositeUserType

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

IntelliJ debug window

现在我已经了解到,在SessionFactory初始化阶段,该键的属性应该改变。或者,根据Hibernate提供的现成持久器类型判断,也许不可能使用非POJO类型作为映射键,也许需要使用该值作为键列的存储


共 (1) 个答案

  1. # 1 楼答案

    我在hibernate documentation中看到了非常相似的映射,但有一个重要的区别:映射键映射到单个列。我在文档中找不到证据,但对于@ElementCollection关联,恐怕无法使用映射到多个列的map key进行类似映射

    然而,我能够为@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是一个实体时,才能使用{}注释
    2. updatable = false被添加到@JoinColumn中,以避免在插入新的FttAdjustment时进行额外的更新查询
    3. 这个映射是用hibernate 5.4.10.Final测试的

    附言,还有一个附加说明。@AttributeOverride是JPA为overriding embeddable types列名定义的注释,但您尝试使用它来设置hibernate custom basic types的列名。恐怕这是个错误