Hibernate – Lazily fetch @OneToOne association with same parent referred by multiple recursively mapped children

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

Hibernate - Lazily fetch @OneToOne association with same parent referred by multiple recursively mapped children

问题

以下是您要翻译的内容:

The following constellation gives me a org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException:

The associations:

1 Account (account) <-> (mainBudget) Budget 0..1
0..1 Budget (parentBudget) <-> (subBudget) Budget *

The actual implementation of an Account objects and Budget objects would look like this:

Forwards:

Account -> (mainBudget) Budget -> (subBudget) Budget -> (subBudget) Budget

Backwards:

Budget -> (parentBudget) Budget -> (parentBudget) Budget -> (account) Account

Each Budget in the budget tree (max height 3 as modeled) has the same account associated to it, where the account only has the mainBudget associated.

For the Account.java:

@Audited
@Entity
public class Account {
  @Id
  @GeneratedValue(strategy = GenerationType.TABLE)
  protected long id;

  @OneToOne(cascade = CascadeType.ALL, orphanRemoval=true, fetch=FetchType.LAZY) 
// Here I also tried mappedBy = &quot;account&quot; with @JoinColumn for the budget (with the exception stated at the end)
  @LazyToOne(LazyToOneOption.PROXY)
  private  Budget mainBudget;
  } 
...
}

For the Budget.java:

@Audited
@Entity
public class Budget {
  @Id
  @GeneratedValue(strategy = GenerationType.TABLE)
  protected long id;

  @OneToOne(fetch=FetchType.LAZY)
  //@JoinColumn(name=&quot;account_id&quot;) - I tried this but also got an exception (stated below)
  private Account account;

  @OneToMany(targetEntity = Budget.class, cascade = CascadeType.ALL, 
    mappedBy = &quot;parentBudget&quot;, orphanRemoval=true)
  @Fetch(value = FetchMode.SUBSELECT)
  private List&lt;Budget&gt; subBudget; 

  @ManyToOne(fetch=FetchType.LAZY)
  @JoinColumn(name=&quot;parentBudget_id&quot;)
  private Budget parentBudget; 
...
} 

The in-memory H2 database looks the following (which seems fine to me):

Hibernate – Lazily fetch @OneToOne association with same parent referred by multiple recursively mapped children

The error message in detail is:

Referential integrity constraint violation: &quot;FKovatioxlljiymn9haxf1yrjs7: PUBLIC.Account FOREIGN KEY(mainBudget_id) REFERENCES PUBLIC.Budget(id) (2259)&quot;; SQL statement:
delete from Budget where id=? [23503-200]

The delete-method looks like this:

  @Transactional
  public boolean delete(long id, String resource)
      throws NoSuchElementException {
    List&lt;Budget&gt; objs = getAllById(id, resource); //SELECT * FROM Budget WHERE id=...

    if (objs.isEmpty()) {
      throw new NoSuchElementException(
          &quot;Delete not possible.&quot;);
    }

    router.setDataSource(resource);

    for (Budget obj : objs) {
      em.remove(obj);
      em.flush(); // Here the exception is thrown
    }
    return true;
  }

Why do I get a referential integrity constraint violation exception? The same mappings worked before with EAGER loading.

As stated as comments in the code, if I use mappedBy for the parent and @JoinColumn for the child I as suggested here, I get the exception:

org.hibernate.HibernateException: More than one row with the given identifier was found: 50, for class: ...Budget
英文:

The following constellation gives me a org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException:

The associations:

1 Account   (account)      &lt;-&gt;  (mainBudget) Budget 0..1
0..1 Budget (parentBudget) &lt;-&gt;  (subBudget) Budget *

The actual implementation of an Account objects and Budget objects would look like this:

Forwards:

Account -&gt; (mainBudget) Budget -&gt; (subBudget) Budget -&gt; (subBudget) Budget

Backwards:

Budget -&gt; (parentBudget) Budget -&gt; (parentBudget) Budget -&gt; (account) Account

Each Budget in the budget tree (max height 3 as modeled) has the same account associated to it, where the account only has the mainBudget associated.

For the Account.java:

@Audited
@Entity
public class Account {
  @Id
  @GeneratedValue(strategy = GenerationType.TABLE)
  protected long id;

  @OneToOne(cascade = CascadeType.ALL, orphanRemoval=true, fetch=FetchType.LAZY) 
// Here I also tried mappedBy = &quot;account&quot; with @JoinColumn for the budget (with the exception stated at the end)
  @LazyToOne(LazyToOneOption.PROXY)
  private  Budget mainBudget;
  } 
...
}

For the Budget.java:

@Audited
@Entity
public class Budget {
  @Id
  @GeneratedValue(strategy = GenerationType.TABLE)
  protected long id;

  @OneToOne(fetch=FetchType.LAZY)
  //@JoinColumn(name=&quot;account_id&quot;) - I tried this but also got an exception (stated below)
  private Account account;

  @OneToMany(targetEntity = Budget.class, cascade = CascadeType.ALL, 
    mappedBy = &quot;parentBudget&quot;, orphanRemoval=true)
  @Fetch(value = FetchMode.SUBSELECT)
  private List&lt;Budget&gt; subBudget; 

  @ManyToOne(fetch=FetchType.LAZY)
  @JoinColumn(name=&quot;parentBudget_id&quot;)
  private Budget parentBudget; 
...
} 

The in-memory H2 database looks the following (which seems fine to me):

Hibernate – Lazily fetch @OneToOne association with same parent referred by multiple recursively mapped children

The error message in detail is:

Referential integrity constraint violation: &quot;FKovatioxlljiymn9haxf1yrjs7: PUBLIC.Account FOREIGN KEY(mainBudget_id) REFERENCES PUBLIC.Budget(id) (2259)&quot;; SQL statement:
delete from Budget where id=? [23503-200]

The delete-method looks like this:

  @Transactional
  public boolean delete(long id, String resource)
      throws NoSuchElementException {
    List&lt;Budget&gt; objs = getAllById(id, resource); //SELECT * FROM Budget WHERE id=...

    if (objs.isEmpty()) {
      throw new NoSuchElementException(
          &quot;Delete not possible.&quot;);
    }

    router.setDataSource(resource);

    for (Budget obj : objs) {
      em.remove(obj);
      em.flush(); // Here the exception is thrown
    }
    return true;
  }

Why do I get a referential integrity constraint violation exception? The same mappings worked before with EAGER loading.

As stated as comments in the code, if I use mappedBy for the parent and @JoinColumn for the child I as suggested here, I get the exception:

org.hibernate.HibernateException: More than one row with the given identifier was found: 50, for class: ...Budget

答案1

得分: 2

以下是翻译好的部分:

正如在评论中提到的,您的账户映射到预算没有主预算或父预算的概念,因此所有引用此帐户的预算都是有效的,并可用于帐户->预算引用。这是整个应用程序的问题,但在删除帐户时特别成问题,因为只有一个引用的预算 - 因此,JPA仅级联删除那一个预算实例。您会收到引用约束异常,因为您已在数据库中保留了所有其他预算。

您可以通过在您之前让数据库清理实例来解决删除的问题(但我强烈建议不这样做)。这取决于数据库,但许多数据库都有级联删除选项,这样它们将在删除帐户时清理预算行。

在我看来,更好的方法是将关系映射为一对多。

@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.TABLE)
protected long id;

@OneToMany(mappedBy = "account", cascade = CascadeType.ALL, orphanRemoval=true, fetch=FetchType.LAZY)
private List budgets;
}
...
}

如果您必须有一个'主'预算引用,并且它必须是根父预算,您可以直接使用"select mainBudget from Budget mainBudget where mainBudget.parentBudget is null and mainBudget.account = :account"进行查询。您还可以在需要时直接在帐户中维护这个关系(但这将触发集合)。

然而,一种非JPA的方法是在您的帐户实例中使用Hibernate特定的过滤来进行映射:

@OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
@Where(clause = "parentBudget is null")
private Budget mainBudget;

但您在评论中提到,您可能需要一种方法来标记一个预算为主要预算,以便帐户可以引用它。这只需通过从帐户->预算添加外键来完成:

@OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
private Budget mainBudget;

需要注意的是,您的'主'预算可以是任何预算。它完全独立于预算->帐户映射,可以是任何预算图中的任何预算(不一定是父预算)。

如果您希望在删除帐户时JPA清理所有预算,仍然需要保留一个从帐户->预算的一对多引用,以便JPA知道它们存在并进行清理。

如果双方(预算和帐户)都将具有对方的外键,请确保允许约束被延迟或允许外键为空。否则,您将在插入、更新和删除时遇到问题。

英文:

As mentioned in a comment, your Account mapping to Budget has no concept of a main or parent budget, so all budgets referencing this account are just as valid and usable for the Account->Budget reference. This is a problem in the application as a whole, but is specifically a problem when you delete an Account as there is only one budget referenced - So JPA only cascades and delete that one budget instance. You get a referential constraint exception because you've left all the others in the DB.

You can get around the deletion quirk (but I strongly advise against it) by having the DB clean up instances before you. It is DB dependent, but many have a cascade on delete option, so that they will clean up Budget rows when the account is deleted.

Better IMO is to map the relationship as it really is: A one to many.

@Entity
public class Account {
  @Id
  @GeneratedValue(strategy = GenerationType.TABLE)
  protected long id;

  @OneToMany(mappedBy = &quot;account&quot;, cascade = CascadeType.ALL, orphanRemoval=true, fetch=FetchType.LAZY) 
  private List&lt;Budget&gt; budgets;
  } 
...
}

if you must have a 'main' budget reference and it must be the root parent, you can query for it directly using &quot;select mainBudget from Budget mainBudget where mainBudget.parentBudget is null and mainBudget.account = :account&quot;. You can also maintain this in the Account directly by going over the collection when needed (but this will trigger the collection).

A non-JPA way though would be to also map it in your Account instance using Hibernate specific filtering:

@OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
@Where(clause = &quot;parentBudget is null&quot;)
private  Budget mainBudget;

But you mentioned in comments you might need a way to mark a budget as the main one so that an Account can reference it. This is simply done by adding a foreign key from Account->budget that tracks the main one:

@OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
private Budget mainBudget;

Note though that your 'main' budget can be any budget. It is completely independent from the the Budget->Account mappings, and can be any budget in any budget graph (not necessarily a parent).

If you want JPA to clean up all Budgets when removing an Account though, you'll still need to keep a OneToMany reference from account->Budget so that JPA knows they exist and to clean them up.

If both sides (budget and account) are going to have foreign keys to the other, be sure to allow constraints to be deferred or allow fks to be nullable. Otherwise you'll get into problems with inserts, updates and deletes.

答案2

得分: 1

The way I see it, your model has two uni-directional relationships between Account and Budget.

It has * - 1 from Budget to Account.
It has 1 - 0..1 from Account to Budget.

I could make it work writing the following entities:

@Entity
public class Account {

    @Id
    private long id;

    @OneToOne
    @JoinColumn(name = "mainBudget_id")
    private Budget budget;
}

@Entity
public class Budget {

    @Id
    private long id;

    @ManyToOne(optional = false)
    @JoinColumn(name = "account_id")
    private Account account;

    @ManyToOne
    @JoinColumn(name = "parentBudget_id")
    private Budget parentBudget;

    @OneToMany(orphanRemoval = true, mappedBy = "parentBudget")
    private List<Budget> subBudget;
}

And using the same data you provided:

insert into Account(id, mainBudget_id) values (76, null);
insert into Budget(id, account_id, parentBudget_id) values (50, 76, null);
update Account set mainBudget_id = 50 where id = 76;
insert into Budget(id, account_id, parentBudget_id) values (51, 76, 50);
insert into Budget(id, account_id, parentBudget_id) values (52, 76, 51);
insert into Budget(id, account_id, parentBudget_id) values (55, 76, 51);
insert into Budget(id, account_id, parentBudget_id) values (58, 76, 51);
insert into Budget(id, account_id, parentBudget_id) values (61, 76, 51);
insert into Budget(id, account_id, parentBudget_id) values (64, 76, 50);
insert into Budget(id, account_id, parentBudget_id) values (65, 76, 64);
insert into Budget(id, account_id, parentBudget_id) values (68, 76, 50);
insert into Budget(id, account_id, parentBudget_id) values (69, 76, 68);
insert into Budget(id, account_id, parentBudget_id) values (72, 76, 50);
insert into Budget(id, account_id, parentBudget_id) values (73, 76, 72);
insert into Budget(id, account_id, parentBudget_id) values (2259, 76, 50);

This was enough to play around retrieving a list of budgets, deleting a budget, etc.

ATTENTION: Using the mapping provided here, you can delete any Budget in the tree. If the deleted budget isn't a leaf node, the persistence provider will delete the tree below it. Nevertheless, if you try to delete the main budget without updating the account entity first, it will fail.

英文:

The way I see it, your model has two uni-directional relationships between Account and Budget.

It has * - 1 from Budget to Account.
It has 1 - 0..1 from Account to Budget.

I could make it work writing the following entities:

@Entity
public class Account {

    @Id
    private long id;

    @OneToOne
    @JoinColumn(name = &quot;mainBudget_id&quot;)
    private Budget budget;
}

@Entity
public class Budget {

    @Id
    private long id;

    @ManyToOne(optional = false)
    @JoinColumn(name = &quot;account_id&quot;)
    private Account account;

    @ManyToOne
    @JoinColumn(name = &quot;parentBudget_id&quot;)
    private Budget parentBudget;

    @OneToMany(orphanRemoval = true, mappedBy = &quot;parentBudget&quot;)
    private List&lt;Budget&gt; subBudget;
}

And using the same data you provided:

insert into Account(id, mainBudget_id) values (76, null);
insert into Budget(id, account_id, parentBudget_id) values (50, 76, null);
update Account set mainBudget_id = 50 where id = 76;
insert into Budget(id, account_id, parentBudget_id) values (51, 76, 50);
insert into Budget(id, account_id, parentBudget_id) values (52, 76, 51);
insert into Budget(id, account_id, parentBudget_id) values (55, 76, 51);
insert into Budget(id, account_id, parentBudget_id) values (58, 76, 51);
insert into Budget(id, account_id, parentBudget_id) values (61, 76, 51);
insert into Budget(id, account_id, parentBudget_id) values (64, 76, 50);
insert into Budget(id, account_id, parentBudget_id) values (65, 76, 64);
insert into Budget(id, account_id, parentBudget_id) values (68, 76, 50);
insert into Budget(id, account_id, parentBudget_id) values (69, 76, 68);
insert into Budget(id, account_id, parentBudget_id) values (72, 76, 50);
insert into Budget(id, account_id, parentBudget_id) values (73, 76, 72);
insert into Budget(id, account_id, parentBudget_id) values (2259, 76, 50);

This was enough to play around retrieving a list of budgets, deleting a budget, etc.

ATTENTION: Using the mapping provided here, you candelete any Budget in the tree. If the deleted budget isn't a leaf node, the persistence provider will delete the tree below it. Nevertheless, if you try to delete the main budget without updating the account entity first, it will fail.

huangapple
  • 本文由 发表于 2023年4月10日 19:51:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/75976850.html
匿名

发表评论

匿名网友

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

确定