
org.dellroad.querystream.jpa.QueryStream Maven / Gradle / Ivy
Show all versions of querystream-jpa Show documentation
/*
* Copyright (C) 2018 Archie L. Cobbs. All rights reserved.
*/
package org.dellroad.querystream.jpa;
import jakarta.persistence.EntityManager;
import jakarta.persistence.FlushModeType;
import jakarta.persistence.LockModeType;
import jakarta.persistence.Parameter;
import jakarta.persistence.Query;
import jakarta.persistence.TemporalType;
import jakarta.persistence.criteria.CollectionJoin;
import jakarta.persistence.criteria.CommonAbstractCriteria;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaDelete;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.CriteriaUpdate;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.From;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.ListJoin;
import jakarta.persistence.criteria.MapJoin;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.criteria.Selection;
import jakarta.persistence.criteria.SetJoin;
import jakarta.persistence.criteria.Subquery;
import jakarta.persistence.metamodel.SingularAttribute;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import org.dellroad.querystream.jpa.querytype.QueryType;
import org.dellroad.querystream.jpa.querytype.SearchType;
import org.dellroad.querystream.jpa.util.ForwardingCriteriaBuilder;
/**
* Builder for JPA criteria queries, based on configuration through a {@link java.util.stream.Stream}-like API.
*
* @param stream item type
* @param criteria type for stream item
* @param configured criteria API query type
* @param final criteria API query type
* @param JPA query type
*/
public interface QueryStream,
C extends CommonAbstractCriteria,
C2 extends C,
Q extends Query> extends QueryConfigurer {
// QueryType
/**
* Get the {@link QueryType} of this instance.
*
* @return associated {@link QueryType}
*/
QueryType getQueryType();
// Queryification
/**
* Get the {@link EntityManager} associated with this instance.
*
* @return associated {@link EntityManager}
*/
EntityManager getEntityManager();
/**
* Build a criteria API query based on this instance.
*
*
* Note that due to limitations of the JPA Criteria API, the returned query object lacks information that is configured
* on the {@link Query} object and not the {@link CriteriaQuery} object, including
* {@linkplain #getLockMode lock mode}, {@linkplain #withHint hints}, {@linkplain #withParam parameters},
* row {@linkplain #getFirstResult offset} and {@linkplain #getMaxResults limit}, etc.
*
*
* This information can only be configured on the fully formed {@link Query}; use {@link #toQuery} instead of this
* method to also include that information.
*
* @return new Criteria API query corresponding to this instance
* @see #toQuery
*/
C2 toCriteriaQuery();
/**
* Build a fully configured JPA query based on this instance.
*
* @return new JPA query corresponding to this instance
*/
Q toQuery();
/**
* Get the row offset associated with this query.
*
* @return row offset, or -1 if there is none configured
* @see #skip
* @see Query#setFirstResult
*/
int getFirstResult();
/**
* Get the row limit associated with this query.
*
* @return row limit, or -1 if there is none configured
* @see #limit
* @see Query#setMaxResults
*/
int getMaxResults();
/**
* Get the {@link FlushModeType} associated with this query.
*
* @return flush mode, or null if none set (i.e., use {@link EntityManager} setting)
* @see Query#setFlushMode
*/
FlushModeType getFlushMode();
/**
* Set the {@link FlushModeType} associated with this query.
*
* @param flushMode new flush mode
* @return new stream with the specified flush mode configured
* @see Query#setFlushMode
* @throws IllegalArgumentException if {@code flushMode} is null
*/
QueryStream withFlushMode(FlushModeType flushMode);
/**
* Get the {@link LockModeType} associated with this query.
*
* @return lock mode, or null if none set
* @see Query#setLockMode
*/
LockModeType getLockMode();
/**
* Set the {@link LockModeType} associated with this query.
*
* @param lockMode new lock mode
* @return new stream with the specified lock mode configured
* @see Query#setLockMode
* @throws IllegalArgumentException if {@code lockMode} is null
*/
QueryStream withLockMode(LockModeType lockMode);
/**
* Get any hints associated with this query.
*
* @return configured hints, if any, otherwise an empty map
* @see Query#setHint
* @return immutable map of hints
*/
Map getHints();
/**
* Associate a hint with this query.
*
* @param name name of hint
* @param value value of hint
* @return new stream with the specified hint configured
* @see Query#setHint
* @throws IllegalArgumentException if {@code lockMode} is null
*/
QueryStream withHint(String name, Object value);
/**
* Associate hints with this query.
*
* @param hints hints to add
* @return new stream with the specified hints added
* @see Query#setHint
* @throws IllegalArgumentException if {@code hints} is null
*/
QueryStream withHints(Map hints);
/**
* Get any parameter bindings associated with this query.
*
* @return configured parameter bindings, if any, otherwise an empty set
* @return immutable set of parameter bindings
* @see Query#setParameter(Parameter, Object)
*/
Set> getParams();
/**
* Bind the value of a query parameter.
*
*
* Replaces any previous binding of the same parameter.
*
* @param parameter the parameter to set
* @param value parameter value
* @param parameter value type
* @return new stream with the specified parameter value set
* @throws IllegalArgumentException if {@code parameter} is null
* @see Query#setParameter(Parameter, Object)
*/
QueryStream withParam(Parameter parameter, T value);
/**
* Bind the value of a query parameter of type {@link Date}.
*
*
* Replaces any previous binding of the same parameter.
*
* @param parameter the parameter to set
* @param value parameter value
* @param temporalType temporal type for {@code value}
* @return new stream with the specified parameter value set
* @throws IllegalArgumentException if {@code parameter} or {@code temporalType} is null
* @see Query#setParameter(Parameter, Date, TemporalType)
*/
QueryStream withParam(Parameter parameter, Date value, TemporalType temporalType);
/**
* Bind the value of a query parameter of type {@link Calendar}.
*
*
* Replaces any previous binding of the same parameter.
*
* @param parameter the parameter to set
* @param value parameter value
* @param temporalType temporal type for {@code value}
* @return new stream with the specified parameter value set
* @throws IllegalArgumentException if {@code parameter} or {@code temporalType} is null
* @see Query#setParameter(Parameter, Calendar, TemporalType)
*/
QueryStream withParam(Parameter parameter, Calendar value, TemporalType temporalType);
/**
* Associate parameter bindings with this query.
*
*
* Replaces any previous bindings of the same parameters.
*
* @param params bindings to add
* @return new stream with the specified parameter bindings added
* @throws IllegalArgumentException if {@code params} or any contained element is null
* @throws IllegalArgumentException if {@code params} contains duplicate bindings for the same parameter
* @see Query#setParameter(Parameter, Object)
*/
QueryStream withParams(Iterable extends ParamBinding>> params);
/**
* Configure a load graph for this query.
*
*
* Equivalent to {@link #withHint withHint}{@code ("jakarta.persistence.loadgraph", name)}.
*
* @param name name of load graph
* @return new stream with the specified load graph configured
* @throws IllegalArgumentException if {@code name} is invalid
*/
QueryStream withLoadGraph(String name);
/**
* Configure a fetch graph for this query.
*
*
* Equivalent to {@link #withHint withHint}{@code ("jakarta.persistence.fetchgraph", name)}.
*
* @param name name of fetch graph
* @return new stream with the specified fetch graph configured
* @throws IllegalArgumentException if {@code name} is invalid
*/
QueryStream withFetchGraph(String name);
// Refs
/**
* Bind an unbound reference to the items in this stream.
*
* @param ref unbound reference
* @throws IllegalArgumentException if {@code ref} is already bound
* @throws IllegalArgumentException if {@code ref} is null
* @return new stream that binds {@code ref}
*/
QueryStream bind(Ref ref);
/**
* Bind an unbound reference to the result of applying the given function to the items in this stream.
*
* @param ref unbound reference
* @param refFunction function mapping this stream's {@link Selection} to the reference value
* @param type of the bound value
* @param criteria type of the bound value
* @throws IllegalArgumentException if {@code ref} is already bound
* @throws IllegalArgumentException if {@code ref} or {@code refFunction} is null
* @return new stream that binds {@code ref}
*/
> QueryStream bind(
Ref ref, Function super S, ? extends S2> refFunction);
// Peek
/**
* Peek at the items in this stream.
*
*
* This is useful in cases where the selection can be modified, e.g., setting join {@code ON} conditions
* using {@link Join#on Join.on()}.
*
* @param peeker peeker into stream
* @return new stream that peeks into this stream
* @throws IllegalArgumentException if {@code peeker} is null
*/
QueryStream peek(Consumer super S> peeker);
// Filtering
/**
* Filter results using the boolean expression produced by the given function.
*
*
* Adds to any previously specified filters.
*
* @param predicateBuilder function mapping this stream's item to a boolean {@link Expression}
* @return new filtered stream
* @throws IllegalArgumentException if {@code predicateBuilder} is null
*/
QueryStream filter(Function super S, ? extends Expression> predicateBuilder);
/**
* Filter results using the specified boolean property.
*
*
* Adds to any previously specified filters.
*
* @param attribute boolean property
* @return new filtered stream
* @throws IllegalArgumentException if {@code attribute} is null
*/
QueryStream filter(SingularAttribute super X, Boolean> attribute);
// Streamy stuff
/**
* Return this stream truncated to the specified maximum length.
*
*
* Due to limitations in the JPA Criteria API, this method is not supported on subquery streams
* and in general must be specified last (after any filtering, sorting, grouping, joins, etc.).
*
* @param maxSize maximum number of elements to return
* @return new truncated stream
* @throws IllegalArgumentException if {@code maxSize} is negative
*/
QueryStream limit(int maxSize);
/**
* Return this stream with the specified number of initial elements skipped.
*
*
* Due to limitations in the JPA Criteria API, this method is not supported on subquery streams
* and in general must be specified last (after any filtering, sorting, grouping, joins, etc.).
*
* @param num number of elements to skip
* @return new elided stream
* @throws IllegalArgumentException if {@code num} is negative
*/
QueryStream skip(int num);
// Builder
/**
* Create a {@link Builder} that constructs {@link QueryStream}s using the given {@link EntityManager}.
*
* @param entityManager entity manager
* @return new stream builder
* @throws IllegalArgumentException if {@code entityManager} is null
*/
static Builder newBuilder(EntityManager entityManager) {
return new Builder(entityManager);
}
/**
* Builder for {@link QueryStream}s, {@link DeleteStream}s, and {@link UpdateStream}s.
*
*
* New instances of this class are created via {@link QueryStream#newBuilder QueryStream.newBuilder()}.
*
*
* For convenience, this class also implements {@link CriteriaBuilder}.
*
*
* The primary stream creation methods are:
*
* - {@link #stream stream()} - Create a {@link SearchStream} for search queries.
* - {@link #deleteStream deleteStream()} - Create a {@link DeleteStream} for bulk delete queries.
* - {@link #updateStream updateStream()} - Create a {@link UpdateStream} for bulk update queries.
*
*
*
* The following methods create {@link SearchStream}s for use in correlated subqueries:
*
* - {@link #substream(Root)} - Create a correlated subquery {@link SearchStream} from a {@link Root}.
* - {@link #substream(Join)} - Create a correlated subquery {@link SearchStream} from a {@link Join}.
* - {@link #substream(SetJoin)} - Create a correlated subquery {@link SearchStream} from a {@link SetJoin}.
* - {@link #substream(MapJoin)} - Create a correlated subquery {@link SearchStream} from a {@link MapJoin}.
* - {@link #substream(ListJoin)} - Create a correlated subquery {@link SearchStream} from a {@link ListJoin}.
* - {@link #substream(CollectionJoin)}
* - Create a correlated subquery {@link SearchStream} from a {@link CollectionJoin}.
* - {@link #substream(From)}
* - Create a correlated subquery {@link SearchStream} from any {@link From} when a more specific type is unknown.
*
*
*
* See {@link #substream(Root)} for an example of using substreams.
*
*
* The following methods provide "convenience" access to objects that are not always readily available:
*
* - {@link #currentQuery} - Access the current Criteria API query under construction.
* - {@link #bindParam bindParam()} - Register a parameter binding with the current {@link Query} under construction.
* - {@link #getEntityManager} - Get the {@link EntityManager} associated with this instance.
*
*/
final class Builder extends ForwardingCriteriaBuilder {
private final EntityManager entityManager;
private final CriteriaBuilder criteriaBuilder;
private Builder(EntityManager entityManager) {
if (entityManager == null)
throw new IllegalArgumentException("null entityManager");
this.entityManager = entityManager;
this.criteriaBuilder = this.entityManager.getCriteriaBuilder();
}
/**
* Get the {@link EntityManager} associated with this instance.
*
* @return associated {@link EntityManager}
*/
public EntityManager getEntityManager() {
return this.entityManager;
}
/**
* Get the {@link CriteriaBuilder} associated with this instance.
*
* @return {@link CriteriaBuilder} created from this instance's {@link EntityManager}
*/
@Override
public CriteriaBuilder getCriteriaBuilder() {
return this.criteriaBuilder;
}
/**
* Create a {@link SearchStream} for search queries.
*
* @param type stream result type
* @param stream result type
* @return new search stream
* @throws IllegalArgumentException if {@code type} is null
*/
public RootStream stream(Class type) {
return new RootStreamImpl<>(this.entityManager, type);
}
/**
* Create a {@link SearchStream} for use as a subquery, using the specified correlated {@link Root}.
*
*
* The returned {@link RootStream} cannot be materialized directly via {@link RootStream#toQuery toQuery()}
* or {@link RootStream#toCriteriaQuery toCriteriaQuery()}; instead, it can only be used indirectly as a
* correlated subquery.
*
*
* Here's an example that returns the names of teachers who have one or more newly enrolled students:
*
* List<String> names = qb.stream(Teacher.class)
* .filter(teacher ->
* qb.substream(teacher)
* .map(Teacher_.students)
* .filter(Student_.newlyEnrolled)
* .exists()))
* .map(Teacher_.name)
* .getResultList();
*
*
* @param root correlated root for subquery
* @param stream result type
* @return new subquery search stream
* @throws IllegalArgumentException if {@code root} is null
*/
@SuppressWarnings("unchecked")
public RootStream substream(Root root) {
if (root == null)
throw new IllegalArgumentException("null root");
return new RootStreamImpl<>(this.entityManager, new SearchType((Class)root.getJavaType()),
(builder, query) -> QueryStreamImpl.getCurrentQuery().getSubquery().correlate(root), new QueryInfo());
}
/**
* Create a {@link SearchStream} for use as a subquery, using the specified {@link From}.
*
*
* This method inspects the type of {@code from} and then delegates to the {@code substream()} variant
* corresponding to whether {@code from} is really a {@link Root}, {@link SetJoin}, {@link MapJoin}, etc.
* You can use this method when you don't have more specific type information about {@code from}.
*
* @param from correlated join object for subquery
* @param source type
* @param target type
* @return new subquery search stream
* @throws IllegalArgumentException if {@code join} is null
* @see #substream(Root)
*/
// https://github.com/jakartaee/persistence/issues/190
@SuppressWarnings("unchecked")
public FromStream> substream(From from) {
if (from == null)
throw new IllegalArgumentException("null join");
if (from instanceof Root)
return (FromStream>)this.substream((Root)from);
if (from instanceof SetJoin)
return this.substream((SetJoin)from);
if (from instanceof ListJoin)
return this.substream((ListJoin)from);
if (from instanceof MapJoin)
return this.substream((MapJoin)from);
if (from instanceof CollectionJoin)
return this.substream((CollectionJoin)from);
if (from instanceof Join)
return this.substream((Join)from);
throw new UnsupportedOperationException("substream() from " + from.getClass().getName());
}
/**
* Create a {@link SearchStream} for use as a subquery, using the specified join.
*
* @param join correlated join object for subquery
* @param join origin type
* @param collection element type
* @return new subquery search stream
* @throws IllegalArgumentException if {@code join} is null
* @see #substream(Root)
*/
@SuppressWarnings("unchecked")
public FromStream> substream(CollectionJoin join) {
if (join == null)
throw new IllegalArgumentException("null join");
return new FromStreamImpl<>(this.entityManager, new SearchType((Class)join.getJavaType()),
(builder, query) -> QueryStreamImpl.getCurrentQuery().getSubquery().correlate(join), new QueryInfo());
}
/**
* Create a {@link SearchStream} for use as a subquery, using the specified join.
*
* @param join correlated join object for subquery
* @param join origin type
* @param list element type
* @return new subquery search stream
* @throws IllegalArgumentException if {@code join} is null
* @see #substream(Root)
*/
@SuppressWarnings("unchecked")
public FromStream> substream(ListJoin join) {
if (join == null)
throw new IllegalArgumentException("null join");
return new FromStreamImpl<>(this.entityManager, new SearchType((Class)join.getJavaType()),
(builder, query) -> QueryStreamImpl.getCurrentQuery().getSubquery().correlate(join), new QueryInfo());
}
/**
* Create a {@link SearchStream} for use as a subquery, using the specified join.
*
* @param join correlated join object for subquery
* @param join origin type
* @param map key type
* @param map value type
* @return new subquery search stream
* @throws IllegalArgumentException if {@code join} is null
* @see #substream(Root)
*/
@SuppressWarnings("unchecked")
public FromStream> substream(MapJoin join) {
if (join == null)
throw new IllegalArgumentException("null join");
return new FromStreamImpl<>(this.entityManager, new SearchType((Class)join.getJavaType()),
(builder, query) -> QueryStreamImpl.getCurrentQuery().getSubquery().correlate(join), new QueryInfo());
}
/**
* Create a {@link SearchStream} for use as a subquery, using the specified join.
*
* @param join correlated join object for subquery
* @param join origin type
* @param set element type
* @return new subquery search stream
* @throws IllegalArgumentException if {@code join} is null
* @see #substream(Root)
*/
@SuppressWarnings("unchecked")
public FromStream> substream(SetJoin join) {
if (join == null)
throw new IllegalArgumentException("null join");
return new FromStreamImpl<>(this.entityManager, new SearchType((Class)join.getJavaType()),
(builder, query) -> QueryStreamImpl.getCurrentQuery().getSubquery().correlate(join), new QueryInfo());
}
/**
* Create a {@link SearchStream} for use as a subquery, using the specified join.
*
* @param join correlated join object for subquery
* @param join origin type
* @param collection element type
* @return new subquery search stream
* @throws IllegalArgumentException if {@code join} is null
* @see #substream(Root)
*/
@SuppressWarnings("unchecked")
public FromStream> substream(Join join) {
if (join == null)
throw new IllegalArgumentException("null join");
return new FromStreamImpl<>(this.entityManager, new SearchType((Class)join.getJavaType()),
(builder, query) -> QueryStreamImpl.getCurrentQuery().getSubquery().correlate(join), new QueryInfo());
}
/**
* Create a {@link DeleteStream} for bulk delete queries.
*
* @param type stream target type
* @param stream target type
* @return new bulk delete stream
* @throws IllegalArgumentException if {@code type} is null
*/
public DeleteStream deleteStream(Class type) {
return new DeleteStreamImpl<>(this.entityManager, type);
}
/**
* Create a {@link UpdateStream} for bulk update queries.
*
* @param type stream target type
* @param stream target type
* @return new bulk update stream
* @throws IllegalArgumentException if {@code type} is null
*/
public UpdateStream updateStream(Class type) {
return new UpdateStreamImpl<>(this.entityManager, type);
}
/**
* Access the current Criteria API query under construction.
*
*
* This method provides a way to access the current {@link CriteriaQuery}, {@link CriteriaUpdate}, {@link CriteriaDelete},
* or {@link Subquery} currently being constructed.
*
*
* This is useful (for example) when implementing a {@link #filter(Function)} function using the traditional
* JPA Criteria API and you need to create a {@link Subquery}:
*
* List<String> names = qb.stream(Teacher.class)
* .filter(teacher -> {
* Subquery<Student> subquery = qb.currentQuery().subquery(Student.class);
* // configure Student subquery...
* return qb.exists(subquery);
* })
* .map(Teacher_.name)
* .getResultList(); // note: the query is actually constructed here
*
*
*
* This method does not work outside of the context of a query being constructed.
*
*
* In the case of nested {@linkplain #substream substream(s)}, then the inner-most query is returned:
*
* List<String> names = qb.stream(Teacher.class)
* .filter(teacher -> {
* // here qb.currentQuery() would return CriteriaQuery<Teacher>
* return qb.substream(teacher)
* .map(Teacher_.students)
* .filter(student -> {
* // here qb.currentQuery() returns CriteriaQuery<Student>
* Subquery<Test> subquery = qb.currentQuery().subquery(Test.class);
* // configure Test subquery...
* return qb.exists(subquery);
* })
* .exists();
* })
* .map(Teacher_.name)
* .getResultList();
* // here qb.currentQuery() will throw IllegalStateException
* qb.currentQuery(); // this will throw IllegalStateException
*
*
*
* The returned query object should not be modified.
*
* @return the current Criteria API query under construction
* @throws IllegalStateException if invoked outside of Criteria API query construction
*/
public CommonAbstractCriteria currentQuery() {
try {
return QueryStreamImpl.getCurrentQuery().getQuery();
} catch (IllegalStateException e) {
throw new IllegalStateException("there is no Criteria API query currently under construction");
}
}
/**
* Register a parameter binding with the current {@link Query} that is under construction.
*
*
* This method addresses an inconvenience in the JPA Criteria API, which is that parameters are (a) used (i.e., within
* some Criteria API expression) and (b) bound (i.e., assigned a value) at two separate stages of query construction:
* parameters are used in the context of building a Criteria API {@link Predicate}, but the value of the parameter
* can only be bound once the overall {@link Query} has been constructed. Often these two steps are implemented
* at different places in the code.
*
*
* This method allows the value of the parameter to be bound at the same time it is used. It simply remembers the
* parameter value until later when the {@link Query} is created and the value can then be actually assigned.
* However, this method only works for {@link Query}s created via QueryStream API query execution methods, e.g.,
* {@link QueryStream#toQuery}, {@link SearchStream#getResultList}, {@link DeleteStream#delete}, {@link SearchValue#value},
* etc.
*
*
* This example shows how parameters would usually be handled:
*
*
* // Create parameter and get parameterized value
* Date startDateCutoff = ...;
* Parameter<Date> startDateParam = qb.parameter(Date.class);
*
* // Build Query
* Query query = qb.stream(Employee.class)
* .filter(e -> qb.greaterThan(e.get(Employee_.startDate), startDateParam)) // parameter used here
* .map(Employee_.name)
* .toQuery();
*
* // Bind parameter value
* query.setParameter(paramRef.get(), startDateCutoff, TemporalType.DATE); // parameter bound here
*
* // Execute query
* return query.getResultStream();
*
*
* This example, which is functionally equivalent to the above, shows how {@link #bindParam bindParam()} allows
* performing all of the parameter handling in one place:
*
*
* return qb.stream(Employee.class)
* .filter(e -> {
* Date startDateCutoff = ...;
* Parameter<Date> startDateParam = qb.parameter(Date.class);
* qb.bindParam(new DateParamBinding(startDateParam, startDateCutoff, TemporalType.DATE));
* return qb.greaterThan(e.get(Employee_.startDate), param);
* })
* .map(Employee_.name)
* .getResultStream(); // note: the Query is actually constructed here
*
*
*
* If this method is invoked outside of the context of {@link Query} construction,
* an {@link IllegalStateException} is thrown.
*
* @param binding parameter binding
* @throws IllegalStateException if invoked outside of {@link QueryStream#toQuery} or other query execution method
* @throws IllegalArgumentException if {@code binding} is null
*/
public void bindParam(ParamBinding> binding) {
QueryStreamImpl.bindParam(binding, true);
}
}
}