JPA merge readonly fields

2019-05-04 15:43发布

问题:

We have the simplest CRUD task with JPA 1.0 and JAX-WS.
Let's say we have an entity Person.

@Entity
public class Person
{
   @Id
   private String email;

   @OneToOne(fetch = FetchType.LAZY)
   @JoinColumn(insertable = false, updatable = false)
   private ReadOnly readOnly;

   @Column
   private String name;      

   @XmlElement
   public String getEmail()
   {
      return email;
   }

   public void setEmail(String email)
   {
      this.email = email;
   }

   @XmlElement
   public Long getReadOnlyValue()
   {
      return readOnly.getValue();
   }

   // more get and set methods
}

Here is scenario. Client make Web Service request to create person. On the server side everything is straightforward. And it does work as expected.

@Stateless
@WebService
public class PersonService
{
   @PersistenceContext(name = "unit-name")
   private EntityManager entityManager;

   public Person create(Person person)
   {
      entityManager.persist(person);

      return person;
   }
}

Now client tries to update person and this is where, as for me, JPA shows its inconsistence.

public Person update(Person person)
{
   Person existingPerson = entityManager.find(Person.class, person.getEmail());

   // some logic with existingPerson
   // ...      

   // At this point existingPerson.readOnly is not null and it can't be null
   // due to the database.
   // The field is not updatable.
   // Person object has readOnly field equal to null as it was not passed 
   // via SOAP request.
   // And now we do merge.

   entityManager.merge(person);

   // At this point existingPerson.getReadOnlyValue() 
   // will throw NullPointerException. 
   // And it throws during marshalling.
   // It is because now existingPerson.readOnly == person.readOnly and thus null.
   // But it won't affect database anyhow because of (updatable = false)

   return existingPerson;
}

To avoid this problem I need to expose set for readOnly object and do something like this before merge.

Person existingPerson = entityManager.find(Person.class, person.getEmail());
person.setReadOnlyObject(existingPerson.getReadOnlyObject()); // Arghhh!

My questions:

  • Is it a feature or just inconsistence?
  • How do you (or would you) handle such situations? Please don't advice me to use DTOs.

回答1:

Is it a feature or just inconsistence?

I don't know but I'd say that this is the expected behavior with merge. Here is what is happening when calling merge on a entity:

  • the existing entity gets loaded in the persistence context (if not already there)
  • the state is copied from object to merge to the loaded entity
  • the changes made to the loaded entity are saved to the database upon flush
  • the loaded entity is returned

This works fine with simple case but doesn't if you receive a partially valued object (with some fields or association set to null) to merge: the null fields will be set to null in the database, this might not be what you want.

How do you (or would you) handle such situations? Please don't advice me to use DTOs.

In that case, you should use a "manual merge": load the existing entity using find and update yourself the fields you want to update by copying the new state and let JPA detect the changes and flush them to the database.