Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/querystream/querystream

Build JPA Criteria queries using a Stream-like API
https://github.com/querystream/querystream

java jpa jpa-criteria-api persistence streams

Last synced: about 2 months ago
JSON representation

Build JPA Criteria queries using a Stream-like API

Awesome Lists containing this project

README

        

### QueryStream

QueryStream allows you to perform JPA queries using a `Stream`-like API.

Just like a Java `Stream`, a `QueryStream` is built up in a pipeline, using methods like `map()`, `flatMap()`, `filter()`, etc.

Each step in a `QueryStream` pipeline modifies the construction of an internal [JPA Criteria query](https://jakarta.ee/specifications/platform/10/apidocs/jakarta/persistence/criteria/package-summary.html).

When you're ready to execute the pipeline:

1. Invoke `QueryStream.toCriteriaQuery()` to extract the [CriteriaQuery](https://jakarta.ee/specifications/platform/10/apidocs/jakarta/persistence/criteria/CriteriaQuery.html); or
1. Invoke `QueryStream.toQuery()` to do #1 and also create a [TypedQuery](https://jakarta.ee/specifications/platform/10/apidocs/jakarta/persistence/TypedQuery.html); or
1. Invoke `QueryStream.getResultList()` or `QueryStream.getResultStream()` to do #1 and #2 execute the query

## Example

Payroll costs are getting out of control. You need a list of all managers whose direct reports have an average salary above $50,000, ordered from highest to lowest.

Here's how you'd build a Criteria query the usual way:

```java
public List getHighPayrollManagers(EntityManager entityManager) {

// Construct query
final CriteriaQuery criteriaQuery = cb.createQuery();
final Root manager = criteriaQuery.from(Employee.class);
final SetJoin directReports = manager.join(Employee_.directReports);
final Expression avgSalary = cb.avg(directReports.get(Employee_.salary));
criteriaQuery.where(cb.greaterThan(avgSalary, 50000.0))
criteriaQuery.groupBy(manager);
criteriaQuery.orderBy(avgSalary);

// Execute query
final TypedQuery typedQuery = entityManager.createQuery(criteriaQuery);
return typedQuery.getResultList();
}

@PersistenceContext
private EntityManager entityManager;
private CriteriaBuilder cb;

@PostConstruct
private void setupCriteriaBuilder() {
this.cb = this.entityManager.getCriteriaBuilder();
}
```

With QueryStream, your code becomes more succint and visually intuitive:

```java
public List getHighPayrollManagers() {

// Create a couple of references
final RootRef manager = new RootRef<>();
final ExprRef avgSalary = new ExprRef<>();

// Build and execute query
return qb.stream(Employee.class)
.bind(manager)
.flatMap(Employee_.directReports)
.mapToDouble(Employee_.salary)
.average()
.filter(v -> qb.greaterThan(v, 50000))
.bind(avgSalary)
.groupBy(manager)
.orderBy(avgSalary, false)
.getResultList();
}

@PersistenceContext
private EntityManager entityManager;
private QueryStream.Builder qb;

@PostConstruct
private void setupBuilders() {
this.qb = QueryStream.newBuilder(this.entityManager);
}
```

[See below](#references) for more information about references.

## Bulk Updates and Deletes

Bulk deletes and updates are also supported.

The [QueryStream](http://querystream.github.io/querystream/site/apidocs/index.html?org/dellroad/querystream/jpa/QueryStream.html) interface has three subinterfaces for searches, bulk deletes, and bulk updates; these are [SearchStream](http://querystream.github.io/querystream/site/apidocs/index.html?org/dellroad/querystream/jpa/SearchStream), [DeleteStream](http://querystream.github.io/querystream/site/apidocs/index.html?org/dellroad/querystream/jpa/DeleteStream), and [UpdateStream](http://querystream.github.io/querystream/site/apidocs/index.html?org/dellroad/querystream/jpa/UpdateStream).

* A [SearchStream](http://querystream.github.io/querystream/site/apidocs/index.html?org/dellroad/querystream/jpa/SearchStream.html) builds an internal [CriteriaQuery](https://jakarta.ee/specifications/platform/10/apidocs/jakarta/persistence/criteria/CriteriaQuery.html) instance
* A [DeleteStream](http://querystream.github.io/querystream/site/apidocs/index.html?org/dellroad/querystream/jpa/DeleteStream.html) builds an internal [CriteriaDelete](https://jakarta.ee/specifications/platform/10/apidocs/jakarta/persistence/criteria/CriteriaDelete.html) instance
* An [UpdateStream](http://querystream.github.io/querystream/site/apidocs/index.html?org/dellroad/querystream/jpa/UpdateStream.html) builds an internal [CriteriaUpdate](https://jakarta.ee/specifications/platform/10/apidocs/jakarta/persistence/criteria/CriteriaUpdate.html) instance

## Single Values

Some queries are known to return a single value. The [SearchValue](http://querystream.github.io/querystream/site/apidocs/index.html?org/dellroad/querystream/jpa/SearchValue.html) and its subinterfaces represent streams for which it is known that at most one result will be found. These interfaces have a `value()` method, which executes the query and returns the single value:

```java
public double getAverageSalary(Employee manager) {
return qb.stream(Employee.class)
.filter(e -> qb.equal(e, manager))
.flatMap(Employee_.directReports)
.mapToDouble(Employee_.salary)
.average()
.value();
}
```

Other similar methods are `min()`, `max()`, `sum()`, and `findFirst()`.

Value queries can be converted to `Optional`s and have several related convenience methods like `orElse()`, `isPresent()`, etc.

## References

[Ref](http://querystream.github.io/querystream/site/apidocs/index.html?org/dellroad/querystream/jpa/Ref.html) objects give you a way to refer to items in the stream pipeline at a later step, by `bind()`'ing the reference at an earlier step.

References also help code clarity, because they provide a way to give meaningful names to important expressions.

See the `getHighPayrollManagers()` example above for how it works. The main thing to remember is that the `bind()` must occur prior to the use of the reference in the pipeline.

## Subqueries

QueryStream makes using subqueries easier. A stream can be used as a subquery via `asSubquery()` or `exists()`.

To find all managers for whom there exists a direct report making over $100,000:

```java
public List findManagersWithSixFigureDirectReport() {
return qb.stream(Employee.class)
.filter(manager -> qb.stream(Employee.class)
.filter(report -> qb.and(
qb.equal(report.get(Employee_.manager), manager),
qb.greaterThan(report.get(Employee_.salary), 100000.0)))
.exists());
}
```

Note the subquery correlation was done "manually" using `CriteriaBuilder.equal()`; you can clean this up a bit with an explicit correlation using `substream()`:

```java
public List findManagersWithSixFigureDirectReport() {
return qb.stream(Employee.class)
.filter(manager -> qb.substream(manager)
.flatMap(Employee_.directReports)
.filter(report -> qb.greaterThan(report.get(Employee_.salary), 100000.0))
.exists());
}
```

To find all employees with salary greater than the average of their manager's direct reports:

```java
public List findEmployeesWithAboveAverageSalaries() {
return qb.stream(Employee.class)
.filter(employee -> qb.greaterThan(
employee.get(Employee_.salary),
qb.stream(Employee.class)
.filter(coworker ->
qb.equal(
coworker.get(Employee_.manager),
employee.get(Employee_.manager)))
.mapToDouble(Employee_.salary)
.average()
.asSubquery()))
.getResultList();
}
```

Hmmm, that's a lot of nesting. You could make the code clearer by building the subquery separately, and using a reference for the correlation:

```java
public DoubleValue avgCoworkerSalary(RootRef employee) {
return qb.stream(Employee.class)
.filter(coworker -> qb.equal(coworker.get(Employee_.manager), employee.get().get(Employee_.manager)))
.mapToDouble(Employee_.salary)
.average();
}

public List findEmployeesWithAboveAverageSalaries() {
final RootRef employee = new RootRef<>();
return qb.stream(Employee.class)
.bind(employee)
.filter(e -> qb.greaterThan(e.get(Employee_.salary), this.avgCoworkerSalary(employee).asSubquery()))
.getResultList();
}
```
That's really just regular Java refactoring.

## Multiselect and Grouping

To select multiple items, or construct a Java instance, use `mapToSelection()`.

For grouping, use `groupBy()` and `having()`.

Here's an example that finds all managers paired with the average salary of their direct reports, where that average salary is at least $50,000, sorted by average salary descending:

```java
public List getHighPayrollManagers2() {

// Create references
final RootRef manager = new RootRef<>();
final ExprRef avgSalary = new ExprRef<>();

// Build and execute stream
return qb.stream(Employee.class)
.bind(manager)
.flatMap(Employee_.directReports)
.mapToDouble(Employee_.salary)
.average()
.bind(avgSalary)
.groupBy(manager)
.mapToSelection(Object[].class, e -> qb.array(manager.get(), avgSalary.get()))
.orderBy(avgSalary, false);
.having(avgSalary -> qb.gt(avgSalary, 50000.0))
.getResultList();
}
```

## Offset and Limit

Use `skip()` and `limit()` to set the row offset and the maximum number of results.

## `CriteriaQuery` vs `TypedQuery` Operations

Normally with JPA you first configure a `CriteriaQuery`, then use it to create a `TypedQuery`.

Both the `CriteriaQuery` and the `TypedQuery` have their own configuration.

For example, `where()` is a `CriteriaQuery` method, but `setLockMode()` is a `TypedQuery` method.

So at the end of the day, we must apply configuration to the appropriate object for that configuration.

`QueryStream` pipelines allow you to configure both `CriteriaQuery` and `TypedQuery` information, but you should be aware of these caveats:

* Any `TypedQuery` configuration must come at the end of the pipeline (after any subqueries or joins)
* If you invoke `QueryStream.toCriteriaQuery()`, the returned object does not capture any `TypedQuery` configuration
* To capture the `TypedQuery` configuration as well, use `QueryStream.toQuery()`.

The `QueryStream` methods that configure the `TypedQuery` are:

* `QueryStream.skip()`
* `QueryStream.limit()`
* `QueryStream.withFlushMode()`
* `QueryStream.withLockMode()`
* `QueryStream.withFetchGraph()`
* `QueryStream.withLoadGraph()`
* `QueryStream.withHint()` and `QueryStream.withHints()`

## Unsupported Operations

In some cases, limitations in the JPA Criteria API impose certain restrictions on what you can do.

For example, `skip()` and `limit()` must be at the end of your pipeline, because the JPA Criteria API doesn't allow setting row offset or the maximum results on a subquery or prior to a join.

For another example, you can't sort in a subquery.

### Installation

QueryStream is available from [Maven Central](http://search.maven.org/#search|ga|1|a%3Aquerystream-jpa):

```xml

org.dellroad
querystream-jpa

```

### API Javadocs

Located [here](https://querystream.github.io/querystream/site/apidocs/index.html?org/dellroad/querystream/jpa/QueryStream.Builder.html).