2

This question sort of asks what I'm trying to achieve, but there isn't really an answer : Hibernate validateManyToOnehas at least one

I have two objects (A and B). A is the parent. B is the child. It's a one to many relationship, however, I need there to always be at least one B for each A. There are default values for all fields in B, so if an A is created without a B then a default B can be added to make sure there is always one B. If one or more B objects are added A then there's no need to create a default B.

This is A:

[Fields]

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "key", nullable = false)
@Fetch(value = FetchMode.SUBSELECT)
private List<B> b = new ArrayList<>();

...

@PrePersist
protected void onCreate() {
    // Default values configured here, for example
    if (fieldA1 == null) {
        fieldA1 = "A DEFAULT";
    }
    ...
}

This is B:

[Fields]

@PrePersist
protected void onCreate() {
    // Default values configured here, for example
    if (fieldB1 == null) {
        fieldB1 = "B DEFAULT";
    }
    ...
}

I thought I could use the same @PrePersist annotation in A, check if there are any B objects, and if not create a default B:

@PrePersist
protected void onCreate() {
    // Deafult values configured here
    ...
    if (b.size() == 0) {
        b.add(new B());
    }
}

That doesn't work. If I create an A with no B objects then in the log I just get:

Handling transient entity in delete processing

and A is created without the B. If I try and create an A with at least one B then I get the following error:

Caused by: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing

Any ideas how I can make this work?

Morteza Jalambadani
  • 2,190
  • 6
  • 21
  • 35
  • Btw, I want to commend you for your question. It's rare to see new contributors that put so much effort into their questions. – Thomas Aug 22 '18 at 07:11
  • @Thomas, thanks for the kind words. It might be my first question but I've been lurking around here for many many years :) I've seen good questions, and bad questions, and that helps to know how best to structure it. – user9946196 Aug 22 '18 at 09:39

1 Answers1

0

Without knowing more of your code (and it might be a little too much for SO anyways) I'll have to make some assumptions but I'd say that when onCreate() is being called the Hibernate session is already in a "flushing" state and thus won't accept any new entities.

Currently I could think of 2 options which we also use in some cases:

  1. Throw a CDI event and have an event handler (asynchronously) trigger the creation of the default element in a new transaction once the current transaction is completed successfully.

  2. Create a sub-session in Hibernate that forks from the current one and uses the same transaction. Then use this sub-session to create your default element.

Here's how we do it in a Hibernate PostInsertEventListener:

if( postInsertEvent.getEntity() instanceof ClassThatNeedsToBeHandled ) {
  ClassThatNeedsToBeHandled insertedEntity = (ClassThatNeedsToBeHandled )postInsertEvent.getEntity(); 
  Session subSession = postInsertEvent.getSession().sessionWithOptions()
    .connection() //use the same connection as the parent session                  
    .noInterceptor() //we don't need additional interceptors
    .flushMode( FlushMode.AUTO ) //flush on close
    .autoClose( true) //close after the transaction is completed
    .autoJoinTransactions( true ) //use the same transaction as the parent session
    .openSession();

  subSession.saveOrUpdate( new SomeRelatedEntity( insertedEntity ) );
}

One thing to keep in mind is that our SomeRelatedEntity is the owner of the relation. Your code indicates that A would be the owner but that might cause problems because you'd have to change the A instance during flush to get the relation persisted. If B was the owning side (it has a backreference to A and in A you have a mappedBy in your @OneToMany) it should work.

Edit:

Actually, there might be a 3rd option: when you create a new A add a default element and when real elements are added you remove it. That way you'd not have to mess with the Hibernate sessions or transaction scopes.

Thomas
  • 87,414
  • 12
  • 119
  • 157
  • 1
    The 3rd option was what I thought about first as this shouldn't add any more complexity to the hibernate part (which can sometimes be veery tricky) – XtremeBaumer Aug 22 '18 at 07:14
  • @XtremeBaumer oh yes, it can :) – Thomas Aug 22 '18 at 07:42
  • Thanks for explaining why I'm seeing those messages/errors. Makes sense. I was trying to intercept the lack of child objects at the hibernate level because that seemed to make sense, especially as I am already setting default values in the @PrePersist. Your 3rd option is definitely the way to go as there doesn't appear to be a straightforward hibernate way to solve this. I've implemented your suggestion and creating default child objects in the rest controller works well. – user9946196 Aug 22 '18 at 09:48