1

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 = "account" 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="account_id") - I tried this but also got an exception (stated below)
  private Account account;

  @OneToMany(targetEntity = Budget.class, cascade = CascadeType.ALL, 
    mappedBy = "parentBudget", orphanRemoval=true)
  @Fetch(value = FetchMode.SUBSELECT)
  private List<Budget> subBudget; 

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

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

the database

The error message in detail is:

Referential integrity constraint violation: "FKovatioxlljiymn9haxf1yrjs7: PUBLIC.Account FOREIGN KEY(mainBudget_id) REFERENCES PUBLIC.Budget(id) (2259)"; 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<Budget> objs = getAllById(id, resource); //SELECT * FROM Budget WHERE id=...

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

    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

lasbr
  • 79
  • 1
  • 8
  • First, the database doesn't look correct. It shows multiple budgets all pointing at the same account (id 76) when your model shows that an Account can only have one budget associated to it. So it is very wrong for your model. If you need multiple budgets associated to the same account, it is a OneToMany relationship (and ManyToOne back), not a bidirectional 1:1. – Chris Apr 10 '23 at 16:09
  • @Chris I understand what you mean, but interestingly enough this constellation worked eagerly for a long time with a huge amount of data. The problem is, that an Account should only have one mainBudget. (budget level 1) A mainBudget then can have subBudgets (budget level 2) and subBudgets can also have subBudgets (level 3 = max level). Now each of the budgets in such a budget tree should have the account as an association. The account although only should have the main budget. You say this can not be done with OneToOne? – lasbr Apr 10 '23 at 18:23
  • @Cris In my mind it would just be the account table containing the mainBudget id and all the subbudget tables are containing the account id. The mainBudget is the `@OneToOne` association and the rest of the budgets have a `@ManyToOne` association to the account, but the account does not have an `@OneToMany` association to these budgets. How should I model this? Should I make the MainBudget a special type of Budget? Should I fake it with `@OneToMany` from the account perspective and filter for the mainBudget on loading the account? – lasbr Apr 10 '23 at 18:27
  • Your mapping has no concept of a 'main' budget - it just goes looking for any 'one' budget that references the account with that ID; this is what I mean by it being incorrect. It is non-deterministic in which one it will find. There are a few ways to map this if your DB design is what you want, but note that your model is more complex as your 'child' chaining has no guarantee they are all pointing at the same account. You might want the root parent associated to an account, and just let the children leave the account fk as null. – Chris Apr 10 '23 at 20:05

2 Answers2

2

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 = "account", cascade = CascadeType.ALL, orphanRemoval=true, fetch=FetchType.LAZY) 
  private List<Budget> budgets;
  } 
...
}

if you must have a 'main' budget reference and it must be the root parent, you can query for it directly using "select mainBudget from Budget mainBudget where mainBudget.parentBudget is null and mainBudget.account = :account". 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 = "parentBudget is null")
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.

Chris
  • 20,138
  • 2
  • 29
  • 43
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 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.

Mauricio Buffon
  • 332
  • 1
  • 8
  • 1
    I tried this approach, and it seems to work better than my approach, but whenever I create a subBudget for a subBudget for some reason the mainBudget of the related account is changed to the created budget. I don't see why the creation of a sub-subBudget would have such a side effect. I found out that the side effect occurs, when I set the parentBudget for the new budget to the subBudget of the mainBudget and then load the parentBudget. – lasbr Apr 12 '23 at 14:46
  • 1
    Well, it's hard to blind guess what is happening in this case. Looking the code you provided, my first attempt would be reviewing the `cascade = ALL` annotations. I had trouble with them in the past and I found very few situations where I really needed them. So, try to remove some of them. In the case you are creating more than one Budget per transaction, maybe you need just a `cascade= PERSIST` on the `private List subBudget` relation. – Mauricio Buffon Apr 12 '23 at 16:04
  • 1
    That was my first guess too, but I now found out, that the budgets and accounts have to be fully loaded (i.e. all budgets and the account have to be loaded) when creating or updating in the respective object in the database – lasbr Apr 12 '23 at 18:45