如何映射具有 OneToMany 关系的两个抽象 InheritanceType.TABLE_PER_CLASS 实体?

How do you map two abstract InheritanceType.TABLE_PER_CLASS entities with a OneToMany relationship?

我将 Hibernate 与 Spring Boot 和 JPA 一起使用,并且有一个业务需求来检索并合并到单个分页响应数据中,该数据存储在四个不同的 table 中数据库

我们将前两个 table 称为“tblCredits”,其中包含 Credits,以及“tblDebits”,其中包含 Debits。出于我们的目的,这两个 table 是相同的 - 相同的列名、相同的列类型、相同的 ID 字段,所有内容。我的端点应该能够 return 贷方和借方的组合列表,并且能够 search/sort 通过 any/all 被 return 编辑的字段,并且带分页。

如果我控制了那个数据库,我会简单地将两个 table 合并到一个 table 中,或者创建一个视图或存储过程来为我做这件事,但这是一个其他应用程序使用的遗留数据库,我无法以任何方式修改,所以这不是一个选项。

如果我不需要排序和分页,我可以只创建两个完全独立的实体,为每个实体创建一个单独的 Spring Data JPA 存储库,分别查询两个存储库,然后合并结果在我自己的代码中。但是特别是对组合结果进行分页会变得非常麻烦,我不想自己实现合并分页逻辑,除非我绝对必须这样做。理想情况下,我应该能够让 JPA 立即为我处理所有这些。

我已经能够为前两个 table 实现第一步,使用抽象 class 声明为 InheritanceType.TABLE_PER_CLASS 的实体,如下所示:

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class AbstractCreditDebitEntity {
/* literally all my properties and ID and column mappings here 
...
*/
}

然后是扩展该抽象实体并简单指定两个不同 table 映射的两个具体 classes,根本没有 class 特定的属性或列映射:

@Entity
@Table(name = "tblCredits")
public final class Credit extends AbstractCreditDebitEntity {
//Literally nothing inside this class
}

@Entity
@Table(name = "tblDebits")
public final class Debit extends AbstractCreditDebitEntity {
//Literally nothing inside this class
}

到目前为止一切顺利,效果很好,我能够在 AbstractCreditDebitEntity 实体上创建一个 Spring JPA 存储库,在后台生成对两个 table 的联合查询,并且我能够通过适当的分页和排序在单个查询中从两个 table 中取回记录。 (联合查询的性能问题目前与我无关。)

然而,我在下一步中被绊倒了,当我合并另外两个 table 时。 tblCredits 与 tblCreditLineItems 具有一对多关系,而 tblDebits 与 tblDebitLineItems 具有一对多关系。同样,从我们的角度来看,tblCreditLineItems 和 tblDebitLineItems 是相同的 tables - 相同的列名、相同的列类型、相同的 ID 字段,所有内容。

所以我可以对那些子实体采用与以前相同的模式:

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class AbstractCreditDebitLineItemEntity {
/* literally all my properties and ID and column mappings here 
...
*/
}
@Entity
@Table(name = "tblCreditLineItems")
public final class CreditLineItem extends AbstractCreditDebitLineItemEntity {
//Literally nothing inside this class
}

@Entity
@Table(name = "tblDebitLineItems")
public final class DebitLineItem extends AbstractCreditDebitLineItemEntity {
//Literally nothing inside this class
}

但现在我需要在 Credit/Debit 实体和 CreditLineItem/DebitLineItem 实体之间创建映射。这就是我挣扎的地方。因为我需要能够根据关联的 CreditLineItem/DebitLineItem 实体内的属性值来过滤我 return 哪些特定的 Credit/Debit 实体,所以我需要两个实体之间的双向映射,并且我一直无法成功运行。

这是我到目前为止的进展情况。首先是三个 Credit/Debit 实体,其 OneToMany 映射到它们关联的 CreditLineItem/DebitLineItem 实体:

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class AbstractCreditDebitEntity {
/* literally all my properties and ID and column mappings here 
...
*/
  @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
  @JoinColumn(
      name = "MyIdColumnName",
      referencedColumnName = "MyIdColumnName"
  )
  public abstract List<AbstractCreditDebitLineItemEntity> getCreditDebitLineItems();

  public abstract void setCreditDebitLineItems(List<AbstractCreditDebitLineItemEntity> items);

}
@Entity
@Table(name = "tblCredits")
public final class Credit extends AbstractCreditDebitEntity {

  private List<CreditLineItem> creditDebitLineItems;

  @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, targetEntity = CreditLineItem.class)
  @JoinColumn(
      name = "MyIdColumnName",
      referencedColumnName = "MyIdColumnName"
  )
  @Override
  public List<AbstractCreditDebitLineItemEntity> getCreditDebitLineItems() {
    return Optional.ofNullable(creditDebitLineItems).stream()
        .flatMap(List::stream)
        .filter(value -> AbstractCreditDebitLineItemEntity.class.isAssignableFrom(value.getClass()))
        .map(AbstractCreditDebitLineItemEntity.class::cast)
        .collect(Collectors.toList());
  }

  @Override
  public void setCreditDebitLineItems(List<AbstractCreditDebitLineItemEntity> items) {
    creditDebitLineItems =  Optional.ofNullable(items).stream()
      .flatMap(List::stream)
      .filter(value -> CreditLineItem.class.isAssignableFrom(value.getClass()))
      .map(CreditLineItem.class::cast)
      .collect(Collectors.toList());
  }

}

@Entity
@Table(name = "tblDebits")
public final class Debit extends AbstractCreditDebitEntity {
  private List<DebitLineItem> creditDebitLineItems;

  @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, targetEntity = DebitLineItem.class)
  @JoinColumn(
      name = "MyIdColumnName",
      referencedColumnName = "MyIdColumnName"
  )
  @Override
  public List<AbstractCreditDebitLineItemEntity> getCreditDebitLineItems() {
    return Optional.ofNullable(creditDebitLineItems).stream()
        .flatMap(List::stream)
        .filter(value -> AbstractCreditDebitLineItemEntity.class.isAssignableFrom(value.getClass()))
        .map(AbstractCreditDebitLineItemEntity.class::cast)
        .collect(Collectors.toList());
  }

  @Override
  public void setCreditDebitLineItems(List<AbstractCreditDebitLineItemEntity> items) {
    creditDebitLineItems =  Optional.ofNullable(items).stream()
      .flatMap(List::stream)
      .filter(value -> DebitLineItem.class.isAssignableFrom(value.getClass()))
      .map(DebitLineItem.class::cast)
      .collect(Collectors.toList());

  }
}

然后三个 CreditLineItem/DebitLineItem 实体及其 ManyToOne 映射回到 Credit/Debit 实体:

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class AbstractCreditDebitLineItemEntity {
/* literally all my properties and ID and column mappings here 
...
*/
  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(
      name = "MyIdColumnName",
      referencedColumnName = "MyIdColumnName",
      updatable = false,
      insertable = false)
  public abstract AbstractCreditDebitEntity getCreditDebit();

  public abstract void setCreditDebit(AbstractCreditDebitEntity creditDebitEntity);
}
@Entity
@Table(name = "tblCreditLineItems")
public final class CreditLineItem extends AbstractCreditDebitLineItemEntity {

    private Credit creditDebit;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(
      name = "MyIdColumnName",
      referencedColumnName = "MyIdColumnName",
      updatable = false,
      insertable = false)
 @Override
  public Credit getCreditDebit() {
    return creditDebit;
  }

  @Override
  public void setCreditDebit(AbstractCreditDebitEntity creditDebitEntity) {
    creditDebit =
        Optional.ofNullable(creditDebitEntity)
            .filter(value -> Credit.class.isAssignableFrom(value.getClass()))
            .map(Credit.class::cast)
            .orElse(throw new RuntimeException());
  }
}

@Entity
@Table(name = "tblDebitLineItems")
public final class DebitLineItem extends AbstractCreditDebitLineItemEntity {
    private Debit creditDebit;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(
      name = "MyIdColumnName",
      referencedColumnName = "MyIdColumnName",
      updatable = false,
      insertable = false)
 @Override
  public Debit getCreditDebit() {
    return creditDebit;
  }

  @Override
  public void setCreditDebit(AbstractCreditDebitEntity creditDebitEntity) {
    creditDebit =
        Optional.ofNullable(creditDebitEntity)
            .filter(value -> Debit.class.isAssignableFrom(value.getClass()))
            .map(Debit.class::cast)
            .orElse(throw new RuntimeException());
  }
}

这段代码可以编译,但是...当我在我的自动化测试中尝试保留我的信用实体之一(我使用一个简单的 H2 数据库进行我的自动化测试)时,我收到以下错误:

2021-04-02 13:53:52 [main] DEBUG org.hibernate.SQL T: S: - update AbstractCreditDebitLineItemEntity set MyIdColumnName=? where ID=?
2021-04-02 13:53:52 [main] DEBUG o.h.e.jdbc.spi.SqlExceptionHelper T: S: - could not prepare statement [update AbstractCreditDebitLineItemEntity set MyIdColumnName=? where ID=?]
org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "ABSTRACTCREDITDEBITLINEITEMENTITY" does not exist

根据从我的 AbstractCreditDebitEntity class 到我的 AbstractCreditDebitLineItemEntity 的 @OneToMany 映射,它似乎试图坚持下去。其中,因为它是一个带有 InheritanceType.TABLE_PER_CLASS 的抽象 class,没有为它指定 table,所以它假定它需要坚持的 table 与 class.

我想在这里发生的是 Credit subclass 中具体 getter 上的 @OneToMany 映射,它将其 targetEntity 指定为具体 CreditLineItem.class,本质上override/replace @OneToMany 映射到其父抽象 class。但似乎具体 class 上的映射被完全忽略了?

我可以从 AbstractCreditDebitEntity class 中完全删除 @OneToMany 映射,并且只在扩展它的两个具体 Credit/Debit 实体中定义该映射。这使得持久性错误消失,并且我的测试用例的 90% 通过了......但是在那种情况下,当我尝试过滤或排序从组合的 AbstractCreditDebitEntity Spring Data JPA 存储库返回的结果时,基于以下之一仅存在于 CreditLineItem/DebitLineItem 子实体中的字段,由于 AbstractCreditDebitEntity 不再具有到 AbstractCreditDebitLineItemEntity 的任何映射,因此查询失败。

有没有解决这个问题的好方法,使得从AbstractCreditDebitEntity到AbstractCreditDebitLineItemEntity的OneToMany映射仍然存在,但是知道Credit实体具体映射到CreditLineItem实体,Debit实体具体映射到DebitLineItem实体也维护了?

经过大量试验,我找到了适合我的东西。

基本上,与其尝试用具体实体中的 OneToMany 映射覆盖抽象实体 class 中的 OneToMany 映射,我不得不让它们完全分离映射到完全不同的属性。这意味着我的具体实体有两个不同的 AbstractCreditDebitLineItemEntity 集合,一些 AbstractCreditDebitLineItemEntity 对象将在两个集合中出现两次。在 memory/computation 方面有点浪费,但我同意,它有效!

这就是我最终得到的结果:

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class AbstractCreditDebitEntity {
/* literally all my properties and ID and column mappings here 
...
*/

  private List<AbstractCreditDebitLineItemEntity> creditDebitLineItems;

  @OneToMany(fetch = FetchType.LAZY, targetEntity = AbstractCreditDebitLineItemEntity.class)
  @JoinColumn(
      name = "MyIdColumnName",
      referencedColumnName = "MyIdColumnName",
      updatable = false,
      insertable = false
  )
  public List<AbstractCreditDebitLineItemEntity> getCreditDebitLineItems() {
    return creditDebitLineItems;
  }

  public void setCreditDebitLineItems(List<AbstractCreditDebitLineItemEntity> items) {
    creditDebitLineItems = items;
  }

}
@Entity
@Table(name = "tblCredits")
public final class Credit extends AbstractCreditDebitEntity {

  private List<CreditLineItem> creditLineItems;

  @OneToMany(cascade = CascadeType.ALL, targetEntity = CreditLineItem.class)
  @LazyCollection(LazyCollectionOption.FALSE)
  @JoinColumn(
      name = "MyIdColumnName",
      referencedColumnName = "MyIdColumnName"
  )
  public List<CreditLineItem> getCreditLineItems() {
    return creditLineItems;
  }

  @Override
  public void setCreditDebitLineItems(List<CreditLineItem> items) {
    creditLineItems =  items;
  }

}

对借方实体重复完全相同的模式。

这让我可以:

  1. 坚持使用从具体的 Credit 和 Debit 实体到具体的 CreditLineItem 和 DebitLineItem 实体的 OneToMany 映射;和

  2. do 使用从该抽象实体到 AbstractCreditDebitLineItemEntity 的完全独立的 OneToMany 映射在 AbstractCreditDebitEntity 的 Spring Data JPA 存储库中找到。

不像我能够用具体子 class 中更具体的 OneToMany 映射覆盖抽象父 class 中的 OneToMany 映射那样干净...但是我说,有用!

(关于这个问题的答案让我知道我需要用 @LazyCollection(LazyCollectionOption.FALSE) 替换具体的 OneToMany 映射中的 fetchType=FetchType.EAGER: Hibernate throws MultipleBagFetchException - cannot simultaneously fetch multiple bags)