Skip to content

Commit 20e0f6c

Browse files
committed
support optional chaining operator for left joins
1 parent cf49848 commit 20e0f6c

File tree

8 files changed

+55
-10
lines changed

8 files changed

+55
-10
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.7</version>
66+
<version>0.1.8</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.7
2+
:revnumber: 0.1.8
33
:jsondir: ../src/test/resources
44
:sectlinks: true
55
:source-highlighter: highlight.js

docs/query_specs.adoc

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ The second step (green):: The JSON is being deserialized into `com.github.mhewed
2626
and thus can be passed to your Repository (the one the extends `com.github.mhewedy.expressions.ExpressionsRepository`).
2727
+
2828
This intermediate step allows you to add additional conditions easily on the deserialized `Expressions` object as
29-
it is not uncommon you need to restrict the query passed from the frontend. +
29+
it is not uncommon you need to restrict the query passed from the frontend.
3030

3131
NOTE: You can see the <<#_public_api,Public API>> for details on how to use the `Expressions` and `Expression` objects.
3232

@@ -313,8 +313,9 @@ 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 `>`, example on left join (department left join city):
316+
==== Left and right joins
317+
1. *Left* join could be represented by *optional chaining operator* (`?.`). +
318+
Example on left join `deparetement` with `city`:
318319
+
319320
[source,json]
320321
----
@@ -326,9 +327,27 @@ Generated SQL:
326327
----
327328
...from employee e join department d on d.id = e.department_id left join city c on c.id = d.city_id where e.first_name = ? or c.name = ?
328329
----
330+
TIP: The `?.` operator in languages like JavaScript or Kotlin enables safe property access, directly mapping to a *left* join in SQL
331+
by allowing property retrieval if the referenced object exists while returning `null` instead of causing an error when it doesn't.
332+
2. Alternative notation (more toward SQL world) which support both *_left and right_* joins
333+
is to prefix entity name by `<` for left join and `>` for right join, example on left join (department left join city):
329334
+
330-
NOTE: `"<department.city.name"` means left join between `employee` (current object) and `department`,
335+
[source,json]
336+
----
337+
include::{jsondir}/testLeftJoinAlternateSyntax.json[]
338+
----
339+
Generated SQL:
340+
+
341+
[source,sql]
342+
----
343+
...from employee e join department d on d.id = e.department_id left join city c on c.id = d.city_id where e.first_name = ? or c.name = ?
344+
----
345+
+
346+
NOTE: `"<department.city.name"` means left join between `employee` (the current object) and `department`,
331347
whereas `"department.<city.name"` means left join between `departement` and `city`.
348+
+
349+
NOTE: Using *optional chaining operator* `"<department.city.name"` could be represented as `"department?.city.name"`
350+
and `"department.<city.name"` could be represented as `"department.city?.name"`.
332351

333352
=== Embedded
334353
1. Using embedded fields:

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.7</version>
15+
<version>0.1.8</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: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,15 +280,18 @@ private static ManagedType<?> extractSubFieldType(Attribute<?, ?> attribute) {
280280
}
281281

282282
private static String extractField(String field) {
283-
return field.contains(".") ? field.split("\\.")[0].replaceAll("^[<>]+", "") : field;
283+
return field.contains(".") ? field.split("\\.")[0]
284+
.replaceAll("^[<>]+", "") // remove '<' and '>' at start
285+
.replaceAll("\\?$", "") // remove '?' at end
286+
: field;
284287
}
285288

286289
private static SubField extractSubField(String field) {
287290
//if field is "abc.efg.xyz", then mainField=>"abc" and subField => "efg.xyz", so to support n-level association
288291
String mainField = Arrays.stream(field.split("\\.")).limit(1).collect(Collectors.joining("."));
289292
String subField = Arrays.stream(field.split("\\.")).skip(1).collect(Collectors.joining("."));
290293

291-
JoinType joinType = mainField.startsWith("<") ? JoinType.LEFT // <abc
294+
JoinType joinType = mainField.startsWith("<") || mainField.endsWith("?") ? JoinType.LEFT // <abc or abc?
292295
: mainField.startsWith(">") ? JoinType.RIGHT // >abc
293296
: JoinType.INNER; //// abc
294297
return new SubField(subField, joinType);

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,19 @@ public void testLeftJoin() throws JsonProcessingException {
743743
List<Employee> employeeList = employeeRepository.findAll(expressions);
744744
assertThat(employeeList.size()).isEqualTo(3);
745745

746+
// ...from employee e join department d on d.id = e.department_id left join city c on c.id = d.city_id where e.first_name = ? or c.name = ?
747+
}
748+
749+
@Test
750+
public void testLeftJoinAlternateSyntax() throws JsonProcessingException {
751+
String json = loadResourceJsonFile("testLeftJoinAlternateSyntax");
752+
753+
Expressions expressions = new ObjectMapper().readValue(json, Expressions.class);
754+
755+
List<Employee> employeeList = employeeRepository.findAll(expressions);
756+
assertThat(employeeList.size()).isEqualTo(3);
757+
758+
// ...from employee e join department d on d.id = e.department_id left join city c on c.id = d.city_id where e.first_name = ? or c.name = ?
746759
}
747760

748761
@SneakyThrows

src/test/resources/testLeftJoin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"firstName": "farida"
55
},
66
{
7-
"department.<city.name": "cairo"
7+
"department.city?.name": "cairo"
88
}
99
]
1010
}
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)