I am having problems with EclipseLink change tracking in one of my entity classes in a Java SE application. I am using Java 8, JPA 3.0 provided by EclipseLink 3.0.2 and HyperSQL 2.6.1. So far I have kept my implementation provider-independent, so switching JPA providers is an option, although not preferable.
This particular entity class has ~10 attributes of the type OverriddenValue
, each of which is a wrapper for 1. a reference to a particular global configuration value, and 2. an optional custom value which will override the global value if present.
public class OverriddenValue<T> { @Nullable private T customValue; private final OverridableValue<?, T> globalConfigValue; [...] }
This class contains getter and setter logic which would make it very inconvenient to store the custom values in the entity directly. So I need these custom values to be wrapped.
Each one of these OverriddenValue
s in my entity class I have marked with @Convert
using a unique AttributeConverter
. All of these AttributeConverters simply return the custom value for the Java -> DB mapping, and for the DB -> Java mapping they reconstruct the object with the correct global configuration OverridableValue
. It is because I need the reference to the OverridableValue
that I did not implement OverriddenValue
as an @Embeddable
– I would have either have had to persist the ID of the global configuration value, or make it transient, and I decided that was too inconvenient. Besides, each OverriddenValue
really only needs one column in the database to store its custom value or null, and so @Convert
should be up to the job.
My problem is that EclipseLink does not detect and persist changes to these objects. In a managed instance of this entity, a change to a basic String attribute will be automatically detected and persisted at the next call to EntityManager#flush
, but a change to the customValue of an OverriddenValue
will not, and these columns in the database will remain as they were.
I looked up how EclipseLink’s change tracking works and found someone saying that it uses .hashCode() and .equals() to determine if an attribute has changed. So I manually implemented these in the OverriddenValue
class, but they must have been wrong since the changes are still not being detected.
Momentarily abandoning provider-independence, I tried marking this entity with EclipseLink’s @ChangeTracking
annotation and changing the ChangeTrackingType
to DEFERRED
, but this only caused already-detected changes to be delayed and did not enable detection of any new ones. The other tracking types (ATTRIBUTE
and OBJECT
) require the entity to implement a particular interface ChangeTracker
which seems like it could be helpful, but I don’t quite understand how to make it work.
I have also tried setting the property “eclipselink.weaving.changetracking” to false in the persistence.xml file, on the off chance that weaving was causing the issue (I don’t really understand weaving). No luck.
As a possible workaround, I could manually merge all entities of this type on application shutdown and force overwrite the values in the database. But I consider this a hack and would like to avoid it if at all possible. I feel like the ORM provider should be capable of detecting wrapped attribute changes. Does anyone have an idea where I might look next to try and fix this?
EDIT: Here is an example of what the converter classes all look like:
@Converter public class FooConverter implements AttributeConverter<OverriddenValue<Integer>, Integer> { @Override default Integer convertToDatabaseColumn(OverriddenValue<Integer> attribute) { return attribute.getCustomValue(); } @Override public OverriddenValue<Integer> convertToEntityAttribute(Integer dbData) { return Config.GLOBAL_CONFIG_VALUE.override(dbData); // Every converter references a different global variable } }
The method override
is just an OverriddenValue
factory method.
Advertisement
Answer
Try marking the mapping as mutable:
@Entity public class YourClass { .. @Convert("yourConverter") @Mutable private OverriddenValue value1; .. }
Alternatively, you might modify your own save methods to clone and set the OverriddenValue instance when you know there are changes within it to be persisted.
YourClass instance = em.find(id, YourClass.class); instance.setValue1(instance.getValue1().clone()); instance.getValue1().setCustomValue(value) em.commit();