Skip to content

Commit 4585c97

Browse files
Support FluentQuery (#317)
Co-authored-by: Roman Garcia <yzerno@gmail.com>
1 parent 6701d24 commit 4585c97

7 files changed

Lines changed: 289 additions & 7 deletions

File tree

core/src/main/java/com/cosium/spring/data/jpa/entity/graph/domain2/EntityGraphQueryHint.java

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
import static java.util.Objects.requireNonNull;
44

5+
import jakarta.persistence.AttributeNode;
56
import jakarta.persistence.EntityGraph;
7+
import jakarta.persistence.Subgraph;
8+
import java.util.ArrayList;
9+
import java.util.Collection;
10+
import java.util.Collections;
11+
import java.util.List;
12+
import org.springframework.data.jpa.repository.JpaSpecificationExecutor.SpecificationFluentQuery;
613

714
/**
815
* @author Réda Housni Alaoui
@@ -19,7 +26,8 @@ public EntityGraphQueryHint(EntityGraphType type, EntityGraph<?> entityGraph) {
1926

2027
/**
2128
* @param failIfInapplicable true if an {@link InapplicableEntityGraphException} must be thrown if
22-
* this entity graph cannot be applied.
29+
* this entity graph cannot be applied. This parameter is ignored in the context of a {@link
30+
* org.springframework.data.repository.query.FluentQuery}.
2331
*/
2432
public EntityGraphQueryHint(
2533
EntityGraphType type, EntityGraph<?> entityGraph, boolean failIfInapplicable) {
@@ -36,11 +44,31 @@ public EntityGraph<?> entityGraph() {
3644
return entityGraph;
3745
}
3846

39-
/**
40-
* @return true if an {@link InapplicableEntityGraphException} must be thrown if this entity graph
41-
* cannot be applied.
42-
*/
4347
public boolean failIfInapplicable() {
4448
return failIfInapplicable;
4549
}
50+
51+
public final <T> SpecificationFluentQuery<T> applyTo(SpecificationFluentQuery<T> query) {
52+
return query.project(toProperties());
53+
}
54+
55+
private Collection<String> toProperties() {
56+
List<String> paths = new ArrayList<>();
57+
for (AttributeNode<?> node : entityGraph.getAttributeNodes()) {
58+
List<String> nodePath = new ArrayList<>();
59+
addPath(node, nodePath);
60+
paths.add(String.join(".", nodePath));
61+
}
62+
Collections.sort(paths);
63+
return paths;
64+
}
65+
66+
private void addPath(AttributeNode<?> node, List<String> path) {
67+
path.add(node.getAttributeName());
68+
for (Subgraph<?> subgraph : node.getSubgraphs().values()) {
69+
for (AttributeNode<?> attributeNode : subgraph.getAttributeNodes()) {
70+
addPath(attributeNode, path);
71+
}
72+
}
73+
}
4674
}

core/src/main/java/com/cosium/spring/data/jpa/entity/graph/repository/EntityGraphJpaSpecificationExecutor.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import com.cosium.spring.data.jpa.entity.graph.domain2.EntityGraph;
44
import java.util.List;
55
import java.util.Optional;
6+
import java.util.function.Function;
67
import org.jspecify.annotations.Nullable;
78
import org.springframework.data.domain.Page;
89
import org.springframework.data.domain.Pageable;
910
import org.springframework.data.domain.Sort;
11+
import org.springframework.data.jpa.domain.PredicateSpecification;
1012
import org.springframework.data.jpa.domain.Specification;
1113
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
1214
import org.springframework.data.repository.NoRepositoryBean;
@@ -46,4 +48,22 @@ Page<T> findAll(
4648
* @see JpaSpecificationExecutor#findAll(Specification, Sort)
4749
*/
4850
List<T> findAll(@Nullable Specification<T> spec, Sort sort, @Nullable EntityGraph entityGraph);
51+
52+
/**
53+
* @see JpaSpecificationExecutor#findBy(PredicateSpecification, Function)
54+
*/
55+
default <S extends T, R> R findBy(
56+
PredicateSpecification<T> spec,
57+
@Nullable EntityGraph entityGraph,
58+
Function<? super SpecificationFluentQuery<S>, R> queryFunction) {
59+
return findBy(Specification.where(spec), entityGraph, queryFunction);
60+
}
61+
62+
/**
63+
* @see JpaSpecificationExecutor#findBy(Specification, Function)
64+
*/
65+
<S extends T, R extends @Nullable Object> R findBy(
66+
Specification<T> spec,
67+
@Nullable EntityGraph entityGraph,
68+
Function<? super SpecificationFluentQuery<S>, R> queryFunction);
4969
}

core/src/main/java/com/cosium/spring/data/jpa/entity/graph/repository/support/EntityGraphQueryHintCandidates.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ public static RepositoryProxyPostProcessor createPostProcessor(EntityManager ent
7171

7272
private @Nullable Object doInvoke(MethodInvocation invocation) throws Throwable {
7373
RepositoryMethodInvocation methodInvocation = new RepositoryMethodInvocation(invocation);
74+
if (methodInvocation.isSpecificationExecutorFindByMethod()) {
75+
return methodInvocation.proceed();
76+
}
77+
7478
EntityGraph providedEntityGraph = methodInvocation.findEntityGraphArgument();
7579
Object repository = methodInvocation.repository();
7680
ResolvableType returnType =

core/src/main/java/com/cosium/spring/data/jpa/entity/graph/repository/support/EntityGraphSimpleJpaRepository.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package com.cosium.spring.data.jpa.entity.graph.repository.support;
22

3+
import static java.util.Objects.requireNonNull;
4+
35
import com.cosium.spring.data.jpa.entity.graph.domain2.EntityGraph;
6+
import com.cosium.spring.data.jpa.entity.graph.domain2.EntityGraphQueryHint;
47
import com.cosium.spring.data.jpa.entity.graph.repository.EntityGraphJpaRepository;
58
import com.cosium.spring.data.jpa.entity.graph.repository.EntityGraphJpaSpecificationExecutor;
69
import jakarta.persistence.EntityManager;
710
import java.util.List;
811
import java.util.Optional;
12+
import java.util.function.Function;
913
import org.jspecify.annotations.Nullable;
1014
import org.springframework.data.domain.Example;
1115
import org.springframework.data.domain.Page;
@@ -23,13 +27,17 @@
2327
public class EntityGraphSimpleJpaRepository<T, ID> extends SimpleJpaRepository<T, ID>
2428
implements EntityGraphJpaRepository<T, ID>, EntityGraphJpaSpecificationExecutor<T> {
2529

30+
private final EntityManager entityManager;
31+
2632
public EntityGraphSimpleJpaRepository(
2733
JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
2834
super(entityInformation, entityManager);
35+
this.entityManager = requireNonNull(entityManager);
2936
}
3037

3138
public EntityGraphSimpleJpaRepository(Class<T> domainClass, EntityManager entityManager) {
3239
super(domainClass, entityManager);
40+
this.entityManager = requireNonNull(entityManager);
3341
}
3442

3543
@Override
@@ -109,4 +117,42 @@ public List<T> findAll(Sort sort, @Nullable EntityGraph entityGraph) {
109117
public List<T> findAll(@Nullable EntityGraph entityGraph) {
110118
return findAll();
111119
}
120+
121+
@Override
122+
public <S extends T, R extends @Nullable Object> R findBy(
123+
Specification<T> spec, Function<? super SpecificationFluentQuery<S>, R> queryFunction) {
124+
return findBy(spec, null, queryFunction);
125+
}
126+
127+
@Override
128+
public <S extends T, R extends @Nullable Object> R findBy(
129+
Specification<T> spec,
130+
@Nullable EntityGraph entityGraph,
131+
Function<? super SpecificationFluentQuery<S>, @Nullable R> queryFunction) {
132+
133+
QueryHint queryHint =
134+
Optional.ofNullable(entityGraph)
135+
.or(this::defaultEntityGraph)
136+
.flatMap(it -> it.buildQueryHint(entityManager, getDomainClass()))
137+
.map(QueryHint::new)
138+
.orElseGet(QueryHint::noop);
139+
140+
return super.findBy(
141+
spec, query -> queryFunction.apply(queryHint.applyTo((SpecificationFluentQuery<S>) query)));
142+
}
143+
144+
private record QueryHint(@Nullable EntityGraphQueryHint entityGraphQueryHint) {
145+
146+
static final QueryHint NOOP = new QueryHint(null);
147+
148+
static QueryHint noop() {
149+
return NOOP;
150+
}
151+
152+
public <T> SpecificationFluentQuery<T> applyTo(SpecificationFluentQuery<T> query) {
153+
return Optional.ofNullable(entityGraphQueryHint)
154+
.map(hint -> hint.applyTo(query))
155+
.orElse(query);
156+
}
157+
}
112158
}

core/src/main/java/com/cosium/spring/data/jpa/entity/graph/repository/support/RepositoryMethodInvocation.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package com.cosium.spring.data.jpa.entity.graph.repository.support;
22

3-
import static java.util.Objects.*;
3+
import static java.util.Objects.requireNonNull;
44

55
import com.cosium.spring.data.jpa.entity.graph.domain2.EntityGraph;
66
import java.lang.reflect.Method;
77
import org.aopalliance.intercept.MethodInvocation;
88
import org.jspecify.annotations.Nullable;
99
import org.springframework.aop.ProxyMethodInvocation;
10+
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
1011

1112
/**
1213
* @author Réda Housni Alaoui
@@ -59,4 +60,15 @@ public Method method() {
5960
}
6061
return providedEntityGraph;
6162
}
63+
64+
public boolean isSpecificationExecutorFindByMethod() {
65+
if (!(repository() instanceof JpaSpecificationExecutor<?>)) {
66+
return false;
67+
}
68+
if (!"findBy".equals(method().getName())) {
69+
return false;
70+
}
71+
@Nullable Object[] arguments = arguments();
72+
return arguments.length == 2 || arguments.length == 3;
73+
}
6274
}

core/src/test/java/com/cosium/spring/data/jpa/entity/graph/repository/EntityGraphJpaSpecificationExecutorTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.cosium.spring.data.jpa.entity.graph.repository;
22

3-
import static org.assertj.core.api.Assertions.*;
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
45

56
import com.cosium.spring.data.jpa.entity.graph.BaseTest;
67
import com.cosium.spring.data.jpa.entity.graph.domain2.EntityGraph;
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package com.cosium.spring.data.jpa.entity.graph.repository;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import com.cosium.spring.data.jpa.entity.graph.BaseTest;
6+
import com.cosium.spring.data.jpa.entity.graph.domain2.EntityGraph;
7+
import com.cosium.spring.data.jpa.entity.graph.domain2.NamedEntityGraph;
8+
import com.cosium.spring.data.jpa.entity.graph.sample.Product;
9+
import com.cosium.spring.data.jpa.entity.graph.sample.ProductEntityGraph;
10+
import com.cosium.spring.data.jpa.entity.graph.sample.Product_;
11+
import com.github.springtestdbunit.annotation.DatabaseSetup;
12+
import java.util.Optional;
13+
import org.hibernate.Hibernate;
14+
import org.junit.jupiter.api.DisplayName;
15+
import org.junit.jupiter.api.Test;
16+
import org.springframework.beans.factory.annotation.Autowired;
17+
import org.springframework.data.domain.Page;
18+
import org.springframework.data.domain.Pageable;
19+
import org.springframework.data.domain.Slice;
20+
import org.springframework.data.jpa.domain.Specification;
21+
import org.springframework.data.repository.query.FluentQuery;
22+
import org.springframework.transaction.annotation.Transactional;
23+
24+
/**
25+
* @author Réda Housni Alaoui
26+
*/
27+
@DatabaseSetup(BaseTest.DATASET)
28+
class FluentQueryTest extends BaseTest {
29+
30+
@Autowired private ProductRepository productRepository;
31+
32+
@Transactional
33+
@Test
34+
@DisplayName("It supports named entity graphs")
35+
void test1() {
36+
NamedEntityGraph entityGraph = NamedEntityGraph.loading(Product.BRAND_EG);
37+
38+
Product product =
39+
productRepository.findBy(
40+
idEquals(1L), entityGraph, FluentQuery.FetchableFluentQuery::oneValue);
41+
42+
assertThat(Hibernate.isInitialized(product.getBrand())).isTrue();
43+
}
44+
45+
@Transactional
46+
@Test
47+
@DisplayName("It supports dynamic entity graphs")
48+
void test2() {
49+
Product product =
50+
productRepository.findBy(
51+
idEquals(1L),
52+
ProductEntityGraph.____().brand().____.maker().country().____.____(),
53+
FluentQuery.FetchableFluentQuery::oneValue);
54+
55+
assertThat(Hibernate.isInitialized(product.getBrand())).isTrue();
56+
assertThat(Hibernate.isInitialized(product.getMaker())).isTrue();
57+
assertThat(Hibernate.isInitialized(product.getMaker().getCountry())).isTrue();
58+
59+
assertThat(Hibernate.isInitialized(product.getCategory())).isFalse();
60+
}
61+
62+
@Transactional
63+
@Test
64+
@DisplayName("It uses default entity graphs when null entity graph is provided")
65+
void test3() {
66+
Product product =
67+
productRepository.findBy(idEquals(1L), null, FluentQuery.FetchableFluentQuery::oneValue);
68+
69+
assertThat(Hibernate.isInitialized(product.getCategory())).isTrue();
70+
71+
assertThat(Hibernate.isInitialized(product.getBrand())).isFalse();
72+
assertThat(Hibernate.isInitialized(product.getMaker())).isFalse();
73+
assertThat(Hibernate.isInitialized(product.getMaker().getCountry())).isFalse();
74+
}
75+
76+
@Transactional
77+
@Test
78+
@DisplayName("It uses default entity graphs when noop entity graph is provided")
79+
void test4() {
80+
Product product =
81+
productRepository.findBy(
82+
idEquals(1L), EntityGraph.NOOP, FluentQuery.FetchableFluentQuery::oneValue);
83+
84+
assertThat(Hibernate.isInitialized(product.getCategory())).isTrue();
85+
86+
assertThat(Hibernate.isInitialized(product.getBrand())).isFalse();
87+
assertThat(Hibernate.isInitialized(product.getMaker())).isFalse();
88+
assertThat(Hibernate.isInitialized(product.getMaker().getCountry())).isFalse();
89+
}
90+
91+
@Transactional
92+
@Test
93+
@DisplayName("It supports pagination")
94+
void test5() {
95+
Page<Product> products =
96+
productRepository.findBy(
97+
idEquals(1L),
98+
ProductEntityGraph.____().brand().____.maker().country().____.____(),
99+
query -> query.page(Pageable.ofSize(1)));
100+
101+
assertThat(products).hasSize(1);
102+
103+
Product product = products.stream().findFirst().orElseThrow();
104+
assertThat(Hibernate.isInitialized(product.getBrand())).isTrue();
105+
assertThat(Hibernate.isInitialized(product.getMaker())).isTrue();
106+
assertThat(Hibernate.isInitialized(product.getMaker().getCountry())).isTrue();
107+
108+
assertThat(Hibernate.isInitialized(product.getCategory())).isFalse();
109+
}
110+
111+
@Transactional
112+
@Test
113+
@DisplayName("It supports slicing")
114+
void test6() {
115+
Slice<Product> products =
116+
productRepository.findBy(
117+
idEquals(1L),
118+
ProductEntityGraph.____().brand().____.maker().country().____.____(),
119+
query -> query.slice(Pageable.ofSize(1)));
120+
121+
assertThat(products).hasSize(1);
122+
123+
Product product = products.stream().findFirst().orElseThrow();
124+
assertThat(Hibernate.isInitialized(product.getBrand())).isTrue();
125+
assertThat(Hibernate.isInitialized(product.getMaker())).isTrue();
126+
assertThat(Hibernate.isInitialized(product.getMaker().getCountry())).isTrue();
127+
128+
assertThat(Hibernate.isInitialized(product.getCategory())).isFalse();
129+
}
130+
131+
@Transactional
132+
@Test
133+
@DisplayName("It supports counting")
134+
void test7() {
135+
long count =
136+
productRepository.findBy(
137+
idEquals(1L),
138+
ProductEntityGraph.____().brand().____.maker().country().____.____(),
139+
FluentQuery.FetchableFluentQuery::count);
140+
141+
assertThat(count).isEqualTo(1);
142+
}
143+
144+
@Transactional
145+
@Test
146+
@DisplayName("It uses default entity graphs when the entity graph less method is used")
147+
void test8() {
148+
Product product =
149+
productRepository.findBy(idEquals(1L), FluentQuery.FetchableFluentQuery::oneValue);
150+
151+
assertThat(Hibernate.isInitialized(product.getCategory())).isTrue();
152+
153+
assertThat(Hibernate.isInitialized(product.getBrand())).isFalse();
154+
assertThat(Hibernate.isInitialized(product.getMaker())).isFalse();
155+
assertThat(Hibernate.isInitialized(product.getMaker().getCountry())).isFalse();
156+
}
157+
158+
private static Specification<Product> idEquals(long id) {
159+
return (root, query, cb) -> cb.equal(root.get(Product_.id), id);
160+
}
161+
162+
public interface ProductRepository
163+
extends EntityGraphJpaRepository<Product, Long>,
164+
EntityGraphJpaSpecificationExecutor<Product> {
165+
166+
@Override
167+
default Optional<EntityGraph> defaultEntityGraph() {
168+
return Optional.of(ProductEntityGraph.____().category().____.____());
169+
}
170+
}
171+
}

0 commit comments

Comments
 (0)