Query provider methods

Annotate a DAO method with @QueryProvider to delegate the execution of the query to one of your own classes:

@Dao
public interface SensorDao {
  @QueryProvider(providerClass = FindSliceProvider.class, entityHelpers = SensorReading.class)
  PagingIterable<SensorReading> findSlice(int id, Integer month, Integer day);
}

/* Schema:
   CREATE TABLE sensor_reading(sensor_id int, month int, day int, value double,
       PRIMARY KEY (id, month, day)
       WITH CLUSTERING ORDER BY (month DESC, day DESC);
 */

Use this for requests that can’t be expressed as static query strings. For example, we want the month and day parameters above to be optional:

  • if both are present, we query for a particular day: WHERE id = ? AND month = ? AND day = ?
  • if day is null, we query for the whole month: WHERE id = ? AND month = ?
  • if month is also null, we query the whole partition: WHERE id = ?

We assume that you’ve already written a corresponding entity class:

@Entity
public class SensorReading {
  @PartitionKey private int id;
  @ClusteringColumn(1) private int month;
  @ClusteringColumn(2) private int day;
  private double value;
  // constructors, getters and setters omitted for conciseness
}

Provider class

@QueryProvider.providerClass() indicates which class to delegate to. The mapper will create one instance for each DAO instance.

This class must expose a constructor that is accessible from the DAO interface’s package.

The first constructor argument must always be MapperContext. This is a utility type that provides access to mapper- and DAO-level state. In particular, this is how you get hold of the session.

If @QueryProvider.entityHelpers() is specified, the constructor must take an additional EntityHelper argument for each provided entity class. We specified SensorReading.class so our argument types are (MapperContext, EntityHelper<SensorReading>).

An entity helper is a utility type generated by the mapper. One thing it can do is construct query templates (with the query builder). We want to retrieve entities so we use selectStart(), chain a first WHERE clause for the id (which is always present), and store the result in a field for later use:

public class FindSliceProvider {
  private final CqlSession session;
  private final EntityHelper<SensorReading> sensorReadingHelper;
  private final Select selectStart;

  public FindSliceProvider(
      MapperContext context, EntityHelper<SensorReading> sensorReadingHelper) {
    this.session = context.getSession();
    this.sensorReadingHelper = sensorReadingHelper;
    this.selectStart =
        sensorReadingHelper.selectStart().whereColumn("id").isEqualTo(bindMarker());
  }

  ... // (to be continued)

Provider method

@QueryProvider.providerMethod() indicates which method to invoke on the provider class. When it’s not specified (as is our case), it defaults to the same name as the DAO method.

The provider method must be accessible from the DAO interface’s package, and have the same parameters and return type as the DAO method.

Here is the full implementation:

  ... // public class FindSliceProvider (continued)

  public PagingIterable<SensorReading> findSlice(int id, Integer month, Integer day) {

    // (1) complete the query
    Select select = this.selectStart;
    if (month != null) {
      select = select.whereColumn("month").isEqualTo(bindMarker());
      if (day != null) {
        select = select.whereColumn("day").isEqualTo(bindMarker());
      }
    }

    // (2) prepare
    PreparedStatement preparedStatement = session.prepare(select.build());

    // (3) bind
    BoundStatementBuilder boundStatementBuilder =
        preparedStatement.boundStatementBuilder().setInt("id", id);
    if (month != null) {
      boundStatementBuilder = boundStatementBuilder.setInt("month", month);
      if (day != null) {
        boundStatementBuilder = boundStatementBuilder.setInt("day", day);
      }
    }

    // (4) execute and map the results
    return session.execute(boundStatementBuilder.build()).map(sensorReadingHelper::get);
  }
}
  1. Retrieve the SELECT query that was started in the constructor, and append additional WHERE clauses as appropriate.

    Note that all query builder objects are immutable, so this creates a new instance every time, there is no risk of corrupting the original field.

  2. Prepare the resulting statement.

    session.prepare caches its results, so if we already prepared that particular combination, there is no network call at this step.

  3. Bind the parameters, according to the WHERE clauses we’ve generated.

  4. Execute the request.

    Another useful helper feature is mapping entities to/from low-level driver data structures: get extracts a SensorReading from a Row, so by mapping it to the ResultSet we get back the desired PagingIterable.