The way OpenJPA deals with collections can cause some surprising defects if you don't understand what OpenJPA is doing for you. This post explains some OpenJPA behaviour in order to help prevent future defects and find current ones.
OpenJPA internals
OpenJPA records which fields are dirty and only updates dirty fields to the database. Much of what the byte code enhancer does is to add code to set methods to mark the fields as dirty. OpenJPA has a problem when it comes to fields which are collections because if you add something to a collection then the parent object will not know about it and therefore cannot set the field to dirty. OpenJPA deals with this situation by using something called smart proxies. When you change a collection that is a smart proxy, OpenJPA will go to the parent object and mark the field as dirty so that you do not normally need to worry about it.
Prevention
Always use the objects returned from OpenJPA methods for further work. Watch how you use collections and make sure you don't put references to them in places that are not updated when the object is saved to the database.
Example defect
Sometimes abstractions bleed and cause surprising failures. Here is an example I found:
customer = (Customer) beanFactory.getBean(ContextIdNames.CUSTOMER);
customer.setAnonymous(true);
customer.setEmail(billingAddress.getEmail());
customer.setStore(store);
this.customerService.validateNewCustomer(customer);
// Anonymous customers are currently persisted before checkout so that their addresses can be
// assigned UIDPKs, which is necessary for manipulating addresses during checkout.
customer = customerService.add(customer);
customer.setFirstName("James");
customer = customerService.update(customer);
It looks okay, but after this code ran, the database did not have the first name set. That's because CustomerImpl had:
public void setEmail(final String email) {
this.getCustomerProfile().setStringProfileValue(ATT_KEY_CP_EMAIL, email);
...
}
public CustomerProfile getCustomerProfile() {
if (this.customerProfile == null) {
initializeCustomerProfile();
}
return this.customerProfile;
}
private void initializeCustomerProfile() {
customerProfile = (CustomerProfile) getElasticPath().getBean(ContextIdNames.CUSTOMER_PROFILE);
customerProfile.setProfileValueBeanId(ContextIdNames.CUSTOMER_PROFILE_VALUE);
customerProfile.setProfileValueMap(getProfileValueMap());
}
So, when setEmail() is called on the newly created object, the CustomerProfile is initialized with a plain hashmap. CustomerService.add gets called and the profile value map is updated with an OpenJPA smart proxy. However, this smart proxy is never placed into the CustomerProfile object so when the setFirstName() method is called the plain hashmap gets updated and OpenJPA never gets a chance to set the field dirty -- and therefore, the database doesn't get updated.