HHH-11188 - SessionImpl.refresh() throws IllegalArgumentException, 'Entity not managed' for detached instances when JPA bootstrapped

This commit is contained in:
Andrea Boriero 2016-10-25 18:37:11 +01:00 committed by Vlad Mihalcea
parent 957ec3fa79
commit bf2bc323f6
17 changed files with 323 additions and 2 deletions

View File

@ -772,5 +772,7 @@ false:: does not allow
|`hibernate.collection_join_subquery`| `true` (default value) or `false` | Setting which indicates whether or not the new JOINS over collection tables should be rewritten to subqueries.
|`hibernate.allow_refresh_detached_entity`| `true` (default value when using Hibernate native bootstrapping) or `false` (default value when using JPA bootstrapping) | Setting that allows to call `javax.persistence.EntityManager#refresh()` or `org.hibernate.Session#refresh()` on a detached instance even when the `org.hibernate.Session` is obtained from a JPA `javax.persistence.EntityManager`.
|=====================================================================================================================================================================================================================================================

View File

@ -285,6 +285,21 @@ However, please note that Hibernate has the capability to handle this automatica
See the discussion of non-identifier <<chapters/domain/basic_types.adoc#mapping-generated,generated attributes>>.
====
[IMPORTANT]
====
Traditionally, Hibernate has been allowing detached entities to be refreshed.
Unfortunately, JPA prohibits this practice and specifies that an `IllegalArgumentException` should be thrown instead.
For this reason, when bootstrapping the Hibernate `SessionFactory` using the native API, the legacy detached entity refresh behavior is going to be preserved.
On the other hand, when bootstrapping Hibernate through JPA `EntityManagerFactory` building process, detached entities are not allowed to be refreshed by default.
However, this default behavior can be overwritten through the `hibernate.allow_refresh_detached_entity` configuration property.
If this property is explicitly set to `true`, then you can refresh detached entities even when using the JPA bootstraps mechanism, therefore bypassing the JPA specification restriction.
For more about the `hibernate.allow_refresh_detached_entity` configuration property,
check out the <<appendices/Configurations.adoc#misc,Configurations>> section as well.
====
[[pc-detach]]
=== Working with detached data

View File

@ -450,6 +450,11 @@ public class SessionFactoryBuilderImpl implements SessionFactoryBuilderImplement
this.options.jpaBootstrap = true;
}
@Override
public void disableRefreshDetachedEntity() {
this.options.allowRefreshDetachedEntity = false;
}
@Override
public void disableJtaTransactionAccess() {
this.options.jtaTransactionAccessEnabled = false;
@ -482,6 +487,7 @@ public class SessionFactoryBuilderImpl implements SessionFactoryBuilderImplement
private boolean jtaTransactionAccessEnabled;
private boolean allowOutOfTransactionUpdateOperations;
private boolean releaseResourcesOnCloseEnabled;
private boolean allowRefreshDetachedEntity;
// (JTA) transaction handling
private boolean jtaTrackByThread;
@ -581,6 +587,12 @@ public class SessionFactoryBuilderImpl implements SessionFactoryBuilderImplement
true
);
this.allowRefreshDetachedEntity = cfgService.getSetting(
ALLOW_REFRESH_DETACHED_ENTITY,
BOOLEAN,
true
);
this.flushBeforeCompletionEnabled = cfgService.getSetting( FLUSH_BEFORE_COMPLETION, BOOLEAN, true );
this.autoCloseSessionEnabled = cfgService.getSetting( AUTO_CLOSE_SESSION, BOOLEAN, false );
@ -871,6 +883,10 @@ public class SessionFactoryBuilderImpl implements SessionFactoryBuilderImplement
return jtaTransactionAccessEnabled;
}
public boolean isAllowRefreshDetachedEntity() {
return allowRefreshDetachedEntity;
}
public boolean isAllowOutOfTransactionUpdateOperations() {
return allowOutOfTransactionUpdateOperations;
}
@ -1181,6 +1197,10 @@ public class SessionFactoryBuilderImpl implements SessionFactoryBuilderImplement
return options.isJtaTransactionAccessEnabled();
}
public boolean isAllowRefreshDetachedEntity() {
return options.isAllowRefreshDetachedEntity();
}
public boolean isAllowOutOfTransactionUpdateOperations() {
return options.isAllowOutOfTransactionUpdateOperations();
}

View File

@ -53,6 +53,7 @@ public class SessionFactoryOptionsImpl implements SessionFactoryOptions {
private final boolean flushBeforeCompletionEnabled;
private final boolean autoCloseSessionEnabled;
private boolean jtaTransactionAccessEnabled;
private boolean allowRefreshDetachedEntity;
private boolean allowOutOfTransactionUpdateOperations;
private boolean releaseResourcesOnCloseEnabled;
@ -132,6 +133,7 @@ public class SessionFactoryOptionsImpl implements SessionFactoryOptions {
this.jpaBootstrap = state.isJpaBootstrap();
this.jtaTransactionAccessEnabled = state.isJtaTransactionAccessEnabled();
this.allowRefreshDetachedEntity = state.isAllowRefreshDetachedEntity();
this.allowOutOfTransactionUpdateOperations = state.isAllowOutOfTransactionUpdateOperations();
this.sessionFactoryName = state.getSessionFactoryName();
this.sessionFactoryNameAlsoJndiName = state.isSessionFactoryNameAlsoJndiName();
@ -215,6 +217,11 @@ public class SessionFactoryOptionsImpl implements SessionFactoryOptions {
return jtaTransactionAccessEnabled;
}
@Override
public boolean isAllowRefreshDetachedEntity() {
return allowRefreshDetachedEntity;
}
@Override
public Object getBeanManagerReference() {
return beanManagerReference;

View File

@ -49,6 +49,8 @@ public interface SessionFactoryOptionsState {
boolean isJtaTransactionAccessEnabled();
boolean isAllowRefreshDetachedEntity();
boolean isAllowOutOfTransactionUpdateOperations();
boolean isReleaseResourcesOnCloseEnabled();

View File

@ -59,6 +59,11 @@ public class AbstractDelegatingSessionFactoryOptions implements SessionFactoryOp
return delegate.isJtaTransactionAccessEnabled();
}
@Override
public boolean isAllowRefreshDetachedEntity() {
return delegate.isAllowRefreshDetachedEntity();
}
@Override
public Object getBeanManagerReference() {
return delegate.getBeanManagerReference();

View File

@ -30,6 +30,9 @@ public interface SessionFactoryBuilderImplementor extends SessionFactoryBuilder
void disableJtaTransactionAccess();
default void disableRefreshDetachedEntity() {
}
/**
* Build the SessionFactoryOptions that will ultimately be passed to SessionFactoryImpl constructor.
*

View File

@ -61,6 +61,10 @@ public interface SessionFactoryOptions {
boolean isJtaTransactionAccessEnabled();
default boolean isAllowRefreshDetachedEntity() {
return false;
}
/**
* The name to be used for the SessionFactory. This is use both in:<ul>
* <li>in-VM serialization</li>

View File

@ -1558,4 +1558,18 @@ public interface AvailableSettings {
* @since 5.2
*/
String COLLECTION_JOIN_SUBQUERY = "hibernate.collection_join_subquery";
/**
* Setting that allows to call {@link javax.persistence.EntityManager#refresh(Object)} *
* (or {@link org.hibernate.Session#refresh(Object)} on a detached entity instance when the {@link org.hibernate.Session} is obtained from
* a JPA {@link javax.persistence.EntityManager}).
* <p>
* <p/>
* Values are {@code true} permits the refresh, {@code false} does not permit the detached instance refresh and an {@link IllegalArgumentException} is thrown.
* <p/>
* The default value is {@code false} when the Session is bootstrapped via JPA {@link javax.persistence.EntityManagerFactory}, otherwise is {@code true}
*
* @since 5.2
*/
String ALLOW_REFRESH_DETACHED_ENTITY = "hibernate.allow_refresh_detached_entity";
}

View File

@ -73,6 +73,7 @@ import org.hibernate.TypeHelper;
import org.hibernate.TypeMismatchException;
import org.hibernate.UnknownProfileException;
import org.hibernate.UnresolvableObjectException;
import org.hibernate.boot.spi.SessionFactoryOptions;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.criterion.NaturalIdentifier;
import org.hibernate.engine.internal.StatefulPersistenceContext;
@ -1264,7 +1265,7 @@ public final class SessionImpl
private void fireRefresh(RefreshEvent event) {
try {
if ( getSessionFactory().getSessionFactoryOptions().isJpaBootstrap() ) {
if ( !getSessionFactory().getSessionFactoryOptions().isAllowRefreshDetachedEntity() ) {
if ( !contains( event.getObject() ) ) {
throw new IllegalArgumentException( "Entity not managed" );
}

View File

@ -103,7 +103,6 @@ import static org.hibernate.internal.HEMLogging.messageLogger;
import static org.hibernate.jpa.AvailableSettings.CFG_FILE;
import static org.hibernate.jpa.AvailableSettings.CLASS_CACHE_PREFIX;
import static org.hibernate.jpa.AvailableSettings.COLLECTION_CACHE_PREFIX;
import static org.hibernate.jpa.AvailableSettings.DISCARD_PC_ON_CLOSE;
import static org.hibernate.jpa.AvailableSettings.PERSISTENCE_UNIT_NAME;
/**
@ -901,6 +900,11 @@ public class EntityManagerFactoryBuilderImpl implements EntityManagerFactoryBuil
( ( SessionFactoryBuilderImplementor ) sfBuilder ).disableJtaTransactionAccess();
}
final boolean allowRefreshDetachedEntity = readBooleanConfigurationValue( org.hibernate.cfg.AvailableSettings.ALLOW_REFRESH_DETACHED_ENTITY );
if ( !allowRefreshDetachedEntity ) {
( (SessionFactoryBuilderImplementor) sfBuilder ).disableRefreshDetachedEntity();
}
// Locate and apply any requested SessionFactoryObserver
final Object sessionFactoryObserverSetting = configurationValues.remove( AvailableSettings.SESSION_FACTORY_OBSERVER );
if ( sessionFactoryObserverSetting != null ) {

View File

@ -0,0 +1,59 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.jpa.test.refresh;
import java.util.Map;
import javax.persistence.EntityManager;
import org.hibernate.Session;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase;
import org.hibernate.testing.TestForIssue;
import org.junit.Before;
import org.junit.Test;
import static org.hibernate.testing.transaction.TransactionUtil.doInJPA;
/**
* @author Andrea Boriero
*/
@TestForIssue(jiraKey = "HHH-11188")
public class RefreshDetachedInstanceWhenIsAllowedTest extends BaseEntityManagerFunctionalTestCase {
private TestEntity testEntity;
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[] {TestEntity.class};
}
@Override
protected void addConfigOptions(Map options) {
options.put( AvailableSettings.ALLOW_REFRESH_DETACHED_ENTITY, "true" );
}
@Before
public void setUp() {
testEntity = new TestEntity();
doInJPA( this::entityManagerFactory, entityManager -> {
entityManager.persist( testEntity );
} );
}
@Test
public void testUnwrappedSessionRefreshDetachedInstance() {
final EntityManager entityManager = createEntityManager();
final Session session = entityManager.unwrap( Session.class );
session.refresh( testEntity );
}
@Test
public void testRefreshDetachedInstance() {
final EntityManager entityManager = createEntityManager();
entityManager.refresh( testEntity );
}
}

View File

@ -0,0 +1,50 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.jpa.test.refresh;
import javax.persistence.EntityManager;
import org.hibernate.Session;
import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase;
import org.junit.Before;
import org.junit.Test;
import static org.hibernate.testing.transaction.TransactionUtil.doInJPA;
/**
* @author Andrea Boriero
*/
public class RefreshDetachedInstanceWhenIsNotAllowedTest extends BaseEntityManagerFunctionalTestCase {
private TestEntity testEntity;
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[] {TestEntity.class};
}
@Before
public void setUp() {
testEntity = new TestEntity();
doInJPA( this::entityManagerFactory, entityManager -> {
entityManager.persist( testEntity );
} );
}
@Test(expected = IllegalArgumentException.class)
public void testUnwrappedSessionRefreshDetachedInstance() {
final EntityManager entityManager = createEntityManager();
final Session session = entityManager.unwrap( Session.class );
session.refresh( testEntity );
}
@Test(expected = IllegalArgumentException.class)
public void testRefreshDetachedInstance() {
final EntityManager entityManager = createEntityManager();
entityManager.refresh( testEntity );
}
}

View File

@ -0,0 +1,21 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.jpa.test.refresh;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
* @author Andrea Boriero
*/
@Entity
public class TestEntity {
@Id
@GeneratedValue
public long id;
}

View File

@ -0,0 +1,44 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.test.refresh;
import org.hibernate.Session;
import org.hibernate.jpa.test.refresh.TestEntity;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Before;
import org.junit.Test;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
/**
* @author Andrea Boriero
*/
@TestForIssue(jiraKey = "HHH-11188")
public class RefreshDetachedInstanceWhenIsAllowedTest extends BaseCoreFunctionalTestCase {
private TestEntity testEntity;
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[] {TestEntity.class};
}
@Before
public void setUp() {
testEntity = new TestEntity();
doInHibernate( this::sessionFactory, session -> {
session.save( testEntity );
} );
}
@Test
public void testRefreshDetachedInstance() {
final Session session = openSession();
session.refresh( testEntity );
}
}

View File

@ -0,0 +1,49 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.test.refresh;
import org.hibernate.Session;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.Configuration;
import org.hibernate.jpa.test.refresh.TestEntity;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Before;
import org.junit.Test;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
/**
* @author Andrea Boriero
*/
public class RefreshDetachedInstanceWhenIsNotAllowedTest extends BaseCoreFunctionalTestCase {
private TestEntity testEntity;
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[] {TestEntity.class};
}
@Override
protected void configure(Configuration configuration) {
configuration.setProperty( AvailableSettings.ALLOW_REFRESH_DETACHED_ENTITY, "false" );
}
@Before
public void setUp() {
testEntity = new TestEntity();
doInHibernate( this::sessionFactory, session -> {
session.save( testEntity );
} );
}
@Test(expected = IllegalArgumentException.class)
public void testRefreshDetachedInstance() {
final Session session = openSession();
session.refresh( testEntity );
}
}

View File

@ -0,0 +1,21 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.test.refresh;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
* @author Andrea Boriero
*/
@Entity
public class TestEntity {
@Id
@GeneratedValue
public long id;
}