3

Let's assume a simple 1 Father -> n Children entity relationship (getters/setters omitted):

@Entity
public class Father {

    @Id
    @GeneratedValue
    private Integer id;

    @OneToMany(mappedBy="father", cascade=CascadeType.ALL)
    private List<Child> children = new ArrayList<>();

    ...
}

@Entity
public class Child {
    @Id
    @GeneratedValue
    private Integer id;

    @ManyToOne(cascade=CascadeType.ALL)
    private Father father;

    ...
}

When now trying to update that relationship by simply adding a child to the father, it doesn't work, since Child.father does not get updated:

Child child = new Child();
// optionally persist child first, tried
father.getChildren().add(child);
...save father

I tried using different CascadeTypes, but nothing seems to work to automatically update the Child.father reference. I know that a traditional way would be to have an "addChild" method, but I would like the know if there is an automatic way to do so?

My configuration is Spring Boot + JPA (Hibernate) + H2, automatically creating to database. Interesting enough, I have found some blog postings that seem to do exactly what I do here, but for them it works, but not for me. Strange.

Jens Schauder
  • 77,657
  • 34
  • 181
  • 348
Florian Schaetz
  • 10,454
  • 5
  • 32
  • 58

1 Answers1

2

Yes, but it's not intended. I only really explain it because it may explain what you've seen in those blog posts. You can make the @OneToMany the owning side of the relationship by removing mappedBy and adding @JoinColumn. However, this is primarily intended for the case where you don't have a back reference. If you keep the @ManyToOne then Hibernate will see two relationships which happen to share the same columns. As far as I know however, it should work.

I have seen one unjustified reference once that it could cause performance issues but I can't find that reference any longer so I don't know for certain. There will be two issues with it which kind of demonstrate the "this isn't really how you should do this" nature of the option:

  1. The parent field will not be updated until the transaction/session is closed and the child entity is reloaded from the database.
  2. If you later decide to implement 2nd-level caching it can change the logic of your code. This is because the child entity might not be reloaded from the database for some time and, until it is, the parent field will be null.

You could implement this yourself with dynamic proxies and reflection, although you would have to control the creation of entities through a DAO/Repository to ensure they got proxied.

In the end, I wouldn't personally recommend it. The older I get the more I feel that the less black magic there is in your application the better.

Pace
  • 41,875
  • 13
  • 113
  • 156
  • Thanks for the answer. The "problem" here is not so much the JPA behavior, as the results from it in Spring Data Rest, which allows me to PUT to the /father/X/children collection - but doesn't do anything there, since it correctly adds to the collection, but this adding does not change anything... – Florian Schaetz May 07 '16 at 13:40
  • Ah, I think the standard approach for adding a child to a father is to do `PUT /child/X/father`. If you need to do a bulk addition of children this article mentions a way you might be able to do it using a `uri-list` http://stackoverflow.com/questions/25311978/posting-a-onetomany-sub-resource-association-in-spring-data-rest – Pace May 07 '16 at 13:46
  • That's pretty much what doesn't work for me, since the last step gets done (the child/comment is put or patched into the list, but that doesn't update its parent/post reference). It works with your workaround, but as you said, that might not be a good idea. So either I would want the whole thing to fail instead of seeming to suceed (204) or work. – Florian Schaetz May 07 '16 at 17:49
  • I'm confused. Are you saying that a `PUT` to `/child/X/father` (not to `father/X/children`) updates the collection and not the parent reference? That seems very counter-intuitive to me. If anything I would expect it to update the parent reference and not the collection. – Pace May 08 '16 at 13:05
  • No, updating `/children/x` with `{ "father" : "http://..." }` works, have not tried it with `/children/x/father` yet. Only the way doesn't work, but it returns 204, as if it worked, which is imho very counter-intuitive, too. – Florian Schaetz May 08 '16 at 13:11
  • I see your point now and agree. At the very least a warning in the log would be helpful. My guess is that Hibernate just ignores collections entirely on a save when they are marked with `mappedBy` – Pace May 08 '16 at 13:22
  • Possible. What I'm still searching now is "the" way to correctly map this. I could, of course, use some Event listener to handle it. Or add custom controller to throw an error when trying to put there. But somehow, I feel it's kind of strange that Spring Data Rest tries something like this and fails at it and it should be "ok". – Florian Schaetz May 08 '16 at 13:51
  • I don't use Spring Data so I can't speak to that but I have worked on a public API and we had several fields that were read only (collections like this, status fields, dynamically computed fields, etc.) and so we had a special annotation we marked those fields with so that we could throw exceptions if customers tried to modify them. – Pace May 08 '16 at 14:05
  • No problem to do so here, of course, but the point of Spring Data Rest is to do as much as possible automatically. You can simply expose your repositories as REST services. So it feels strange to invest time manually there and I was hoping for some better option via hibernate or similar. – Florian Schaetz May 08 '16 at 14:46
  • You might try creating a method with `@PreUpdate / @PrePersist` that runs though the children and sets the parent reference correctly. Would this be more palatable? In Java 8 you may even be able to do this with a default method on an interface if you have many different parent types. Not fully worked that one through in my head though. – Pace May 09 '16 at 02:03
  • Personally, I would have tried a Spring BeforeSaveEvent next, but it's the same idea, of course. Unfortunately, I'm not sure if it's a much better than idea than the @JoinColumn... – Florian Schaetz May 09 '16 at 05:31