views:

125

answers:

1

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.
A: 

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.

Pascal Thivent
"the null fields will be set to null in the database" not the case if I use 'updatable = false'
Mykola Golubyev
I have 40+ fields in the entity, it is not that good to have 40 set and get calls for a such simple task.
Mykola Golubyev
@Mykola Indeed, not the case if you use "updatable=false". But still, my point was to either use merge (and to return the merged instance) or a "manual merge". Your current approach looks weird.
Pascal Thivent
Having 40 sets and gets is more weird. Is it so strange to have a read only view? Anyway what is the reason to merge field which even won't be stored in the database (updatable = false)
Mykola Golubyev
@Mykola Well, `updatable=false` just means the field won't be part of the SQL update, nothing more, I wouldn't (and I don't) expect behavioral changes at the object level. In other words, this doesn't mean the field is "readonly".
Pascal Thivent
merge is a part of JPA. updatable = false is a part of JPA. We do not talk about Apache generic bean utils but about specific JPA merge which we can't use on objects which are not entities and this merge doesn't take in account entity meta info.
Mykola Golubyev
@Mykola `updatable=false` works like I said (it ONLY means the **column** won't be part of the SQL UPDATE statement), and `merge` works like I said. They just don't work like YOU want them to work. I suggest sending your wishes to the Expert Group for JPA 3.0.
Pascal Thivent