Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -622,11 +622,19 @@ public <T> Stream<T> loadAllOfStream(Class<T> clazz) {

@Override
public <T> ImmutableList<T> loadAllOfSorted(Class<T> clazz, String... sortFields) {
return loadAllOfSortedStream(clazz, sortFields).collect(toImmutableList());
// Use getResultList() instead of getResultStream() so Hibernate hydrates all entities into
// the PersistenceContext and BatchFetchQueue before initializing collections and detaching.
return createLoadAllQuery(clazz, sortFields).getResultList().stream()
.map(this::detach)
.collect(toImmutableList());
}

@Override
public <T> Stream<T> loadAllOfSortedStream(Class<T> clazz, String... sortFields) {
return createLoadAllQuery(clazz, sortFields).getResultStream().map(this::detach);
}

private <T> TypedQuery<T> createLoadAllQuery(Class<T> clazz, String... sortFields) {
checkArgumentNotNull(clazz, "clazz must be specified");
checkArgumentNotNull(sortFields, "sortFields must not be null");
assertInTransaction();
Expand All @@ -642,10 +650,7 @@ public <T> Stream<T> loadAllOfSortedStream(Class<T> clazz, String... sortFields)
queryString.append(" ORDER BY ");
queryString.append(String.join(", ", sortFields));
}
return getEntityManager()
.createQuery(queryString.toString(), clazz)
.getResultStream()
.map(this::detach);
return getEntityManager().createQuery(queryString.toString(), clazz);
}

@Override
Expand Down Expand Up @@ -1034,13 +1039,14 @@ public TypedQuery<T> setCacheRetrieveMode(CacheRetrieveMode mode) {

@Override
public T getSingleResultOrNull() {
return delegate.getSingleResultOrNull();
return detachIfEntity(delegate.getSingleResultOrNull());
}

@Override
public List<T> getResultList() {
return delegate
.getResultStream()
// Delegate to getResultList() rather than getResultStream() so Hibernate can batch-fetch
// child collections across all loaded entities before each entity is detached.
return delegate.getResultList().stream()
.map(JpaTransactionManagerImpl.this::detachIfEntity)
.collect(toImmutableList());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,28 @@

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import google.registry.config.RegistryConfig;
import google.registry.model.ImmutableObject;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.JpaTestExtensions.JpaUnitTestExtension;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeClock;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.OptimisticLockException;
import jakarta.persistence.PersistenceException;
import jakarta.persistence.RollbackException;
import java.io.Serializable;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.Supplier;
import org.hibernate.SessionFactory;
import org.hibernate.stat.Statistics;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -89,7 +95,10 @@
.withInitScript(fileClassPath(getClass(), "test_schema.sql"))
.withClock(fakeClock)
.withEntityClass(
TestEntity.class, TestCompoundIdEntity.class, TestNamedCompoundIdEntity.class)
TestEntity.class,
TestCompoundIdEntity.class,
TestNamedCompoundIdEntity.class,
TestCollectionEntity.class)
.buildUnitTestExtension();

@Test
Expand Down Expand Up @@ -960,6 +969,52 @@
"FROM TestEntity WHERE name = 'entity1'", TestEntity.class)
.getSingleResult()))
.isFalse());
tm().transact(
() -> {
TestEntity result =
tm().query("FROM TestEntity WHERE name = 'entity1'", TestEntity.class)
.getSingleResultOrNull();
assertThat(result).isEqualTo(moreEntities.get(0));
assertThat(tm().getEntityManager().contains(result)).isFalse();
assertThat(
tm().query("FROM TestEntity WHERE name = 'nonexistent'", TestEntity.class)
.getSingleResultOrNull())
.isNull();
});
}

@Test
void queryAndLoadAllOf_batchFetchEagerCollections() {
ImmutableList<TestCollectionEntity> entities =
ImmutableList.of(
new TestCollectionEntity("c1", ImmutableSet.of("a", "b")),
new TestCollectionEntity("c2", ImmutableSet.of("c")),
new TestCollectionEntity("c3", ImmutableSet.of("d", "e")));
persistResources(entities);

for (Supplier<List<TestCollectionEntity>> querySupplier :
ImmutableList.<Supplier<List<TestCollectionEntity>>>of(
() ->
tm().query("FROM TestCollectionEntity ORDER BY name", TestCollectionEntity.class)
.getResultList(),
() -> tm().loadAllOfSorted(TestCollectionEntity.class, "name"))) {
tm().transact(
() -> {
Statistics stats =
tm().getEntityManager()
.getEntityManagerFactory()
.unwrap(SessionFactory.class)
.getStatistics();
stats.setStatisticsEnabled(true);
stats.clear();

List<TestCollectionEntity> loaded = querySupplier.get();
// 1 query for TestCollectionEntity + 1 batched query for all 3 eager collections.
assertThat(stats.getPrepareStatementCount()).isEqualTo(2);
loaded.forEach(DatabaseHelper::assertDetachedFromEntityManager);
assertThat(loaded).containsExactlyElementsIn(entities).inOrder();
});
}
}

@Test
Expand Down Expand Up @@ -1168,4 +1223,19 @@
this.ageField = ageField;
}
}

@Entity(name = "TestCollectionEntity")
private static class TestCollectionEntity extends ImmutableObject {
Comment thread
gbrodman marked this conversation as resolved.
Dismissed
@Id private String name;

@ElementCollection(fetch = FetchType.EAGER)
private Set<String> items;

private TestCollectionEntity() {}

private TestCollectionEntity(String name, Set<String> items) {
this.name = name;
this.items = items;
}
}
}
Loading