1

First let me say that I'm new to JPA. I've been tasked with maintaining some existing code that I did not write. I made one simple change, which was adding FetchType.LAZY to an existing @OneToOne annotation. This reduced the number of database calls being made when doing a query on one page in our application. However, it had the unintended consequence that on a separate page data is now being lost when doing a merge with the EntityManager. The merge works fine without FetchType.LAZY.

Here is some highly simplified code that illustrates the problem. The JPA entities:

@Entity
@Table(name = "parent")
public class Parent {

  @OneToOne(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
  private Child child;

  @Column(name = "name")
  private String name;

  // getters/setters omitted for brevity
}

@Entity
@Table(name = "child")
public class Child {

  @OneToOne
  @JoinColumn(name = "parent_id", nullable = false)
  private Parent parent;

  @Column(name = "name")
  private String name;

  // getters/setters omitted for brevity
}

Note that I added FetchType.LAZY to the @OneToOne annotation on the child property (look to the far right). That did not exist before.

Next, there exists a view-scoped bean that queries the database for a specific Parent in the postConstruct method. Later on, in a method called via ajax, the parentis refreshed and the child's name is reassigned. Finally, at some point later on the parent is saved.

@ManagedBean
@ViewScoped
public class DemoBean {

  @Inject
  private ParentDao parentDao;

  private Parent selectedParent;

  @PostConstruct
  public void postConstruct() {
    selectedParent = parentDao.findByName("Bob");
  }

  // Called via ajax sometime after postConstruct.
  public void changeChildName() {
    // Under the hood, `parentDao.refresh` calls `entityManager.refresh`.
    selectedParent = parentDao.refresh(selectedParent);
    selectedParent.getChild().setName("Joe");
  }

  // Called via ajax sometime after changeChildName.
  public void save() {
    // Under the hood, `parentDao.merge` calls `entityManager.merge`.
    selectedParent = parentDao.merge(selectedParent);
  }
}

The reason I post the code for the view-scoped bean is prior to asking this question on StackOverflow I wrote a single method that queries for a Parent, refreshes the parent, reassigns the child name, and then saves the Parent. However, I could not reproduce the problem if I did it all in a single method. So I'm guessing that somehow having these operations in separate methods that are called at separate times is part of the problem.

Anyways, the problem is that once I attempt to merge the Parent, JPA cascades the merge to the child (which is good), but the bad part is that it appears to "reload" the child from the database and overwrite my change. After the merge the change to the child's name is lost and not saved. How can I fix this while keeping FetchType.LAZY?

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
battmanz
  • 2,266
  • 4
  • 23
  • 32
  • Does it work in a unit test without jsf? – Kukeltje Feb 27 '19 at 07:20
  • 1
    Which JPA implementation (and version) do you use? The methods invoked from different HTTP AJAX requests are likely processed using different JPA sessions (a new one for each request). This seemes to be not related to JSF but to working with detached entities and multiple JPA sessions. – Selaron Feb 27 '19 at 08:09
  • If you do it in a single method and clear the entitymanager right before the merge, you'll have the same problem,. Learn about: `cascade = CascadeType.ALL` Maybe exclude the merge... https://stackoverflow.com/questions/13027214/what-is-the-meaning-of-the-cascadetype-all-for-a-manytoone-jpa-association. This might even be the duplicate of your question – Kukeltje Feb 27 '19 at 13:00

1 Answers1

1

You are cascading the refresh operation as well, wiping out any existing changes in the child object. This might be expected by the application, but now that the Parent->Child relationship is lazy, the timing of the refresh is also lazy and gets done when the relationship is first accessed. Any changes done before the selectedParent.getChild() access will be lost. Before you made it lazy, any changes to the child object before the refresh call would have been lost.

You will want to evaluate your cascade options and make sure they line up with what you really need from the application.

Chris
  • 20,138
  • 2
  • 29
  • 43
  • I believe this is the correct answer. I replaced `CascadeType.ALL` with an array that includes all cascade types *except* `CascadeType.REFRESH`. Now everything works as expected. However, that leaves me with one question: `selectedParent.getChild().setName("...")` is *definitely* being called after the call to `refresh`. But it's acting like the refresh doesn't happen until `merge` is called. Why is that? – battmanz Feb 28 '19 at 00:03
  • I would just be guessing, as I can't say how you have a view of these changes that are getting wiped out, but your merge operation is taking everything from the parent->Child graph and pushing it into the EntityManager context. This is everything from the last point you refreshed. So if this object you are calling merge on doesn't have your changes anymore, using it for the merge operation will wipe out any changes since that read. It would all depend on what changes are going missing. – Chris Feb 28 '19 at 16:50