org.dellroad.querystream.jpa.QueryStream Maven / Gradle / Ivy
/*
* Copyright (C) 2018 Archie L. Cobbs. All rights reserved.
*/
package org.dellroad.querystream.jpa;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.persistence.EntityManager;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.Query;
import javax.persistence.criteria.CollectionJoin;
import javax.persistence.criteria.CommonAbstractCriteria;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.ListJoin;
import javax.persistence.criteria.MapJoin;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Selection;
import javax.persistence.criteria.SetJoin;
import javax.persistence.metamodel.SingularAttribute;
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 javax.persistence.criteria.CriteriaQuery} object (for example,
* {@linkplain #getLockMode lock mode}, {@linkplain #withHint hints}, row {@linkplain #getFirstResult offset} and
* {@linkplain #getMaxResults limit}, etc.); such 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 configured
* @see Query#setHint
* @throws IllegalArgumentException if {@code hints} is null
*/
QueryStream withHints(Map hints);
/**
* Configure a load graph for this query.
*
*
* Equivalent to {@link #withHint withHint}{@code ("javax.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 ("javax.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} and related classes.
*
*
* Instances are created via {@link QueryStream#newBuilder QueryStream.newBuilder()}.
*
*
* For convenience, this class also implements {@link CriteriaBuilder}.
* The primary methods in this class (i.e., not inherited from {@link CriteriaBuilder}) 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.
* - {@link #getEntityManager} - Get the {@link EntityManager} associated with this instance.
*
*
*
* In addition, 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}.
*
*
*
* See {@link #substream(Root) substream()} for an example of using substreams.
*/
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.getSubqueryInfo().getSubquery().correlate(root), 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(CollectionJoin join) {
if (join == null)
throw new IllegalArgumentException("null join");
return new FromStreamImpl<>(this.entityManager, new SearchType((Class)join.getJavaType()),
(builder, query) -> QueryStreamImpl.getSubqueryInfo().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.getSubqueryInfo().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.getSubqueryInfo().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.getSubqueryInfo().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.getSubqueryInfo().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);
}
}
}