-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from marcopotok/release/1.0.0-alpha
Release/1.0.0 alpha
- Loading branch information
Showing
24 changed files
with
3,447 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
target/ | ||
!.mvn/wrapper/maven-wrapper.jar | ||
!**/src/main/**/target/ | ||
!**/src/test/**/target/ | ||
|
||
### IntelliJ IDEA ### | ||
/.idea/ | ||
*.iws | ||
*.iml | ||
*.ipr | ||
|
||
### Eclipse ### | ||
.apt_generated | ||
.classpath | ||
.factorypath | ||
.project | ||
.settings | ||
.springBeans | ||
.sts4-cache | ||
|
||
### NetBeans ### | ||
/nbproject/private/ | ||
/nbbuild/ | ||
/dist/ | ||
/nbdist/ | ||
/.nb-gradle/ | ||
build/ | ||
!**/src/main/**/build/ | ||
!**/src/test/**/build/ | ||
|
||
### VS Code ### | ||
.vscode/ | ||
|
||
### Mac OS ### | ||
.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
# JPA Predicate Builder | ||
|
||
A lightweight layer on top of Javax Persistence for easy query construction in Java. | ||
|
||
# Project Description | ||
|
||
Writing query on for java application faces many challenges, mainly cleanness of code, maintainability and performances. This project aims to address all three of these with a simple builder created on top of Jakarta Persistence APIs. | ||
Other libraries already exists, but usually make the integration difficult and not always they work with other libraries (e.g. Lombok). In this case we wanted a library with no extra dependencies and ready to go. | ||
|
||
Key features: | ||
|
||
- predicates concatenation with a builder style | ||
- works seamlessly with jpa specifications | ||
- implementation agnostic | ||
- remove duplicated joins | ||
- easy fetch of related entities (prefetching) | ||
|
||
# How to install | ||
|
||
To install it is enough to add the dependency to your pom file. | ||
|
||
# How to use | ||
|
||
To create your predicate, start with one constructor or factory method available: | ||
```java | ||
import java.util.Collection; | ||
|
||
import io.github.marcopotok.jpb.PredicateBuilder; | ||
import jakarta.persistence.EntityManager; | ||
import jakarta.persistence.criteria.CriteriaBuilder; | ||
|
||
class UserService { | ||
|
||
private final EntityManager entityManager; | ||
|
||
public UserService(EntityManager entityManager) { | ||
this.entityManager = entityManager; | ||
} | ||
|
||
private Collection<User> getUserWithName(String name) { | ||
Session session = entityManager.unwrap(Session.class); | ||
CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder(); | ||
CriteriaQuery<User> query = criteriaBuilder.createQuery(User.class); | ||
Root<User> root = query.from(User.class); | ||
query.where(PredicateBuilder.of(User.class).withProperty("name", name).build(root, query, criteriaBuilder)); | ||
return session.createQuery(query).getResultList(); | ||
} | ||
} | ||
``` | ||
## Specification | ||
Another way to get the same result with specifications: | ||
```java | ||
import java.util.Collection; | ||
|
||
class UserService { | ||
|
||
private final UserRepository userRepository; | ||
|
||
public UserService(UserRepository userRepository) { | ||
this.userRepository = userRepository; | ||
} | ||
|
||
private Collection<User> getUserWithName(String name) { | ||
Specification<User> specification = PredicateBuilder.of(User.class).withProperty("name", name)::build; | ||
return userRepository.findAll(specification); | ||
} | ||
} | ||
``` | ||
## Joins | ||
In order to filter by an attribute of a relation, use the dot notation. For example, if you want to find all the orders of a user, you can write: | ||
```java | ||
import java.util.Collection; | ||
|
||
class OrderService { | ||
|
||
private final OrderRepository orderRepository; | ||
|
||
public OrderService(OrderRepository orderRepository) { | ||
this.orderRepository = orderRepository; | ||
} | ||
|
||
private Collection<Order> getOrdersOfUser(String userId) { | ||
Specification<Order> specification = PredicateBuilder.of(Order.class).withProperty("user.id", userId)::build; | ||
return orderRepository.findAll(specification); | ||
} | ||
} | ||
``` | ||
## Prefetch | ||
To avoid multiple queries with a lazy relationship with another entity, you can use the prefetch method. | ||
```java | ||
import java.util.Collection; | ||
|
||
import io.github.marcopotok.jpb.PredicateBuilder; | ||
|
||
class OrderService { | ||
|
||
private final OrderRepository orderRepository; | ||
|
||
public OrderService(OrderRepository orderRepository) { | ||
this.orderRepository = orderRepository; | ||
} | ||
|
||
private Collection<Order> getOrdersOfUser(String userId) { | ||
Specification<Order> specification = PredicateBuilder.of(Order.class) | ||
.prefetch("user.[nested.deep,other.deep]") | ||
.withProperty("user.id", userId)::build; | ||
return orderRepository.findAll(specification); | ||
} | ||
} | ||
``` | ||
In this case the engine will perform a fetch on _user_, _user.nested_, _user.nested.deep_, _user.other_ and _user.other.deep_. Note that the fetch with the _user_ entity is **not** duplicated. | ||
# Limitations and further improvements | ||
|
||
Current limitation and possible future improvements: | ||
- Only left joins -> possible auto detection | ||
- Add field checking at build time |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
<groupId>io.github.marcopotok</groupId> | ||
<artifactId>jpa-predicate-builder</artifactId> | ||
<version>1.0.0-alpha-1</version> | ||
<packaging>jar</packaging> | ||
|
||
<name>io.github.marcopotok:jpa-predicate-builder</name> | ||
<description>Lightweight layer on top of Java Persistence for easy query construction in Java</description> | ||
<url>https://github.com/marcopotok/jpa-predicate-builder</url> | ||
|
||
<properties> | ||
<maven.compiler.source>18</maven.compiler.source> | ||
<maven.compiler.target>18</maven.compiler.target> | ||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||
</properties> | ||
|
||
<licenses> | ||
<license> | ||
<name>MIT License</name> | ||
<url>http://www.opensource.org/licenses/mit-license.php</url> | ||
</license> | ||
</licenses> | ||
<dependencies> | ||
<dependency> | ||
<groupId>javax.persistence</groupId> | ||
<artifactId>javax.persistence-api</artifactId> | ||
<version>2.2</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.junit.jupiter</groupId> | ||
<artifactId>junit-jupiter</artifactId> | ||
<version>5.9.0</version> | ||
<scope>test</scope> | ||
</dependency> | ||
</dependencies> | ||
|
||
<build> | ||
<plugins> | ||
<plugin> | ||
<groupId>org.apache.maven.plugins</groupId> | ||
<artifactId>maven-surefire-plugin</artifactId> | ||
<version>2.22.0</version> | ||
</plugin> | ||
</plugins> | ||
</build> | ||
</project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package io.github.marcopotok.jpb; | ||
|
||
import javax.persistence.criteria.CriteriaBuilder; | ||
import javax.persistence.criteria.Predicate; | ||
|
||
@FunctionalInterface | ||
public interface Clause { | ||
|
||
Clause CONJUNCTION = (criteriaBuilder, pathProvider) -> criteriaBuilder.conjunction(); | ||
Clause DISJUNCTION = (criteriaBuilder, pathProvider) -> criteriaBuilder.disjunction(); | ||
|
||
/** | ||
* Converts the clause to a {@link Predicate} | ||
* @param criteriaBuilder - must not be null | ||
* @param pathProvider - must not be null | ||
* @return the predicate | ||
*/ | ||
Predicate toPredicate(CriteriaBuilder criteriaBuilder, PathProvider pathProvider); | ||
|
||
/** | ||
* Concatenates two clauses with a logic AND | ||
* @param clause - can be null | ||
* @return the result clause | ||
*/ | ||
default Clause and(Clause clause) { | ||
return ClauseComposition.composed(this, clause, CriteriaBuilder::and); | ||
} | ||
|
||
/** | ||
* Concatenates two clauses with a logic OR | ||
* @param clause - can be null | ||
* @return the result clause | ||
*/ | ||
default Clause or(Clause clause) { | ||
return ClauseComposition.composed(this, clause, CriteriaBuilder::or); | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
src/main/java/io/github/marcopotok/jpb/ClauseComposition.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package io.github.marcopotok.jpb; | ||
|
||
import java.io.Serializable; | ||
|
||
import javax.persistence.criteria.CriteriaBuilder; | ||
import javax.persistence.criteria.Predicate; | ||
|
||
class ClauseComposition { | ||
|
||
interface Combiner extends Serializable { | ||
Predicate combine(CriteriaBuilder builder, Predicate lhs, Predicate rhs); | ||
} | ||
|
||
static Clause composed(Clause lhs, Clause rhs, Combiner combiner) { | ||
return (builder, provider) -> { | ||
Predicate thisPredicate = toPredicate(lhs, builder, provider); | ||
Predicate otherPredicate = toPredicate(rhs, builder, provider); | ||
if (thisPredicate == null) { | ||
return otherPredicate; | ||
} | ||
return otherPredicate == null ? thisPredicate : combiner.combine(builder, thisPredicate, otherPredicate); | ||
}; | ||
} | ||
|
||
private static Predicate toPredicate(Clause clause, CriteriaBuilder criteriaBuilder, PathProvider pathProvider) { | ||
return clause == null ? null : clause.toPredicate(criteriaBuilder, pathProvider); | ||
} | ||
} |
101 changes: 101 additions & 0 deletions
101
src/main/java/io/github/marcopotok/jpb/DefaultPrefetchEngine.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package io.github.marcopotok.jpb; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.regex.Matcher; | ||
import java.util.regex.Pattern; | ||
|
||
import javax.persistence.criteria.CriteriaQuery; | ||
import javax.persistence.criteria.Fetch; | ||
import javax.persistence.criteria.FetchParent; | ||
import javax.persistence.criteria.JoinType; | ||
import javax.persistence.criteria.Root; | ||
|
||
class DefaultPrefetchEngine implements PrefetchEngine { | ||
private static final Pattern NESTED_ATTRIBUTES_MATCHER = Pattern.compile("^\\[(.*)]$"); | ||
private static final int NESTED_LIST_GROUP = 1; | ||
private static final char LIST_START_CHAR = '['; | ||
private static final char LIST_END_CHAR = ']'; | ||
private static final String ATTRIBUTE_CHAIN_DELIMITER = "\\."; | ||
private static final char ATTRIBUTES_DELIMITER = ','; | ||
|
||
private final Map<String, Fetch<?, ?>> fetchCache = new HashMap<>(); | ||
|
||
@Override | ||
public <T> void prefetch(String attributeList, Root<T> root, CriteriaQuery<?> query) { | ||
if (Long.class != query.getResultType() && long.class != query.getResultType() && !attributeList.isBlank()) { | ||
prefetch(root, attributeList, ""); | ||
} | ||
} | ||
|
||
private void prefetch(FetchParent<?, ?> node, String attributeList, String currentPath) { | ||
String[] split = splitSameLevel(attributeList); | ||
for (String rootAttributes : split) { | ||
prefetchChain(node, currentPath, rootAttributes); | ||
} | ||
} | ||
|
||
private void prefetchChain(FetchParent<?, ?> node, String currentPath, String rootAttributes) { | ||
String[] attributes = getAttributes(rootAttributes); | ||
FetchParent<?, ?> nodeFetch = node; | ||
for (String attribute : attributes) { | ||
Matcher matcher = NESTED_ATTRIBUTES_MATCHER.matcher(attribute); | ||
if (matcher.matches()) { | ||
prefetch(nodeFetch, matcher.group(NESTED_LIST_GROUP), currentPath); | ||
} else { | ||
currentPath += "." + attribute; | ||
nodeFetch = fetch(currentPath, nodeFetch, attribute); | ||
} | ||
} | ||
} | ||
|
||
private String[] splitSameLevel(String attributeList) { | ||
List<Integer> splitIndexes = calculateSplitIndexes(attributeList); | ||
String[] split = new String[splitIndexes.size() + 1]; | ||
for (int i = 0, l = splitIndexes.size() + 1; i < l; i++) { | ||
boolean firstIteration = i == 0; | ||
boolean lastIteration = i == l - 1; | ||
int beginIndex = firstIteration ? 0 : splitIndexes.get(i - 1) + 1; | ||
int endIndex = lastIteration ? attributeList.length() : splitIndexes.get(i); | ||
split[i] = attributeList.substring(beginIndex, endIndex); | ||
} | ||
return split; | ||
} | ||
|
||
private List<Integer> calculateSplitIndexes(String attributeList) { | ||
List<Integer> splitIndexes = new ArrayList<>(); | ||
char[] charArray = attributeList.toCharArray(); | ||
for (int i = 0, nestingLevel = 0, charArrayLength = charArray.length; i < charArrayLength; i++) { | ||
char currentChar = charArray[i]; | ||
if (currentChar == LIST_START_CHAR) { | ||
nestingLevel++; | ||
} | ||
if (currentChar == LIST_END_CHAR) { | ||
nestingLevel--; | ||
} | ||
if (nestingLevel == 0 && currentChar == ATTRIBUTES_DELIMITER) { | ||
splitIndexes.add(i); | ||
} | ||
} | ||
return splitIndexes; | ||
} | ||
|
||
private Fetch<?, ?> fetch(String currentPath, FetchParent<?, ?> node, String attributePath) { | ||
return fetchCache.computeIfAbsent(currentPath, ignored -> node.fetch(attributePath, JoinType.LEFT)); | ||
} | ||
|
||
private String[] getAttributes(String attributeList) { | ||
int indexOfList = attributeList.indexOf(LIST_START_CHAR); | ||
if (indexOfList < 0) { | ||
return attributeList.split(ATTRIBUTE_CHAIN_DELIMITER); | ||
} else { | ||
String[] split = attributeList.substring(0, indexOfList).split(ATTRIBUTE_CHAIN_DELIMITER); | ||
String[] attributes = Arrays.copyOf(split, split.length + 1); | ||
attributes[attributes.length - 1] = attributeList.substring(indexOfList); | ||
return attributes; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package io.github.marcopotok.jpb; | ||
|
||
import javax.persistence.criteria.CriteriaBuilder; | ||
import javax.persistence.criteria.Expression; | ||
import javax.persistence.criteria.Predicate; | ||
|
||
@FunctionalInterface | ||
interface Operator<U> { | ||
|
||
Predicate toPredicate(U value, Expression<U> path, CriteriaBuilder criteriaBuilder); | ||
} |
Oops, something went wrong.