OPENJPA-1873 fix PostLoad entity listener behaviour

This fix introduces a new flag POST_LOAD_ON_MERGE wich is disabled
by default, retaining the old behaviour.
Enabling it will guarantee that the Entity posted to PostLoad entity 
listeners are always the one from the database. This fixes the old
habit that PostLoad will also get triggered (with false/mixed values)
for lazy loading, merging, etc.



git-svn-id: https://svn.apache.org/repos/asf/openjpa/trunk@1211873 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Mark Struberg 2011-12-08 13:27:31 +00:00
parent 20772bc962
commit 4bffcc9c9a
13 changed files with 387 additions and 58 deletions

View File

@ -21,7 +21,6 @@ package org.apache.openjpa.conf;
import java.util.Collection;
import java.util.Map;
import org.apache.openjpa.kernel.AuditManager;
import org.apache.openjpa.audit.Auditor;
import org.apache.openjpa.datacache.CacheDistributionPolicy;
import org.apache.openjpa.datacache.DataCache;
@ -214,6 +213,12 @@ public interface OpenJPAConfiguration
public static final String OPTION_JDBC_CONNECTION =
"openjpa.option.JDBCConnection";
/**
* Option for enable fire @PostLoad events on merge operations
*/
public static final String OPTION_POSTLOAD_ON_MERGE =
"openjpa.option.PostLoadOnMerge";
/**
* Return the set of option strings supported by this runtime. This set
* is mutable.
@ -242,7 +247,7 @@ public interface OpenJPAConfiguration
* This change will trigger all registered Product Derivations to mutate
* other configuration properties.
*
* @param fullname of the specification that possibly encodes major and
* @param spec fullname of the specification that possibly encodes major and
* minor version information. For encoding format
* @see Specification
*
@ -258,7 +263,7 @@ public interface OpenJPAConfiguration
* This change will trigger all registered Product Derivations to mutate
* other configuration properties.
*
* @param fullname of the specification that possibly encodes major and
* @param spec fullname of the specification that possibly encodes major and
* minor version information. For encoding format
* @see Specification
*
@ -1894,6 +1899,24 @@ public interface OpenJPAConfiguration
* @since 2.2.0
*/
public void setAuditor(String s);
/**
* Whether to send @PostLoad events on a merge operation.
* @since 2.2.0
*/
public boolean getPostLoadOnMerge();
/**
* Whether to send @PostLoad events on a merge operation.
* @since 2.2.0
*/
public void setPostLoadOnMerge(boolean postLoadOnMerge);
/**
* Whether to send @PostLoad events on a merge operation.
* @since 2.2.0
*/
public void setPostLoadOnMerge(Boolean postLoadOnMerge);
}

View File

@ -176,6 +176,7 @@ public class OpenJPAConfigurationImpl
public BooleanValue dynamicEnhancementAgent;
public ObjectValue instrumentationManager;
public PluginListValue instrumentationProviders;
public BooleanValue postLoadOnMerge;
// custom values
public BrokerFactoryValue brokerFactoryPlugin;
@ -397,6 +398,10 @@ public class OpenJPAConfigurationImpl
optimistic.setDefault("true");
optimistic.set(true);
postLoadOnMerge = addBoolean("PostLoadOnMerge");
postLoadOnMerge.setDefault("false");
postLoadOnMerge.set(false);
autoClear = addInt("AutoClear");
aliases =
new String[] { "datastore",
@ -612,7 +617,8 @@ public class OpenJPAConfigurationImpl
supportedOptions.add(OPTION_VALUE_AUTOASSIGN);
supportedOptions.add(OPTION_VALUE_INCREMENT);
supportedOptions.add(OPTION_DATASTORE_CONNECTION);
supportedOptions.add(OPTION_POSTLOAD_ON_MERGE);
if (derivations)
ProductDerivations.beforeConfigurationLoad(this);
if (loadGlobals)
@ -1836,5 +1842,19 @@ public class OpenJPAConfigurationImpl
public void setAuditor(String auditor) {
auditorPlugin.setString(auditor);
}
public boolean getPostLoadOnMerge() {
return postLoadOnMerge.get();
}
public void setPostLoadOnMerge(boolean postLoadOnMerge) {
this.postLoadOnMerge.set(postLoadOnMerge);
}
public void setPostLoadOnMerge(Boolean postLoadOnMerge) {
if (postLoadOnMerge != null)
setPostLoadOnMerge(postLoadOnMerge.booleanValue());
}
}

View File

@ -594,6 +594,7 @@ public abstract class AbstractBrokerFactory
broker.setMultithreaded(_conf.getMultithreaded());
broker.setAutoDetach(_conf.getAutoDetachConstant());
broker.setDetachState(_conf.getDetachStateInstance().getDetachState());
broker.setPostLoadOnMerge(_conf.getPostLoadOnMerge());
}
/**

View File

@ -241,6 +241,7 @@ public class BrokerImpl
private boolean _cacheFinderQuery = true;
private boolean _suppressBatchOLELogging = false;
private boolean _allowReferenceToSiblingContext = false;
private boolean _postLoadOnMerge = false;
// status
private int _flags = 0;
@ -5199,27 +5200,33 @@ public class BrokerImpl
return ((_flags & FLAG_FLUSHING) != 0);
}
public boolean getPostLoadOnMerge() {
return _postLoadOnMerge;
}
public void setPostLoadOnMerge(boolean allow) {
_postLoadOnMerge = allow;
}
/**
* Asserts consistencey of given automatic detachment option value.
*/
private void assertAutoDetachValue(int value) {
if (((value & AutoDetach.DETACH_NONE) != 0) && (value != AutoDetach.DETACH_NONE)) {
throw new UserException(_loc.get("detach-none-exclusive", toAutoDetachString(value)));
}
}
/**
* Generates a user-readable String from the given integral value of AutoDetach options.
*/
private String toAutoDetachString(int value) {
List<String> result = new ArrayList<String>();
for (int i = 0; i < AutoDetach.values.length; i++) {
if ((value & AutoDetach.values[i]) != 0) {
result.add(AutoDetach.names[i]);
}
}
return Arrays.toString(result.toArray(new String[result.size()]));
}
/**
* Asserts consistencey of given automatic detachment option value.
*/
private void assertAutoDetachValue(int value) {
if (((value & AutoDetach.DETACH_NONE) != 0) && (value != AutoDetach.DETACH_NONE)) {
throw new UserException(_loc.get("detach-none-exclusive", toAutoDetachString(value)));
}
}
/**
* Generates a user-readable String from the given integral value of AutoDetach options.
*/
private String toAutoDetachString(int value) {
List<String> result = new ArrayList<String>();
for (int i = 0; i < AutoDetach.values.length; i++) {
if ((value & AutoDetach.values[i]) != 0) {
result.add(AutoDetach.names[i]);
}
}
return Arrays.toString(result.toArray(new String[result.size()]));
}
}

View File

@ -1485,4 +1485,13 @@ public class DelegatingBroker
_broker.setAllowReferenceToSiblingContext(allow);
}
public boolean getPostLoadOnMerge() {
return _broker.getPostLoadOnMerge();
}
public void setPostLoadOnMerge(boolean allow) {
_broker.setPostLoadOnMerge(allow);
}
}

View File

@ -28,9 +28,11 @@ import java.util.Map;
import org.apache.openjpa.conf.Compatibility;
import org.apache.openjpa.enhance.PersistenceCapable;
import org.apache.openjpa.enhance.StateManager;
import org.apache.openjpa.event.LifecycleEventManager;
import org.apache.openjpa.lib.util.Localizer;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FetchGroup;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.meta.ValueMetaData;
@ -128,45 +130,62 @@ public class DetachedStateManager
// pre-load for efficiency: current field values for restore, dependent
// for delete
FieldMetaData[] fields = sm.getMetaData().getFields();
FieldMetaData[] fields = sm.getMetaData().getFields();
int restore = broker.getRestoreState();
if (_dirty.length() > 0) {
BitSet load = new BitSet(fields.length);
for (int i = 0; i < fields.length; i++) {
if (!_dirty.get(i))
continue;
switch (fields[i].getDeclaredTypeCode()) {
case JavaTypes.ARRAY:
case JavaTypes.COLLECTION:
if (restore == RestoreState.RESTORE_ALL
|| fields[i].getElement().getCascadeDelete()
== ValueMetaData.CASCADE_AUTO)
load.set(i);
break;
case JavaTypes.MAP:
if (restore == RestoreState.RESTORE_ALL
|| fields[i].getElement().getCascadeDelete()
== ValueMetaData.CASCADE_AUTO
|| fields[i].getKey().getCascadeDelete()
== ValueMetaData.CASCADE_AUTO)
load.set(i);
break;
default:
if (restore != RestoreState.RESTORE_NONE
|| fields[i].getCascadeDelete()
== ValueMetaData.CASCADE_AUTO)
load.set(i);
boolean postLoadOnMerge = broker.getPostLoadOnMerge();
if (_dirty.length() > 0 || postLoadOnMerge) {
BitSet load = new BitSet(fields.length);
if (postLoadOnMerge && broker.getLifecycleEventManager().hasLoadListeners(pc, meta)) {
// load all fields
// this will automatically lead to invoking the PostLoad lifecycle event
// when the last field got set
// @see StateManagerImpl#postLoad(String, FetchConfiguration)
load.set(0, fields.length);
}
else {
for (int i = 0; i < fields.length; i++) {
if (!_dirty.get(i))
continue;
switch (fields[i].getDeclaredTypeCode()) {
case JavaTypes.ARRAY:
case JavaTypes.COLLECTION:
if (restore == RestoreState.RESTORE_ALL
|| fields[i].getElement().getCascadeDelete()
== ValueMetaData.CASCADE_AUTO)
load.set(i);
break;
case JavaTypes.MAP:
if (restore == RestoreState.RESTORE_ALL
|| fields[i].getElement().getCascadeDelete()
== ValueMetaData.CASCADE_AUTO
|| fields[i].getKey().getCascadeDelete()
== ValueMetaData.CASCADE_AUTO)
load.set(i);
break;
default:
if (restore != RestoreState.RESTORE_NONE
|| fields[i].getCascadeDelete()
== ValueMetaData.CASCADE_AUTO)
load.set(i);
}
}
}
if (!postLoadOnMerge) {
// prevent PostLoad callbacks even for the load operation
sm.setPostLoadCallback(false);
}
FetchConfiguration fc = broker.getFetchConfiguration();
sm.loadFields(load, fc, fc.getWriteLockLevel(), null);
}
}
Object origVersion = sm.getVersion();
sm.setVersion(_version);
BitSet loaded = sm.getLoaded();
int set = StateManager.SET_ATTACH;
sm.setPostLoadCallback(false);
for (int i = 0; i < fields.length; i++) {
if (!_loaded.get(i))
continue;
@ -214,8 +233,8 @@ public class DetachedStateManager
break;
case JavaTypes.SHORT:
if (_dirty.get(i))
sm.settingShortField(pc, i, (!loaded.get(i)) ? (short) 0
: sm.fetchShortField(i), (short) longval, set);
sm.settingShortField(pc, i,
(!loaded.get(i)) ? (short) 0 : sm.fetchShortField(i), (short) longval, set);
else
sm.storeShortField(i, (short) longval);
break;
@ -299,6 +318,7 @@ public class DetachedStateManager
objval = null;
}
}
sm.setPostLoadCallback(true);
pc.pcReplaceStateManager(sm);
// if we were clean at least make sure a version check is done to

View File

@ -157,6 +157,13 @@ public class StateManagerImpl
private transient ReentrantLock _instanceLock = null;
/**
* <p>set to <code>false</code> to prevent the postLoad method from
* sending lifecycle callback events.</p>
* <p>Callbacks are enabled by default</>
*/
private boolean postLoadCallback = true;
/**
* Constructor; supply id, type metadata, and owning persistence manager.
*/
@ -3192,6 +3199,14 @@ public class StateManagerImpl
_loaded.clear(field);
}
/**
* Set to <code>false</code> to prevent the postLoad method from
* sending lifecycle callback events.
*/
public void setPostLoadCallback(boolean enabled) {
this.postLoadCallback = enabled;
}
/**
* Perform post-load steps, including the post load callback.
* We have to check the dfg after all field loads because it might be
@ -3254,8 +3269,8 @@ public class StateManagerImpl
return false;
_flags |= FLAG_LOADED;
_broker.fireLifecycleEvent(getManagedInstance(), fetch, _meta,
LifecycleEvent.AFTER_LOAD);
if (postLoadCallback)
_broker.fireLifecycleEvent(getManagedInstance(), fetch, _meta, LifecycleEvent.AFTER_LOAD);
return true;
}

View File

@ -531,4 +531,22 @@ public interface StoreContext {
* @since 2.1
*/
public boolean getAllowReferenceToSiblingContext();
/**
* Set to <code>true</code> if the merge operation should trigger
* a &#064;PostLoad lifecycle event.
* @param allow PostLoad lifecycle events to be triggered on a merge operation
*/
public void setPostLoadOnMerge(boolean allow);
/**
* Force sending a &#064;PostLoad lifecycle event while merging.
*
* @return <code>false</code> by default
*
* @since 2.2
*/
public boolean getPostLoadOnMerge();
}

View File

@ -0,0 +1,110 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.openjpa.persistence.callbacks;
import org.apache.openjpa.persistence.OpenJPAEntityManager;
import org.apache.openjpa.persistence.test.SingleEMFTestCase;
public class EntityListenerPostLoadTest extends SingleEMFTestCase {
public void setUp() {
setUp(CLEAR_TABLES);
}
@Override
protected String getPersistenceUnitName() {
return "listener-pu";
}
/**
* If an entity gets merged it is first read from the database prior to the update. In this read step, the
* &#064;PostLoad get's executed. After I save my entity to the database, the &#064;PostLoad following merge should
* return exactly the value stored to the database. Even if the value got changed locally in the meantime.
*/
public void testPostLoadValues() {
OpenJPAEntityManager em = emf.createEntityManager();
try {
em.getTransaction().begin();
PostLoadListenerEntity entity = null;
entity = new PostLoadListenerEntity();
entity.setValue("val1");
em.persist(entity);
em.getTransaction().commit();
// close the EntityManager so our entity is now detached
em.close();
// reopen a new EntityManager
em = emf.createEntityManager();
assertTrue(em.isDetached(entity));
em.getTransaction().begin();
// entity = em.find(PostLoadListenerEntity.class, entity.getId());
entity = em.find(PostLoadListenerEntity.class, entity.getId());
assertNotNull(entity);
// the merge invoked a PostLoad, so this should now be 'val1'
assertEquals("val1", PostLoadListenerImpl.postLoadValue);
em.getTransaction().commit();
// close the EntityManager so our entity is now detached again
em.close();
// reopen a new EntityManager
em = emf.createEntityManager();
em.getTransaction().begin();
assertTrue(em.isDetached(entity));
entity.setValue("val2");
//X entity.setValue2("val2");
entity = em.merge(entity);
// the merge invoked a PostLoad, and this should now STILL be 'val1'
assertEquals("val1", PostLoadListenerImpl.postLoadValue);
em.getTransaction().commit();
// close the EntityManager so our entity is now detached again
em.close();
// reopen a new EntityManager
em = emf.createEntityManager();
em.getTransaction().begin();
entity.setValue("val3");
entity = em.merge(entity);
// the merge invoked a PostLoad, and this should now STILL be 'val1'
assertEquals("val2", PostLoadListenerImpl.postLoadValue);
em.getTransaction().commit();
} finally {
if (em != null && em.getTransaction().isActive())
em.getTransaction().rollback();
if (em != null && em.isOpen())
em.close();
}
}
}

View File

@ -0,0 +1,64 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.openjpa.persistence.callbacks;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
import javax.persistence.EntityListeners;
@Entity
@EntityListeners({PostLoadListenerImpl.class})
public class PostLoadListenerEntity {
@Id @GeneratedValue
private long id;
private String value;
// those fields are important for the test since
// OpenJPA will load the full Table at once if you remove them
private String value2;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getValue2() {
return value2;
}
public void setValue2(String value2) {
this.value2 = value2;
}
}

View File

@ -0,0 +1,39 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.openjpa.persistence.callbacks;
import javax.persistence.PostLoad;
/**
* JPA Listener which maintains changelog information of the {@link PostLoadListenerEntity}.
* The &#064;PostLoad gets called once the entity is being loaded from the database.
* This happens either if the entity get's loaded freshly into the EntityManager, or
* while performing a call to EntityManager#merge(entity)
*/
public class PostLoadListenerImpl {
static String postLoadValue;
@PostLoad
public void postLoad(Object o) {
PostLoadListenerEntity ple = (PostLoadListenerEntity) o;
postLoadValue = ple.getValue();
}
}

View File

@ -70,6 +70,7 @@ public class TestOpenJPAConfiguration
assertEquals(cfactory, conf.getConnectionFactory());
assertEquals(cfactory2, conf.getConnectionFactory2());
assertEquals(false, conf.getOptimistic());
assertEquals(false, conf.getPostLoadOnMerge());
assertEquals(503, conf.getLockTimeout());
assertEquals(1500, conf.getQueryTimeout());

View File

@ -101,8 +101,10 @@
<class>org.apache.openjpa.persistence.callbacks.EntityListenerEntity</class>
<class>org.apache.openjpa.persistence.callbacks.GlobalListenerEntity</class>
<class>org.apache.openjpa.persistence.callbacks.DuplicateListenerEntity</class>
<class>org.apache.openjpa.persistence.callbacks.PostLoadListenerEntity</class>
<class>org.apache.openjpa.persistence.callbacks.Message</class>
<properties>
<property name="openjpa.PostLoadOnMerge" value="true"/>
<property name="openjpa.jdbc.SynchronizeMappings"
value="buildSchema(ForeignKeys=true)"/>
</properties>