All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dellroad.querystream.jpa.QueryStream Maven / Gradle / Ivy

The newest version!

/*
 * 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> 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 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 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> 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 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); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy