Detach result objects obtained through jpaTm().query() (#1183)

* Added TransformingTypedQuery class

Added class to wrap TypedQuery so that we can detach all objects on load.

* Don't detach non-entity results; complete tests

* Changes for review

* Make non-static and call detach directly
This commit is contained in:
Michael Muller 2021-06-01 14:20:04 -04:00 committed by GitHub
parent b6976dc50e
commit ea28e015d4
3 changed files with 272 additions and 2 deletions

View file

@ -35,10 +35,14 @@ public interface JpaTransactionManager extends TransactionManager {
<T> TypedQuery<T> query(String sqlString, Class<T> resultClass);
/**
* Creates a JPA SQL query for the given query string (which does not return results).
* Creates a JPA SQL query for the given query string.
*
* <p>This is a convenience method for the longer <code>
* jpaTm().getEntityManager().createQuery(...)</code>.
*
* <p>Note that while this method can legally be used for queries that return results, <u>it
* should not be</u>, as it does not correctly detach entities as must be done for nomulus model
* objects.
*/
Query query(String sqlString);

View file

@ -23,6 +23,7 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import static java.util.AbstractMap.SimpleEntry;
import static java.util.stream.Collectors.joining;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@ -43,8 +44,11 @@ import google.registry.persistence.VKey;
import google.registry.util.Clock;
import google.registry.util.Retrier;
import google.registry.util.SystemSleeper;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
@ -58,8 +62,12 @@ import javax.annotation.Nullable;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.Parameter;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import javax.persistence.TemporalType;
import javax.persistence.TypedQuery;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.SingularAttribute;
@ -116,7 +124,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
@Override
public <T> TypedQuery<T> query(String sqlString, Class<T> resultClass) {
return getEntityManager().createQuery(sqlString, resultClass);
return new DetachingTypedQuery(getEntityManager().createQuery(sqlString, resultClass));
}
@Override
@ -665,6 +673,33 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
}
@Nullable
private <T> T detachIfEntity(@Nullable T object) {
if (object == null) {
return null;
}
// Check if the object is an array, if so we'll want to recurse through the elements.
if (object.getClass().isArray()) {
for (int i = 0; i < Array.getLength(object); ++i) {
detachIfEntity(Array.get(object, i));
}
return object;
}
// Check to see if it is an entity (queries can return raw column values or counts, so this
// could be String, Long, ...).
try {
getEntityManager().getMetamodel().entity(object.getClass());
} catch (IllegalArgumentException e) {
// The object is not an entity. Return without detaching.
return object;
}
// At this point, object must be an entity.
return detach(object);
}
/** Detach the entity, suitable for use in Optional.map(). */
@Nullable
private <T> T detach(@Nullable T entity) {
@ -758,6 +793,207 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
}
/**
* Typed query wrapper that applies a transform to all result objects.
*
* <p>This is used to detach objects upon load.
*/
@VisibleForTesting
class DetachingTypedQuery<T> implements TypedQuery<T> {
TypedQuery<T> delegate;
public DetachingTypedQuery(TypedQuery<T> delegate) {
this.delegate = delegate;
}
@Override
public List<T> getResultList() {
return delegate
.getResultStream()
.map(JpaTransactionManagerImpl.this::detachIfEntity)
.collect(toImmutableList());
}
@Override
public Stream<T> getResultStream() {
return delegate.getResultStream().map(JpaTransactionManagerImpl.this::detachIfEntity);
}
@Override
public T getSingleResult() {
return detachIfEntity(delegate.getSingleResult());
}
@Override
public TypedQuery<T> setMaxResults(int maxResults) {
delegate.setMaxResults(maxResults);
return this;
}
@Override
public TypedQuery<T> setFirstResult(int startPosition) {
delegate.setFirstResult(startPosition);
return this;
}
@Override
public TypedQuery<T> setHint(String hintName, Object value) {
delegate.setHint(hintName, value);
return this;
}
@Override
public <U> TypedQuery<T> setParameter(Parameter<U> param, U value) {
delegate.setParameter(param, value);
return this;
}
@Override
public TypedQuery<T> setParameter(
Parameter<Calendar> param, Calendar value, TemporalType temporalType) {
delegate.setParameter(param, value, temporalType);
return this;
}
@Override
public TypedQuery<T> setParameter(
Parameter<Date> param, Date value, TemporalType temporalType) {
delegate.setParameter(param, value, temporalType);
return this;
}
@Override
public TypedQuery<T> setParameter(String name, Object value) {
delegate.setParameter(name, value);
return this;
}
@Override
public TypedQuery<T> setParameter(String name, Calendar value, TemporalType temporalType) {
delegate.setParameter(name, value, temporalType);
return this;
}
@Override
public TypedQuery<T> setParameter(String name, Date value, TemporalType temporalType) {
delegate.setParameter(name, value, temporalType);
return this;
}
@Override
public TypedQuery<T> setParameter(int position, Object value) {
delegate.setParameter(position, value);
return this;
}
@Override
public TypedQuery<T> setParameter(int position, Calendar value, TemporalType temporalType) {
delegate.setParameter(position, value, temporalType);
return this;
}
@Override
public TypedQuery<T> setParameter(int position, Date value, TemporalType temporalType) {
delegate.setParameter(position, value, temporalType);
return this;
}
@Override
public TypedQuery<T> setFlushMode(FlushModeType flushMode) {
delegate.setFlushMode(flushMode);
return this;
}
@Override
public TypedQuery<T> setLockMode(LockModeType lockMode) {
delegate.setLockMode(lockMode);
return this;
}
// Query interface
@Override
public int executeUpdate() {
return delegate.executeUpdate();
}
@Override
public int getMaxResults() {
return delegate.getMaxResults();
}
@Override
public int getFirstResult() {
return delegate.getFirstResult();
}
@Override
public Map<String, Object> getHints() {
return delegate.getHints();
}
@Override
public Set<Parameter<?>> getParameters() {
return delegate.getParameters();
}
@Override
public Parameter<?> getParameter(String name) {
return delegate.getParameter(name);
}
@Override
public <U> Parameter<U> getParameter(String name, Class<U> type) {
return delegate.getParameter(name, type);
}
@Override
public Parameter<?> getParameter(int position) {
return delegate.getParameter(position);
}
@Override
public <U> Parameter<U> getParameter(int position, Class<U> type) {
return delegate.getParameter(position, type);
}
@Override
public boolean isBound(Parameter<?> param) {
return delegate.isBound(param);
}
@Override
public <U> U getParameterValue(Parameter<U> param) {
return delegate.getParameterValue(param);
}
@Override
public Object getParameterValue(String name) {
return delegate.getParameterValue(name);
}
@Override
public Object getParameterValue(int position) {
return delegate.getParameterValue(position);
}
@Override
public FlushModeType getFlushMode() {
return delegate.getFlushMode();
}
@Override
public LockModeType getLockMode() {
return delegate.getLockMode();
}
@Override
public <U> U unwrap(Class<U> cls) {
return delegate.unwrap(cls);
}
}
private class JpaQueryComposerImpl<T> extends QueryComposer<T> {
private static final int DEFAULT_FETCH_SIZE = 1000;

View file

@ -588,6 +588,36 @@ class JpaTransactionManagerImplTest {
.contains("Inserted/updated object reloaded: ");
}
@Test
void query_detachesResults() {
jpaTm().transact(() -> jpaTm().insertAll(moreEntities));
jpaTm()
.transact(
() ->
jpaTm().query("FROM TestEntity", TestEntity.class).getResultList().stream()
.forEach(e -> assertThat(jpaTm().getEntityManager().contains(e)).isFalse()));
jpaTm()
.transact(
() ->
jpaTm()
.query("FROM TestEntity", TestEntity.class)
.getResultStream()
.forEach(e -> assertThat(jpaTm().getEntityManager().contains(e)).isFalse()));
jpaTm()
.transact(
() ->
assertThat(
jpaTm()
.getEntityManager()
.contains(
jpaTm()
.query(
"FROM TestEntity WHERE name = 'entity1'", TestEntity.class)
.getSingleResult()))
.isFalse());
}
private void insertPerson(int age) {
jpaTm()
.getEntityManager()