Skip to content

Commit 1e8a11d

Browse files
committed
JAVA-2118: Generate QueryProvider DAO methods
1 parent cdc883c commit 1e8a11d

9 files changed

Lines changed: 749 additions & 0 deletions

File tree

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/*
2+
* Copyright DataStax, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.datastax.oss.driver.mapper;
17+
18+
import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker;
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
import com.datastax.oss.driver.api.core.CqlIdentifier;
22+
import com.datastax.oss.driver.api.core.CqlSession;
23+
import com.datastax.oss.driver.api.core.PagingIterable;
24+
import com.datastax.oss.driver.api.core.cql.BoundStatementBuilder;
25+
import com.datastax.oss.driver.api.core.cql.PreparedStatement;
26+
import com.datastax.oss.driver.api.core.cql.SimpleStatement;
27+
import com.datastax.oss.driver.api.mapper.MapperContext;
28+
import com.datastax.oss.driver.api.mapper.annotations.ClusteringColumn;
29+
import com.datastax.oss.driver.api.mapper.annotations.Dao;
30+
import com.datastax.oss.driver.api.mapper.annotations.DaoFactory;
31+
import com.datastax.oss.driver.api.mapper.annotations.DaoKeyspace;
32+
import com.datastax.oss.driver.api.mapper.annotations.Entity;
33+
import com.datastax.oss.driver.api.mapper.annotations.Insert;
34+
import com.datastax.oss.driver.api.mapper.annotations.Mapper;
35+
import com.datastax.oss.driver.api.mapper.annotations.PartitionKey;
36+
import com.datastax.oss.driver.api.mapper.annotations.QueryProvider;
37+
import com.datastax.oss.driver.api.mapper.entity.EntityHelper;
38+
import com.datastax.oss.driver.api.querybuilder.select.Select;
39+
import com.datastax.oss.driver.api.testinfra.ccm.CcmRule;
40+
import com.datastax.oss.driver.api.testinfra.session.SessionRule;
41+
import com.datastax.oss.driver.categories.ParallelizableTests;
42+
import java.util.Objects;
43+
import org.junit.BeforeClass;
44+
import org.junit.ClassRule;
45+
import org.junit.Test;
46+
import org.junit.experimental.categories.Category;
47+
import org.junit.rules.RuleChain;
48+
import org.junit.rules.TestRule;
49+
50+
@Category(ParallelizableTests.class)
51+
public class QueryProviderIT {
52+
53+
private static CcmRule ccm = CcmRule.getInstance();
54+
private static SessionRule<CqlSession> sessionRule = SessionRule.builder(ccm).build();
55+
@ClassRule public static TestRule chain = RuleChain.outerRule(ccm).around(sessionRule);
56+
57+
private static SensorDao dao;
58+
59+
@BeforeClass
60+
public static void setup() {
61+
CqlSession session = sessionRule.session();
62+
63+
session.execute(
64+
SimpleStatement.builder(
65+
"CREATE TABLE sensor_reading(id int, month int, day int, value double, "
66+
+ "PRIMARY KEY (id, month, day)) "
67+
+ "WITH CLUSTERING ORDER BY (month DESC, day DESC)")
68+
.setExecutionProfile(sessionRule.slowProfile())
69+
.build());
70+
71+
SensorMapper mapper = new QueryProviderIT_SensorMapperBuilder(session).build();
72+
dao = mapper.sensorDao(sessionRule.keyspace());
73+
}
74+
75+
@Test
76+
public void should_invoke_query_provider() {
77+
SensorReading readingFeb3 = new SensorReading(1, 2, 3, 9.3);
78+
SensorReading readingFeb2 = new SensorReading(1, 2, 2, 8.6);
79+
SensorReading readingFeb1 = new SensorReading(1, 2, 1, 8.7);
80+
SensorReading readingJan31 = new SensorReading(1, 1, 31, 8.2);
81+
dao.save(readingFeb3);
82+
dao.save(readingFeb2);
83+
dao.save(readingFeb1);
84+
dao.save(readingJan31);
85+
86+
assertThat(dao.findSlice(1, null, null).all())
87+
.containsExactly(readingFeb3, readingFeb2, readingFeb1, readingJan31);
88+
assertThat(dao.findSlice(1, 2, null).all())
89+
.containsExactly(readingFeb3, readingFeb2, readingFeb1);
90+
assertThat(dao.findSlice(1, 2, 3).all()).containsExactly(readingFeb3);
91+
}
92+
93+
@Mapper
94+
public interface SensorMapper {
95+
@DaoFactory
96+
SensorDao sensorDao(@DaoKeyspace CqlIdentifier keyspace);
97+
}
98+
99+
@Dao
100+
public interface SensorDao {
101+
@QueryProvider(providerClass = FindSliceProvider.class, entityHelpers = SensorReading.class)
102+
PagingIterable<SensorReading> findSlice(int id, Integer month, Integer day);
103+
104+
@Insert
105+
void save(SensorReading reading);
106+
}
107+
108+
public static class FindSliceProvider {
109+
private final CqlSession session;
110+
private final EntityHelper<SensorReading> sensorReadingHelper;
111+
private final Select selectStart;
112+
113+
public FindSliceProvider(
114+
MapperContext context, EntityHelper<SensorReading> sensorReadingHelper) {
115+
this.session = context.getSession();
116+
this.sensorReadingHelper = sensorReadingHelper;
117+
this.selectStart =
118+
sensorReadingHelper.selectStart().whereColumn("id").isEqualTo(bindMarker());
119+
}
120+
121+
public PagingIterable<SensorReading> findSlice(int id, Integer month, Integer day) {
122+
if (month == null && day != null) {
123+
throw new IllegalArgumentException("Can't specify day if month is null");
124+
}
125+
126+
Select select = this.selectStart;
127+
if (month != null) {
128+
select = select.whereColumn("month").isEqualTo(bindMarker());
129+
if (day != null) {
130+
select = select.whereColumn("day").isEqualTo(bindMarker());
131+
}
132+
}
133+
PreparedStatement preparedStatement = session.prepare(select.build());
134+
BoundStatementBuilder boundStatementBuilder =
135+
preparedStatement.boundStatementBuilder().setInt("id", id);
136+
if (month != null) {
137+
boundStatementBuilder = boundStatementBuilder.setInt("month", month);
138+
if (day != null) {
139+
boundStatementBuilder = boundStatementBuilder.setInt("day", day);
140+
}
141+
}
142+
return session.execute(boundStatementBuilder.build()).map(sensorReadingHelper::get);
143+
}
144+
}
145+
146+
@Entity
147+
public static class SensorReading {
148+
@PartitionKey private int id;
149+
150+
@ClusteringColumn(1)
151+
private int month;
152+
153+
@ClusteringColumn(2)
154+
private int day;
155+
156+
private double value;
157+
158+
public SensorReading() {}
159+
160+
public SensorReading(int id, int month, int day, double value) {
161+
this.id = id;
162+
this.month = month;
163+
this.day = day;
164+
this.value = value;
165+
}
166+
167+
public int getId() {
168+
return id;
169+
}
170+
171+
public void setId(int id) {
172+
this.id = id;
173+
}
174+
175+
public int getMonth() {
176+
return month;
177+
}
178+
179+
public void setMonth(int month) {
180+
this.month = month;
181+
}
182+
183+
public int getDay() {
184+
return day;
185+
}
186+
187+
public void setDay(int day) {
188+
this.day = day;
189+
}
190+
191+
public double getValue() {
192+
return value;
193+
}
194+
195+
public void setValue(double value) {
196+
this.value = value;
197+
}
198+
199+
@Override
200+
public boolean equals(Object other) {
201+
if (other == this) {
202+
return true;
203+
} else if (other instanceof SensorReading) {
204+
SensorReading that = (SensorReading) other;
205+
return this.id == that.id
206+
&& this.month == that.month
207+
&& this.day == that.day
208+
&& this.value == that.value;
209+
} else {
210+
return false;
211+
}
212+
}
213+
214+
@Override
215+
public int hashCode() {
216+
return Objects.hash(id, month, day, value);
217+
}
218+
219+
@Override
220+
public String toString() {
221+
return String.format("%d %d/%d %f", id, month, day, value);
222+
}
223+
}
224+
}

manual/mapper/daos/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ annotations:
2828
* [@GetEntity](getentity/)
2929
* [@Insert](insert/)
3030
* [@Query](query/)
31+
* [@QueryProvider](queryprovider/)
3132
* [@Select](select/)
3233
* [@SetEntity](setentity/)
3334
* [@Update](update/)
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
## Query provider methods
2+
3+
Annotate a DAO method with [@QueryProvider] to delegate the execution of the query to one of your
4+
own classes:
5+
6+
```java
7+
@Dao
8+
public interface SensorDao {
9+
@QueryProvider(providerClass = FindSliceProvider.class, entityHelpers = SensorReading.class)
10+
PagingIterable<SensorReading> findSlice(int id, Integer month, Integer day);
11+
}
12+
13+
/* Schema:
14+
CREATE TABLE sensor_reading(sensor_id int, month int, day int, value double,
15+
PRIMARY KEY (id, month, day)
16+
WITH CLUSTERING ORDER BY (month DESC, day DESC);
17+
*/
18+
```
19+
20+
Use this for requests that can't be expressed as static query strings. For example, we want the
21+
`month` and `day` parameters above to be optional:
22+
23+
* if both are present, we query for a particular day: `WHERE id = ? AND month = ? AND day = ?`
24+
* if `day` is null, we query for the whole month: `WHERE id = ? AND month = ?`
25+
* if `month` is also null, we query the whole partition: `WHERE id = ?`
26+
27+
We assume that you've already written a corresponding [entity](../../entities/) class:
28+
29+
```java
30+
@Entity
31+
public class SensorReading {
32+
@PartitionKey private int id;
33+
@ClusteringColumn(1) private int month;
34+
@ClusteringColumn(2) private int day;
35+
private double value;
36+
// constructors, getters and setters omitted for conciseness
37+
}
38+
```
39+
40+
### Provider class
41+
42+
[@QueryProvider.providerClass()][providerClass] indicates which class to delegate to. The mapper
43+
will create one instance for each DAO instance.
44+
45+
This class must expose a constructor that is accessible from the DAO interface's package.
46+
47+
The first constructor argument must always be [MapperContext]. This is a utility type that
48+
provides access to mapper- and DAO-level state. In particular, this is how you get hold of the
49+
session.
50+
51+
If [@QueryProvider.entityHelpers()][entityHelpers] is specified, the constructor must take an
52+
additional [EntityHelper] argument for each provided entity class. We specified
53+
`SensorReading.class` so our argument types are `(MapperContext, EntityHelper<SensorReading>)`.
54+
55+
An entity helper is a utility type generated by the mapper. One thing it can do is construct query
56+
templates (with the [query builder](../../../query_builder/)). We want to retrieve entities so we
57+
use `selectStart()`, chain a first WHERE clause for the id (which is always present), and store the
58+
result in a field for later use:
59+
60+
```java
61+
public class FindSliceProvider {
62+
private final CqlSession session;
63+
private final EntityHelper<SensorReading> sensorReadingHelper;
64+
private final Select selectStart;
65+
66+
public FindSliceProvider(
67+
MapperContext context, EntityHelper<SensorReading> sensorReadingHelper) {
68+
this.session = context.getSession();
69+
this.sensorReadingHelper = sensorReadingHelper;
70+
this.selectStart =
71+
sensorReadingHelper.selectStart().whereColumn("id").isEqualTo(bindMarker());
72+
}
73+
74+
... // (to be continued)
75+
```
76+
77+
### Provider method
78+
79+
[@QueryProvider.providerMethod()][providerMethod] indicates which method to invoke on the provider
80+
class. When it's not specified (as is our case), it defaults to the same name as the DAO method.
81+
82+
The provider method must be accessible from the DAO interface's package, and have the same
83+
parameters and return type as the DAO method.
84+
85+
Here is the full implementation:
86+
87+
```java
88+
... // public class FindSliceProvider (continued)
89+
90+
public PagingIterable<SensorReading> findSlice(int id, Integer month, Integer day) {
91+
92+
// (1) complete the query
93+
Select select = this.selectStart;
94+
if (month != null) {
95+
select = select.whereColumn("month").isEqualTo(bindMarker());
96+
if (day != null) {
97+
select = select.whereColumn("day").isEqualTo(bindMarker());
98+
}
99+
}
100+
101+
// (2) prepare
102+
PreparedStatement preparedStatement = session.prepare(select.build());
103+
104+
// (3) bind
105+
BoundStatementBuilder boundStatementBuilder =
106+
preparedStatement.boundStatementBuilder().setInt("id", id);
107+
if (month != null) {
108+
boundStatementBuilder = boundStatementBuilder.setInt("month", month);
109+
if (day != null) {
110+
boundStatementBuilder = boundStatementBuilder.setInt("day", day);
111+
}
112+
}
113+
114+
// (4) execute and map the results
115+
return session.execute(boundStatementBuilder.build()).map(sensorReadingHelper::get);
116+
}
117+
}
118+
```
119+
120+
1. Retrieve the SELECT query that was started in the constructor, and append additional WHERE
121+
clauses as appropriate.
122+
123+
Note that all query builder objects are immutable, so this creates a new instance every time,
124+
there is no risk of corrupting the original field.
125+
126+
2. Prepare the resulting statement.
127+
128+
`session.prepare` caches its results, so if we already prepared that particular combination,
129+
there is no network call at this step.
130+
131+
3. Bind the parameters, according to the WHERE clauses we've generated.
132+
133+
4. Execute the request.
134+
135+
Another useful helper feature is mapping entities to/from low-level driver data structures:
136+
`get` extracts a `SensorReading` from a `Row`, so by mapping it to the [ResultSet] we get back
137+
the desired [PagingIterable<SensorReading>][PagingIterable].
138+
139+
140+
[@QueryProvider]: https://docs.datastax.com/en/drivers/java/4.0/com/datastax/oss/driver/api/mapper/annotations/QueryProvider.html
141+
[providerClass]: https://docs.datastax.com/en/drivers/java/4.0/com/datastax/oss/driver/api/mapper/annotations/QueryProvider.html#providerClass--
142+
[entityHelpers]: https://docs.datastax.com/en/drivers/java/4.0/com/datastax/oss/driver/api/mapper/annotations/QueryProvider.html#entityHelpers--
143+
[providerMethod]: https://docs.datastax.com/en/drivers/java/4.0/com/datastax/oss/driver/api/mapper/annotations/QueryProvider.html#providerMethod--
144+
[MapperContext]: https://docs.datastax.com/en/drivers/java/4.0/com/datastax/oss/driver/api/mapper/MapperContext.html
145+
[EntityHelper]: https://docs.datastax.com/en/drivers/java/4.0/com/datastax/oss/driver/api/mapper/EntityHelper.html
146+
[ResultSet]: https://docs.datastax.com/en/drivers/java/4.0/com/datastax/oss/driver/api/core/cql/ResultSet.html
147+
[PagingIterable]: https://docs.datastax.com/en/drivers/java/4.0/com/datastax/oss/driver/api/core/PagingIterable.html

0 commit comments

Comments
 (0)