Skip to content

Commit ba0b65d

Browse files
author
mhewedy
committed
allow doing left and right joins
1 parent 1b21012 commit ba0b65d

File tree

7 files changed

+74
-24
lines changed

7 files changed

+74
-24
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ For spring-boot 3.x:
6363
<dependency>
6464
<groupId>com.github.mhewedy</groupId>
6565
<artifactId>spring-data-jpa-mongodb-expressions</artifactId>
66-
<version>0.1.4</version>
66+
<version>0.1.5</version>
6767
</dependency>
6868

6969
```

docs/include.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
:author: Mohammad Hewedy, The Spring Data JPA MongoDB Expressions Team
2-
:revnumber: 0.1.4
2+
:revnumber: 0.1.5
33
:jsondir: ../src/test/resources
44
:sectlinks: true
55
:source-highlighter: highlight.js

docs/query_specs.adoc

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ https://docs.mongodb.com/manual/tutorial/query-documents/[MongoDB Query Language
88
and uses a subset of the language and converts it to Spring Data JPA Specifications under the hood.
99
1010
In this section we will see by example all the supported features started by basic querying to
11-
comparision operators to joins and other features.
11+
comparison operators to joins and other features.
1212
****
1313

1414
== Overview
@@ -313,6 +313,20 @@ in such case you can create a database _view_ and map it to an JPA Entity and th
313313

314314
TIP: The returned properties are the properties of the primary Entity, which means the `projection` is not supported due to limitation in spring-data-jpa addressed in this https://github.com/mhewedy/spring-data-jpa-mongodb-expressions/issues/4[bug,role=external,window=_blank], until it is fixed and if you need to return properties from other entities involved in the join, you need to follow the database _view_ workaround mentioned in the previous tip.
315315

316+
==== Left and right joins
317+
1. Left join uses notation `.<` and right join uses `.>`, exmaple on left join:
318+
+
319+
[source,json]
320+
----
321+
include::{jsondir}/testLeftJoin.json[]
322+
----
323+
Generated SQL:
324+
+
325+
[source,sql]
326+
----
327+
... from employee join department on department.id=employee.department_id left join city on city.id=department.city_id where employee.first_name=? or city.name=?
328+
----
329+
316330
=== Embedded
317331
1. Using embedded fields:
318332
+

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
<groupId>com.github.mhewedy</groupId>
1414
<artifactId>spring-data-jpa-mongodb-expressions</artifactId>
15-
<version>0.1.4</version>
15+
<version>0.1.5</version>
1616
<name>spring-data-jpa-mongodb-expressions</name>
1717
<description>Spring Data JPA Mongodb Expressions</description>
1818

src/main/java/com/github/mhewedy/expressions/ExpressionsPredicateBuilder.java

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package com.github.mhewedy.expressions;
22

3-
import org.springframework.util.Assert;
4-
53
import jakarta.persistence.criteria.*;
64
import jakarta.persistence.metamodel.Attribute;
75
import jakarta.persistence.metamodel.ManagedType;
86
import jakarta.persistence.metamodel.PluralAttribute;
97
import jakarta.persistence.metamodel.SingularAttribute;
8+
import org.springframework.util.Assert;
9+
1010
import java.time.*;
1111
import java.time.chrono.HijrahDate;
1212
import java.util.ArrayList;
@@ -16,9 +16,9 @@
1616
import java.util.stream.Collectors;
1717

1818
import static com.github.mhewedy.expressions.Expression.*;
19+
import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType;
1920
import static java.util.Collections.singletonList;
2021
import static java.util.stream.Collectors.toList;
21-
import static jakarta.persistence.metamodel.Attribute.PersistentAttributeType;
2222

2323
class ExpressionsPredicateBuilder {
2424

@@ -65,13 +65,13 @@ private static List<Predicate> getPredicates(CriteriaQuery<?> query, CriteriaBui
6565
query.distinct(true);
6666
}
6767

68-
final String subField = extractSubField(singularExpression.field);
69-
if (!subField.isEmpty()) {
68+
final SubField subField = extractSubField(singularExpression.field);
69+
if (!subField.name.isEmpty()) {
7070
final SingularExpression subExpression =
71-
new SingularExpression(subField, singularExpression.operator, singularExpression.value);
71+
new SingularExpression(subField.name, singularExpression.operator, singularExpression.value);
7272
predicates.addAll(
7373
getPredicates(query, cb,
74-
reuseOrCreateJoin((From<?, ?>) from, attribute, field),
74+
reuseOrCreateJoin((From<?, ?>) from, attribute, field, subField.joinType),
7575
extractSubFieldType(attribute),
7676
singletonList(subExpression)
7777
)
@@ -83,8 +83,8 @@ private static List<Predicate> getPredicates(CriteriaQuery<?> query, CriteriaBui
8383
Path exprPath = from.get((SingularAttribute) attribute);
8484

8585
if (PersistentAttributeType.EMBEDDED == attribute.getPersistentAttributeType()) {
86-
final String subField = extractSubField(singularExpression.field);
87-
attribute = extractSubFieldType(attribute).getAttribute(subField);
86+
final SubField subField = extractSubField(singularExpression.field);
87+
attribute = extractSubFieldType(attribute).getAttribute(subField.name);
8888
exprPath = exprPath.get((SingularAttribute) attribute);
8989
}
9090

@@ -188,13 +188,13 @@ private static List<Predicate> getPredicates(CriteriaQuery<?> query, CriteriaBui
188188
query.distinct(true);
189189
}
190190

191-
final String subField = extractSubField(listExpression.field);
192-
if (!subField.isEmpty()) {
191+
final SubField subField = extractSubField(listExpression.field);
192+
if (!subField.name.isEmpty()) {
193193
final ListExpression subExpression =
194-
new ListExpression(subField, listExpression.operator, listExpression.values);
194+
new ListExpression(subField.name, listExpression.operator, listExpression.values);
195195
predicates.addAll(
196196
getPredicates(query, cb,
197-
reuseOrCreateJoin((From<?, ?>) from, attribute, field),
197+
reuseOrCreateJoin((From<?, ?>) from, attribute, field, subField.joinType),
198198
extractSubFieldType(attribute),
199199
singletonList(subExpression)
200200
)
@@ -206,8 +206,8 @@ private static List<Predicate> getPredicates(CriteriaQuery<?> query, CriteriaBui
206206
Path exprPath = from.get((SingularAttribute) attribute);
207207

208208
if (PersistentAttributeType.EMBEDDED == attribute.getPersistentAttributeType()) {
209-
final String subField = extractSubField(listExpression.field);
210-
attribute = extractSubFieldType(attribute).getAttribute(subField);
209+
final SubField subField = extractSubField(listExpression.field);
210+
attribute = extractSubFieldType(attribute).getAttribute(subField.name);
211211
exprPath = exprPath.get((SingularAttribute) attribute);
212212
}
213213

@@ -266,11 +266,11 @@ private static List<Predicate> getPredicates(CriteriaQuery<?> query, CriteriaBui
266266
}
267267
}
268268

269-
private static Path<?> reuseOrCreateJoin(From<?, ?> from, Attribute<?, ?> attribute, String field) {
269+
private static Path<?> reuseOrCreateJoin(From<?, ?> from, Attribute<?, ?> attribute, String field, JoinType joinType) {
270270
return from.getJoins().stream()
271271
.filter(it -> it.getAttribute() == attribute)
272272
.findFirst()
273-
.orElseGet(() -> from.join(field));
273+
.orElseGet(() -> from.join(field, joinType));
274274
}
275275

276276
@SuppressWarnings({"rawtypes"})
@@ -283,9 +283,23 @@ private static String extractField(String field) {
283283
return field.contains(".") ? field.split("\\.")[0] : field;
284284
}
285285

286-
private static String extractSubField(String field) {
286+
private static SubField extractSubField(String field) {
287287
//if field is "abc.efg.xyz", then return "efg.xyz", so to support n-level association
288-
return Arrays.stream(field.split("\\.")).skip(1).collect(Collectors.joining("."));
288+
String subField = Arrays.stream(field.split("\\.")).skip(1).collect(Collectors.joining("."));
289+
JoinType joinType;
290+
if (subField.startsWith("<")) { /// .<
291+
subField = subField.substring(1);
292+
joinType = JoinType.LEFT;
293+
} else if (subField.startsWith(">")) { /// .>
294+
subField = subField.substring(1);
295+
joinType = JoinType.RIGHT;
296+
} else {
297+
joinType = JoinType.INNER;
298+
}
299+
return new SubField(subField, joinType);
300+
}
301+
302+
private record SubField(String name, JoinType joinType) {
289303
}
290304

291305
@SuppressWarnings({"rawtypes", "unchecked"})

src/test/java/com/github/mhewedy/expressions/ExpressionsRepositoryImplTest.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.github.mhewedy.expressions;
22

3+
import com.fasterxml.jackson.core.JsonProcessingException;
34
import com.fasterxml.jackson.databind.ObjectMapper;
45
import com.github.mhewedy.expressions.model.*;
56
import jakarta.persistence.EntityManager;
@@ -89,7 +90,7 @@ public void setup() {
8990
Instant.parse("2009-12-03T10:15:30.00Z"),
9091
(short) 1,
9192
true,
92-
new Department(null, "sw arch", new City(null, "giaz")),
93+
new Department(null, "sw arch", null),
9394
Arrays.asList(new Task(null, "fix sw arch", ACTIVE), new Task(null, "fix sw arch", ACTIVE)),
9495
UUID.randomUUID(),
9596
Lang.AR,
@@ -733,6 +734,17 @@ public void testTheList() {
733734
// where last_name=? and (age in (? , ?) or birth_date<?)
734735
}
735736

737+
@Test
738+
public void testLeftJoin() throws JsonProcessingException {
739+
String json = loadResourceJsonFile("testLeftJoin");
740+
741+
Expressions expressions = new ObjectMapper().readValue(json, Expressions.class);
742+
743+
List<Employee> employeeList = employeeRepository.findAll(expressions);
744+
assertThat(employeeList.size()).isEqualTo(3);
745+
746+
}
747+
736748
@SneakyThrows
737749
private String loadResourceJsonFile(String name) {
738750
File file = ResourceUtils.getFile("classpath:" + name + ".json");

src/test/resources/testLeftJoin.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"$or": [
3+
{
4+
"firstName": "farida"
5+
},
6+
{
7+
"department.<city.name": "cairo"
8+
}
9+
]
10+
}

0 commit comments

Comments
 (0)