Skip to content

Commit

Permalink
Merge pull request #1 from marcopotok/release/1.0.0-alpha
Browse files Browse the repository at this point in the history
Release/1.0.0 alpha
  • Loading branch information
marcopotok authored Sep 3, 2022
2 parents 490a756 + cfbaa71 commit d048fc8
Show file tree
Hide file tree
Showing 24 changed files with 3,447 additions and 0 deletions.
35 changes: 35 additions & 0 deletions .gitignore
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
116 changes: 116 additions & 0 deletions README.md
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
48 changes: 48 additions & 0 deletions pom.xml
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>
37 changes: 37 additions & 0 deletions src/main/java/io/github/marcopotok/jpb/Clause.java
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 src/main/java/io/github/marcopotok/jpb/ClauseComposition.java
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 src/main/java/io/github/marcopotok/jpb/DefaultPrefetchEngine.java
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;
}
}
}
11 changes: 11 additions & 0 deletions src/main/java/io/github/marcopotok/jpb/Operator.java
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);
}
Loading

0 comments on commit d048fc8

Please sign in to comment.