Skip to content

Commit 2838543

Browse files
committed
Revisit Insert methods
- introduce separate ifNotExists() property - fix bug when return type is entity and conditional insert succeeds - allow Optional return types
1 parent 478535d commit 2838543

8 files changed

Lines changed: 89 additions & 24 deletions

File tree

integration-tests/src/test/java/com/datastax/oss/driver/mapper/InsertEntityIT.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424
import com.datastax.oss.driver.api.testinfra.ccm.CcmRule;
2525
import com.datastax.oss.driver.api.testinfra.session.SessionRule;
2626
import com.datastax.oss.driver.categories.ParallelizableTests;
27+
import com.datastax.oss.driver.mapper.model.inventory.Dimensions;
2728
import com.datastax.oss.driver.mapper.model.inventory.InventoryFixtures;
2829
import com.datastax.oss.driver.mapper.model.inventory.InventoryMapper;
2930
import com.datastax.oss.driver.mapper.model.inventory.InventoryMapperBuilder;
3031
import com.datastax.oss.driver.mapper.model.inventory.Product;
3132
import com.datastax.oss.driver.mapper.model.inventory.ProductDao;
33+
import java.util.Optional;
3234
import org.junit.Before;
3335
import org.junit.BeforeClass;
3436
import org.junit.ClassRule;
@@ -109,4 +111,24 @@ public void should_insert_entity_with_custom_clause() {
109111
// Then
110112
assertThat(writeTime).isEqualTo(timestamp);
111113
}
114+
115+
@Test
116+
public void should_insert_entity_if_not_exists() {
117+
// Given
118+
Product product = InventoryFixtures.FLAMETHROWER.entity;
119+
120+
// When
121+
Optional<Product> maybeExisting = productDao.saveIfNotExists(product);
122+
123+
// Then
124+
assertThat(maybeExisting).isEmpty();
125+
126+
// When
127+
Product otherProduct =
128+
new Product(product.getId(), "Other description", new Dimensions(1, 1, 1));
129+
maybeExisting = productDao.saveIfNotExists(otherProduct);
130+
131+
// Then
132+
assertThat(maybeExisting).contains(product);
133+
}
112134
}

integration-tests/src/test/java/com/datastax/oss/driver/mapper/model/inventory/ProductDao.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,11 @@ public interface ProductDao {
6363
@Insert
6464
void save(Product product);
6565

66-
@Insert(customClause = "USING TIMESTAMP :timestamp")
67-
Product saveWithBoundTimestamp(Product product, long timestamp);
66+
@Insert(customUsingClause = "USING TIMESTAMP :timestamp")
67+
void saveWithBoundTimestamp(Product product, long timestamp);
68+
69+
@Insert(ifNotExists = true)
70+
Optional<Product> saveIfNotExists(Product product);
6871

6972
@Select
7073
Product findById(UUID productId);

mapper-processor/src/main/java/com/datastax/oss/driver/internal/mapper/processor/dao/DaoInsertMethodGenerator.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717

1818
import static com.datastax.oss.driver.internal.mapper.processor.dao.ReturnTypeKind.ENTITY;
1919
import static com.datastax.oss.driver.internal.mapper.processor.dao.ReturnTypeKind.FUTURE_OF_ENTITY;
20+
import static com.datastax.oss.driver.internal.mapper.processor.dao.ReturnTypeKind.FUTURE_OF_OPTIONAL_ENTITY;
2021
import static com.datastax.oss.driver.internal.mapper.processor.dao.ReturnTypeKind.FUTURE_OF_VOID;
22+
import static com.datastax.oss.driver.internal.mapper.processor.dao.ReturnTypeKind.OPTIONAL_ENTITY;
2123
import static com.datastax.oss.driver.internal.mapper.processor.dao.ReturnTypeKind.VOID;
2224

2325
import com.datastax.oss.driver.api.core.cql.BoundStatement;
@@ -39,7 +41,13 @@
3941
public class DaoInsertMethodGenerator extends DaoMethodGenerator {
4042

4143
private static final EnumSet<ReturnTypeKind> SUPPORTED_RETURN_TYPES =
42-
EnumSet.of(VOID, FUTURE_OF_VOID, ENTITY, FUTURE_OF_ENTITY);
44+
EnumSet.of(
45+
VOID,
46+
FUTURE_OF_VOID,
47+
ENTITY,
48+
FUTURE_OF_ENTITY,
49+
OPTIONAL_ENTITY,
50+
FUTURE_OF_OPTIONAL_ENTITY);
4351

4452
public DaoInsertMethodGenerator(
4553
ExecutableElement methodElement,
@@ -145,13 +153,19 @@ public Optional<MethodSpec> generate() {
145153
private void generatePrepareRequest(
146154
MethodSpec.Builder methodBuilder, String requestName, String helperFieldName) {
147155
methodBuilder.addCode(
148-
"$[$1T $2L = $1T.newInstance($3L.insert().asCql()",
156+
"$[$1T $2L = $1T.newInstance($3L.insert()",
149157
SimpleStatement.class,
150158
requestName,
151159
helperFieldName);
152-
String customClause = methodElement.getAnnotation(Insert.class).customClause();
153-
if (!customClause.isEmpty()) {
154-
methodBuilder.addCode(" + $S", " " + customClause);
160+
Insert annotation = methodElement.getAnnotation(Insert.class);
161+
if (annotation.ifNotExists()) {
162+
methodBuilder.addCode(".ifNotExists()");
163+
}
164+
methodBuilder.addCode(".asCql()");
165+
166+
String customUsingClause = annotation.customUsingClause();
167+
if (!customUsingClause.isEmpty()) {
168+
methodBuilder.addCode(" + $S", " " + customUsingClause);
155169
}
156170
methodBuilder.addCode(")$];\n");
157171
}

mapper-processor/src/main/java/com/datastax/oss/driver/internal/mapper/processor/entity/EntityHelperInsertMethodGenerator.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
package com.datastax.oss.driver.internal.mapper.processor.entity;
1717

1818
import com.datastax.oss.driver.api.core.CqlIdentifier;
19-
import com.datastax.oss.driver.api.querybuilder.BuildableQuery;
2019
import com.datastax.oss.driver.api.querybuilder.QueryBuilder;
2120
import com.datastax.oss.driver.api.querybuilder.insert.InsertInto;
21+
import com.datastax.oss.driver.api.querybuilder.insert.RegularInsert;
2222
import com.datastax.oss.driver.internal.mapper.processor.MethodGenerator;
2323
import com.datastax.oss.driver.internal.mapper.processor.ProcessorContext;
2424
import com.squareup.javapoet.MethodSpec;
@@ -42,7 +42,7 @@ public Optional<MethodSpec> generate() {
4242
MethodSpec.methodBuilder("insert")
4343
.addAnnotation(Override.class)
4444
.addModifiers(Modifier.PUBLIC)
45-
.returns(BuildableQuery.class)
45+
.returns(RegularInsert.class)
4646
.addStatement("$T keyspaceId = context.getKeyspaceId()", CqlIdentifier.class)
4747
.addStatement("$T tableId = context.getTableId()", CqlIdentifier.class)
4848
.beginControlFlow("if (tableId == null)")

mapper-processor/src/test/java/com/datastax/oss/driver/internal/mapper/processor/dao/DaoInsertMethodGeneratorTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.tngtech.java.junit.dataprovider.DataProvider;
2525
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
2626
import com.tngtech.java.junit.dataprovider.UseDataProvider;
27+
import java.util.Optional;
2728
import java.util.concurrent.CompletableFuture;
2829
import java.util.concurrent.CompletionStage;
2930
import javax.lang.model.element.Modifier;
@@ -133,6 +134,26 @@ public static Object[][] validSignatures() {
133134
ClassName.get(CompletableFuture.class), ENTITY_CLASS_NAME))
134135
.build()
135136
},
137+
// Returns an optional of the entity class, or a future thereof:
138+
{
139+
MethodSpec.methodBuilder("insert")
140+
.addAnnotation(Insert.class)
141+
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
142+
.addParameter(ParameterSpec.builder(ENTITY_CLASS_NAME, "entity").build())
143+
.returns(ParameterizedTypeName.get(ClassName.get(Optional.class), ENTITY_CLASS_NAME))
144+
.build()
145+
},
146+
{
147+
MethodSpec.methodBuilder("insert")
148+
.addAnnotation(Insert.class)
149+
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
150+
.addParameter(ParameterSpec.builder(ENTITY_CLASS_NAME, "entity").build())
151+
.returns(
152+
ParameterizedTypeName.get(
153+
ClassName.get(CompletionStage.class),
154+
ParameterizedTypeName.get(ClassName.get(Optional.class), ENTITY_CLASS_NAME)))
155+
.build()
156+
},
136157
// Extra parameters in addition to the entity (to bind into the request):
137158
{
138159
MethodSpec.methodBuilder("insert")

mapper-runtime/src/main/java/com/datastax/oss/driver/api/mapper/annotations/Insert.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,7 @@
2323
@Target(ElementType.METHOD)
2424
@Retention(RetentionPolicy.CLASS)
2525
public @interface Insert {
26-
String customClause() default "";
26+
boolean ifNotExists() default false;
27+
28+
String customUsingClause() default "";
2729
}

mapper-runtime/src/main/java/com/datastax/oss/driver/api/mapper/entity/EntityHelper.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.datastax.oss.driver.api.mapper.annotations.PartitionKey;
2727
import com.datastax.oss.driver.api.querybuilder.BuildableQuery;
2828
import com.datastax.oss.driver.api.querybuilder.delete.Delete;
29+
import com.datastax.oss.driver.api.querybuilder.insert.RegularInsert;
2930
import com.datastax.oss.driver.api.querybuilder.select.Select;
3031

3132
/**
@@ -104,7 +105,7 @@ public interface EntityHelper<EntityT> {
104105
* if the DAO was built without a specific keyspace and table, the query doesn't specify a
105106
* keyspace, and the table name is inferred from the naming strategy.
106107
*/
107-
BuildableQuery insert();
108+
RegularInsert insert();
108109

109110
/**
110111
* Builds a select query to fetch an instance of the entity by primary key (partition key +

mapper-runtime/src/main/java/com/datastax/oss/driver/internal/mapper/DaoBase.java

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public class DaoBase {
4646
/** The qualified table id placeholder in {@link Query#value()}. */
4747
public static final String QUALIFIED_TABLE_ID_PLACEHOLDER = "${qualifiedTableId}";
4848

49+
private static final CqlIdentifier APPLIED = CqlIdentifier.fromInternal("[applied]");
50+
4951
protected static CompletionStage<PreparedStatement> prepare(
5052
SimpleStatement statement, MapperContext context) {
5153
if (statement == null) {
@@ -153,8 +155,17 @@ protected Row executeAndExtractFirstRow(Statement<?> statement) {
153155
protected <EntityT> EntityT executeAndMapToSingleEntity(
154156
Statement<?> statement, EntityHelper<EntityT> entityHelper) {
155157
ResultSet rs = execute(statement);
156-
Row row = rs.one();
157-
return (row == null) ? null : entityHelper.get(row);
158+
return asEntity(rs.one(), entityHelper);
159+
}
160+
161+
private <EntityT> EntityT asEntity(Row row, EntityHelper<EntityT> entityHelper) {
162+
return (row == null
163+
// Special case for INSERT IF NOT EXISTS. If the row did not exists, the query returns
164+
// only [applied], we want to return null to indicate there was no previous entity
165+
|| (row.getColumnDefinitions().size() == 1
166+
&& row.getColumnDefinitions().get(0).getName().equals(APPLIED)))
167+
? null
168+
: entityHelper.get(row);
158169
}
159170

160171
protected <EntityT> Optional<EntityT> executeAndMapToOptionalEntity(
@@ -199,22 +210,13 @@ protected CompletableFuture<Row> executeAsyncAndExtractFirstRow(Statement<?> sta
199210

200211
protected <EntityT> CompletableFuture<EntityT> executeAsyncAndMapToSingleEntity(
201212
Statement<?> statement, EntityHelper<EntityT> entityHelper) {
202-
return executeAsync(statement)
203-
.thenApply(
204-
rs -> {
205-
Row row = rs.one();
206-
return (row == null) ? null : entityHelper.get(row);
207-
});
213+
return executeAsync(statement).thenApply(rs -> asEntity(rs.one(), entityHelper));
208214
}
209215

210216
protected <EntityT> CompletableFuture<Optional<EntityT>> executeAsyncAndMapToOptionalEntity(
211217
Statement<?> statement, EntityHelper<EntityT> entityHelper) {
212218
return executeAsync(statement)
213-
.thenApply(
214-
rs -> {
215-
Row row = rs.one();
216-
return (row == null) ? Optional.empty() : Optional.of(entityHelper.get(row));
217-
});
219+
.thenApply(rs -> Optional.ofNullable(asEntity(rs.one(), entityHelper)));
218220
}
219221

220222
protected <EntityT>

0 commit comments

Comments
 (0)