Browse Source

Introduced pessimistic locks for derived queries.

Methods which use the derive query functionality now can be annotated with `@Lock` to used a given `LockMode`. Right now there are two different modes `PESSIMISTIC_READ` and `PESSIMISTIC_WRITE`. Based on the dialect the right select is generated. For example for H2 `Select ... FOR UPDATE`.

Closes spring-projects/spring-data-jdbc#1041
See #643,
Original pull request spring-projects/spring-data-jdbc/pull/1158
pull/1188/head
Diego Krupitza 4 years ago committed by Jens Schauder
parent
commit
910c34014d
No known key found for this signature in database
GPG Key ID: 45CC872F17423DBF
  1. 5
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultStatementMapper.java
  2. 58
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/StatementMapper.java
  3. 16
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/dialect/H2Dialect.java
  4. 3
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/PartTreeR2dbcQuery.java
  5. 31
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryCreator.java
  6. 33
      spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java
  7. 24
      spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/StatementMapperUnitTests.java
  8. 36
      spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java
  9. 44
      spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/query/PartTreeR2dbcQueryUnitTests.java
  10. 35
      spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethodUnitTests.java

5
spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultStatementMapper.java

@ -45,6 +45,7 @@ import org.springframework.util.Assert;
* @author Mark Paluch * @author Mark Paluch
* @author Roman Chigvintsev * @author Roman Chigvintsev
* @author Mingyuan Wu * @author Mingyuan Wu
* @author Diego Krupitza
*/ */
class DefaultStatementMapper implements StatementMapper { class DefaultStatementMapper implements StatementMapper {
@ -132,6 +133,10 @@ class DefaultStatementMapper implements StatementMapper {
selectBuilder.offset(selectSpec.getOffset()); selectBuilder.offset(selectSpec.getOffset());
} }
if (selectSpec.getLock() != null) {
selectBuilder.lock(selectSpec.getLock());
}
Select select = selectBuilder.build(); Select select = selectBuilder.build();
return new DefaultPreparedOperation<>(select, this.renderContext, bindings); return new DefaultPreparedOperation<>(select, this.renderContext, bindings);
} }

58
spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/StatementMapper.java

@ -15,13 +15,7 @@
*/ */
package org.springframework.data.r2dbc.core; package org.springframework.data.r2dbc.core;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -33,6 +27,7 @@ import org.springframework.data.r2dbc.dialect.R2dbcDialect;
import org.springframework.data.relational.core.query.Criteria; import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.query.CriteriaDefinition; import org.springframework.data.relational.core.query.CriteriaDefinition;
import org.springframework.data.relational.core.sql.Expression; import org.springframework.data.relational.core.sql.Expression;
import org.springframework.data.relational.core.sql.LockMode;
import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.relational.core.sql.Table; import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.core.sql.render.RenderContext; import org.springframework.data.relational.core.sql.render.RenderContext;
@ -53,6 +48,7 @@ import org.springframework.util.Assert;
* @author Mark Paluch * @author Mark Paluch
* @author Roman Chigvintsev * @author Roman Chigvintsev
* @author Mingyuan Wu * @author Mingyuan Wu
* @author Diego Krupitza
*/ */
public interface StatementMapper { public interface StatementMapper {
@ -227,9 +223,10 @@ public interface StatementMapper {
private final long offset; private final long offset;
private final int limit; private final int limit;
private final boolean distinct; private final boolean distinct;
private final LockMode lockMode;
protected SelectSpec(Table table, List<String> projectedFields, List<Expression> selectList, protected SelectSpec(Table table, List<String> projectedFields, List<Expression> selectList,
@Nullable CriteriaDefinition criteria, Sort sort, int limit, long offset, boolean distinct) { @Nullable CriteriaDefinition criteria, Sort sort, int limit, long offset, boolean distinct, LockMode lockMode) {
this.table = table; this.table = table;
this.projectedFields = projectedFields; this.projectedFields = projectedFields;
this.selectList = selectList; this.selectList = selectList;
@ -238,6 +235,7 @@ public interface StatementMapper {
this.offset = offset; this.offset = offset;
this.limit = limit; this.limit = limit;
this.distinct = distinct; this.distinct = distinct;
this.lockMode = lockMode;
} }
/** /**
@ -262,7 +260,7 @@ public interface StatementMapper {
List<String> projectedFields = Collections.emptyList(); List<String> projectedFields = Collections.emptyList();
List<Expression> selectList = Collections.emptyList(); List<Expression> selectList = Collections.emptyList();
return new SelectSpec(Table.create(table), projectedFields, selectList, Criteria.empty(), Sort.unsorted(), -1, -1, return new SelectSpec(Table.create(table), projectedFields, selectList, Criteria.empty(), Sort.unsorted(), -1, -1,
false); false, null);
} }
public SelectSpec doWithTable(BiFunction<Table, SelectSpec, SelectSpec> function) { public SelectSpec doWithTable(BiFunction<Table, SelectSpec, SelectSpec> function) {
@ -304,7 +302,7 @@ public interface StatementMapper {
selectList.addAll(Arrays.asList(expressions)); selectList.addAll(Arrays.asList(expressions));
return new SelectSpec(this.table, projectedFields, selectList, this.criteria, this.sort, this.limit, this.offset, return new SelectSpec(this.table, projectedFields, selectList, this.criteria, this.sort, this.limit, this.offset,
this.distinct); this.distinct, this.lockMode);
} }
/** /**
@ -320,7 +318,7 @@ public interface StatementMapper {
selectList.addAll(projectedFields); selectList.addAll(projectedFields);
return new SelectSpec(this.table, this.projectedFields, selectList, this.criteria, this.sort, this.limit, return new SelectSpec(this.table, this.projectedFields, selectList, this.criteria, this.sort, this.limit,
this.offset, this.distinct); this.offset, this.distinct, this.lockMode);
} }
/** /**
@ -331,7 +329,7 @@ public interface StatementMapper {
*/ */
public SelectSpec withCriteria(CriteriaDefinition criteria) { public SelectSpec withCriteria(CriteriaDefinition criteria) {
return new SelectSpec(this.table, this.projectedFields, this.selectList, criteria, this.sort, this.limit, return new SelectSpec(this.table, this.projectedFields, this.selectList, criteria, this.sort, this.limit,
this.offset, this.distinct); this.offset, this.distinct, this.lockMode);
} }
/** /**
@ -344,11 +342,11 @@ public interface StatementMapper {
if (sort.isSorted()) { if (sort.isSorted()) {
return new SelectSpec(this.table, this.projectedFields, this.selectList, this.criteria, sort, this.limit, return new SelectSpec(this.table, this.projectedFields, this.selectList, this.criteria, sort, this.limit,
this.offset, this.distinct); this.offset, this.distinct, this.lockMode);
} }
return new SelectSpec(this.table, this.projectedFields, this.selectList, this.criteria, this.sort, this.limit, return new SelectSpec(this.table, this.projectedFields, this.selectList, this.criteria, this.sort, this.limit,
this.offset, this.distinct); this.offset, this.distinct, this.lockMode);
} }
/** /**
@ -364,11 +362,11 @@ public interface StatementMapper {
Sort sort = page.getSort(); Sort sort = page.getSort();
return new SelectSpec(this.table, this.projectedFields, this.selectList, this.criteria, return new SelectSpec(this.table, this.projectedFields, this.selectList, this.criteria,
sort.isSorted() ? sort : this.sort, page.getPageSize(), page.getOffset(), this.distinct); sort.isSorted() ? sort : this.sort, page.getPageSize(), page.getOffset(), this.distinct, this.lockMode);
} }
return new SelectSpec(this.table, this.projectedFields, this.selectList, this.criteria, this.sort, this.limit, return new SelectSpec(this.table, this.projectedFields, this.selectList, this.criteria, this.sort, this.limit,
this.offset, this.distinct); this.offset, this.distinct, this.lockMode);
} }
/** /**
@ -379,7 +377,7 @@ public interface StatementMapper {
*/ */
public SelectSpec offset(long offset) { public SelectSpec offset(long offset) {
return new SelectSpec(this.table, this.projectedFields, this.selectList, this.criteria, this.sort, this.limit, return new SelectSpec(this.table, this.projectedFields, this.selectList, this.criteria, this.sort, this.limit,
offset, this.distinct); offset, this.distinct, this.lockMode);
} }
/** /**
@ -390,7 +388,7 @@ public interface StatementMapper {
*/ */
public SelectSpec limit(int limit) { public SelectSpec limit(int limit) {
return new SelectSpec(this.table, this.projectedFields, this.selectList, this.criteria, this.sort, limit, return new SelectSpec(this.table, this.projectedFields, this.selectList, this.criteria, this.sort, limit,
this.offset, this.distinct); this.offset, this.distinct, this.lockMode);
} }
/** /**
@ -400,7 +398,28 @@ public interface StatementMapper {
*/ */
public SelectSpec distinct() { public SelectSpec distinct() {
return new SelectSpec(this.table, this.projectedFields, this.selectList, this.criteria, this.sort, limit, return new SelectSpec(this.table, this.projectedFields, this.selectList, this.criteria, this.sort, limit,
this.offset, true); this.offset, true, this.lockMode);
}
/**
* Associate a lock mode with the select and create a new {@link SelectSpec}.
*
* @param lockMode the {@link LockMode} we want to use. This might be null
* @return the {@link SelectSpec}.
*/
public SelectSpec lock(LockMode lockMode) {
return new SelectSpec(this.table, this.projectedFields, this.selectList, this.criteria, this.sort, limit,
this.offset, this.distinct, lockMode);
}
/**
* The used lockmode
*
* @return might be null if no lockmode defined.
*/
@Nullable
public LockMode getLock() {
return this.lockMode;
} }
public Table getTable() { public Table getTable() {
@ -440,6 +459,7 @@ public interface StatementMapper {
public boolean isDistinct() { public boolean isDistinct() {
return this.distinct; return this.distinct;
} }
} }
/** /**

16
spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/dialect/H2Dialect.java

@ -1,5 +1,7 @@
package org.springframework.data.r2dbc.dialect; package org.springframework.data.r2dbc.dialect;
import org.springframework.data.relational.core.dialect.AnsiDialect;
import org.springframework.data.relational.core.dialect.LockClause;
import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.relational.core.sql.SqlIdentifier;
/** /**
@ -7,6 +9,7 @@ import org.springframework.data.relational.core.sql.SqlIdentifier;
* *
* @author Mark Paluch * @author Mark Paluch
* @author Jens Schauder * @author Jens Schauder
* @author Diego Krupitza
*/ */
public class H2Dialect extends PostgresDialect { public class H2Dialect extends PostgresDialect {
@ -19,4 +22,17 @@ public class H2Dialect extends PostgresDialect {
public String renderForGeneratedValues(SqlIdentifier identifier) { public String renderForGeneratedValues(SqlIdentifier identifier) {
return identifier.getReference(getIdentifierProcessing()); return identifier.getReference(getIdentifierProcessing());
} }
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.dialect.Dialect#lock()
*/
@Override
public LockClause lock() {
// H2 Dialect does not support the same lock keywords as PostgreSQL, but it supports the ANSI SQL standard.
// see https://www.h2database.com/html/commands.html
// and https://www.h2database.com/html/features.html#compatibility
return AnsiDialect.INSTANCE.lock();
}
} }

3
spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/PartTreeR2dbcQuery.java

@ -39,6 +39,7 @@ import org.springframework.r2dbc.core.PreparedOperation;
* *
* @author Roman Chigvintsev * @author Roman Chigvintsev
* @author Mark Paluch * @author Mark Paluch
* @author Diego Krupitza
* @since 1.1 * @since 1.1
*/ */
public class PartTreeR2dbcQuery extends AbstractR2dbcQuery { public class PartTreeR2dbcQuery extends AbstractR2dbcQuery {
@ -119,7 +120,7 @@ public class PartTreeR2dbcQuery extends AbstractR2dbcQuery {
RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation(); RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation();
R2dbcQueryCreator queryCreator = new R2dbcQueryCreator(tree, dataAccessStrategy, entityMetadata, accessor, R2dbcQueryCreator queryCreator = new R2dbcQueryCreator(tree, dataAccessStrategy, entityMetadata, accessor,
projectedProperties); projectedProperties, this.getQueryMethod().getLock());
return queryCreator.createQuery(getDynamicSort(accessor)); return queryCreator.createQuery(getDynamicSort(accessor));
}); });
} }

31
spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryCreator.java

@ -18,21 +18,18 @@ package org.springframework.data.r2dbc.repository.query;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.core.StatementMapper; import org.springframework.data.r2dbc.core.StatementMapper;
import org.springframework.data.relational.repository.Lock;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.relational.core.query.Criteria; import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.sql.Column; import org.springframework.data.relational.core.sql.*;
import org.springframework.data.relational.core.sql.Expression;
import org.springframework.data.relational.core.sql.Expressions;
import org.springframework.data.relational.core.sql.Functions;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.repository.query.RelationalEntityMetadata; import org.springframework.data.relational.repository.query.RelationalEntityMetadata;
import org.springframework.data.relational.repository.query.RelationalParameterAccessor; import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
import org.springframework.data.relational.repository.query.RelationalQueryCreator; import org.springframework.data.relational.repository.query.RelationalQueryCreator;
@ -48,6 +45,7 @@ import org.springframework.r2dbc.core.PreparedOperation;
* @author Mark Paluch * @author Mark Paluch
* @author Mingyuan Wu * @author Mingyuan Wu
* @author Myeonghyeon Lee * @author Myeonghyeon Lee
* @author Diego Krupitza
* @since 1.1 * @since 1.1
*/ */
class R2dbcQueryCreator extends RelationalQueryCreator<PreparedOperation<?>> { class R2dbcQueryCreator extends RelationalQueryCreator<PreparedOperation<?>> {
@ -58,6 +56,7 @@ class R2dbcQueryCreator extends RelationalQueryCreator<PreparedOperation<?>> {
private final RelationalEntityMetadata<?> entityMetadata; private final RelationalEntityMetadata<?> entityMetadata;
private final List<String> projectedProperties; private final List<String> projectedProperties;
private final Class<?> entityToRead; private final Class<?> entityToRead;
private final Optional<Lock> lock;
/** /**
* Creates new instance of this class with the given {@link PartTree}, {@link ReactiveDataAccessStrategy}, * Creates new instance of this class with the given {@link PartTree}, {@link ReactiveDataAccessStrategy},
@ -71,7 +70,7 @@ class R2dbcQueryCreator extends RelationalQueryCreator<PreparedOperation<?>> {
*/ */
public R2dbcQueryCreator(PartTree tree, ReactiveDataAccessStrategy dataAccessStrategy, public R2dbcQueryCreator(PartTree tree, ReactiveDataAccessStrategy dataAccessStrategy,
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor, RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor,
List<String> projectedProperties) { List<String> projectedProperties, Optional<Lock> lock) {
super(tree, accessor); super(tree, accessor);
this.tree = tree; this.tree = tree;
@ -81,6 +80,7 @@ class R2dbcQueryCreator extends RelationalQueryCreator<PreparedOperation<?>> {
this.entityMetadata = entityMetadata; this.entityMetadata = entityMetadata;
this.projectedProperties = projectedProperties; this.projectedProperties = projectedProperties;
this.entityToRead = entityMetadata.getTableEntity().getType(); this.entityToRead = entityMetadata.getTableEntity().getType();
this.lock = lock;
} }
/** /**
@ -138,6 +138,10 @@ class R2dbcQueryCreator extends RelationalQueryCreator<PreparedOperation<?>> {
selectSpec = selectSpec.distinct(); selectSpec = selectSpec.distinct();
} }
if (this.lock.isPresent()) {
selectSpec = selectSpec.lock(this.lock.get().value());
}
return statementMapper.getMappedObject(selectSpec); return statementMapper.getMappedObject(selectSpec);
} }
@ -154,15 +158,15 @@ class R2dbcQueryCreator extends RelationalQueryCreator<PreparedOperation<?>> {
for (String projectedProperty : projectedProperties) { for (String projectedProperty : projectedProperties) {
RelationalPersistentProperty property = entity.getPersistentProperty(projectedProperty); RelationalPersistentProperty property = entity.getPersistentProperty(projectedProperty);
Column column = table.column(property != null ? property.getColumnName() : SqlIdentifier.unquoted(projectedProperty)); Column column = table
.column(property != null ? property.getColumnName() : SqlIdentifier.unquoted(projectedProperty));
expressions.add(column); expressions.add(column);
} }
} else if (tree.isExistsProjection()) { } else if (tree.isExistsProjection()) {
expressions = dataAccessStrategy.getIdentifierColumns(entityToRead).stream() expressions = dataAccessStrategy.getIdentifierColumns(entityToRead).stream().map(table::column)
.map(table::column) .collect(Collectors.toList());
.collect(Collectors.toList());
} else if (tree.isCountProjection()) { } else if (tree.isCountProjection()) {
Expression countExpression = entityMetadata.getTableEntity().hasIdProperty() Expression countExpression = entityMetadata.getTableEntity().hasIdProperty()
@ -171,9 +175,8 @@ class R2dbcQueryCreator extends RelationalQueryCreator<PreparedOperation<?>> {
expressions = Collections.singletonList(Functions.count(countExpression)); expressions = Collections.singletonList(Functions.count(countExpression));
} else { } else {
expressions = dataAccessStrategy.getAllColumns(entityToRead).stream() expressions = dataAccessStrategy.getAllColumns(entityToRead).stream().map(table::column)
.map(table::column) .collect(Collectors.toList());
.collect(Collectors.toList());
} }
return expressions.toArray(new Expression[0]); return expressions.toArray(new Expression[0]);

33
spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java

@ -28,6 +28,7 @@ import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.relational.repository.Lock;
import org.springframework.data.r2dbc.repository.Modifying; import org.springframework.data.r2dbc.repository.Modifying;
import org.springframework.data.r2dbc.repository.Query; import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
@ -53,6 +54,7 @@ import org.springframework.util.ClassUtils;
* *
* @author Mark Paluch * @author Mark Paluch
* @author Stephen Cohen * @author Stephen Cohen
* @author Diego Krupitza
*/ */
public class R2dbcQueryMethod extends QueryMethod { public class R2dbcQueryMethod extends QueryMethod {
@ -66,6 +68,7 @@ public class R2dbcQueryMethod extends QueryMethod {
private final Optional<Query> query; private final Optional<Query> query;
private final boolean modifying; private final boolean modifying;
private final Lazy<Boolean> isCollectionQuery; private final Lazy<Boolean> isCollectionQuery;
private final Optional<Lock> lock;
private @Nullable RelationalEntityMetadata<?> metadata; private @Nullable RelationalEntityMetadata<?> metadata;
@ -113,11 +116,11 @@ public class R2dbcQueryMethod extends QueryMethod {
} }
} }
this.query = Optional.ofNullable( this.query = Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Query.class));
AnnotatedElementUtils.findMergedAnnotation(method, Query.class));
this.modifying = AnnotatedElementUtils.hasAnnotation(method, Modifying.class); this.modifying = AnnotatedElementUtils.hasAnnotation(method, Modifying.class);
this.isCollectionQuery = Lazy.of(() -> (!(isPageQuery() || isSliceQuery()) this.isCollectionQuery = Lazy.of(() -> (!(isPageQuery() || isSliceQuery())
&& ReactiveWrappers.isMultiValueType(metadata.getReturnType(method).getType())) || super.isCollectionQuery()); && ReactiveWrappers.isMultiValueType(metadata.getReturnType(method).getType())) || super.isCollectionQuery());
this.lock = Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Lock.class));
} }
/* (non-Javadoc) /* (non-Javadoc)
@ -144,6 +147,22 @@ public class R2dbcQueryMethod extends QueryMethod {
return modifying; return modifying;
} }
/**
* @return is a {@link Lock} annotation present or not.
*/
public boolean hasLockMode() {
return this.lock.isPresent();
}
/**
* Looks up the {@link Lock} annotation from the query method.
*
* @return the {@link Optional} wrapped {@link Lock} annotation.
*/
Optional<Lock> getLock() {
return this.lock;
}
/* /*
* All reactive query methods are streaming queries. * All reactive query methods are streaming queries.
* (non-Javadoc) * (non-Javadoc)
@ -166,8 +185,7 @@ public class R2dbcQueryMethod extends QueryMethod {
Class<?> returnedObjectType = getReturnedObjectType(); Class<?> returnedObjectType = getReturnedObjectType();
Class<?> domainClass = getDomainClass(); Class<?> domainClass = getDomainClass();
if (ClassUtils.isPrimitiveOrWrapper(returnedObjectType) if (ClassUtils.isPrimitiveOrWrapper(returnedObjectType) || ReflectionUtils.isVoid(returnedObjectType)) {
|| ReflectionUtils.isVoid(returnedObjectType)) {
this.metadata = new SimpleRelationalEntityMetadata<>((Class<Object>) domainClass, this.metadata = new SimpleRelationalEntityMetadata<>((Class<Object>) domainClass,
mappingContext.getRequiredPersistentEntity(domainClass)); mappingContext.getRequiredPersistentEntity(domainClass));
@ -214,8 +232,8 @@ public class R2dbcQueryMethod extends QueryMethod {
} }
/** /**
* Returns the required query string declared in a {@link Query} annotation * Returns the required query string declared in a {@link Query} annotation or throws {@link IllegalStateException} if
* or throws {@link IllegalStateException} if neither the annotation found nor the attribute was specified. * neither the annotation found nor the attribute was specified.
* *
* @return the query string. * @return the query string.
* @throws IllegalStateException in case query method has no annotated query. * @throws IllegalStateException in case query method has no annotated query.
@ -235,8 +253,7 @@ public class R2dbcQueryMethod extends QueryMethod {
} }
/** /**
* @return {@literal true} if the {@link Method} is annotated with * @return {@literal true} if the {@link Method} is annotated with {@link Query}.
* {@link Query}.
*/ */
public boolean hasAnnotatedQuery() { public boolean hasAnnotatedQuery() {
return getQueryAnnotation().isPresent(); return getQueryAnnotation().isPresent();

24
spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/StatementMapperUnitTests.java

@ -26,6 +26,7 @@ import org.springframework.data.r2dbc.core.StatementMapper.UpdateSpec;
import org.springframework.data.r2dbc.dialect.PostgresDialect; import org.springframework.data.r2dbc.dialect.PostgresDialect;
import org.springframework.data.relational.core.query.Criteria; import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.query.Update; import org.springframework.data.relational.core.query.Update;
import org.springframework.data.relational.core.sql.LockMode;
import org.springframework.r2dbc.core.PreparedOperation; import org.springframework.r2dbc.core.PreparedOperation;
import org.springframework.r2dbc.core.binding.BindTarget; import org.springframework.r2dbc.core.binding.BindTarget;
@ -33,6 +34,7 @@ import org.springframework.r2dbc.core.binding.BindTarget;
* Unit tests for {@link DefaultStatementMapper}. * Unit tests for {@link DefaultStatementMapper}.
* *
* @author Mark Paluch * @author Mark Paluch
* @author Diego Krupitza
*/ */
class StatementMapperUnitTests { class StatementMapperUnitTests {
@ -80,4 +82,26 @@ class StatementMapperUnitTests {
assertThat(preparedOperation.toQuery()) assertThat(preparedOperation.toQuery())
.isEqualTo("SELECT table.* FROM table ORDER BY table.id DESC LIMIT 2 OFFSET 2"); .isEqualTo("SELECT table.* FROM table ORDER BY table.id DESC LIMIT 2 OFFSET 2");
} }
@Test // gh-1041
void shouldMapSelectWithSharedLock() {
StatementMapper.SelectSpec selectSpec = StatementMapper.SelectSpec.create("table").withProjection("*")
.lock(LockMode.PESSIMISTIC_READ);
PreparedOperation<?> preparedOperation = mapper.getMappedObject(selectSpec);
assertThat(preparedOperation.toQuery()).isEqualTo("SELECT table.* FROM table FOR SHARE OF table");
}
@Test // gh-1041
void shouldMapSelectWithWriteLock() {
StatementMapper.SelectSpec selectSpec = StatementMapper.SelectSpec.create("table").withProjection("*")
.lock(LockMode.PESSIMISTIC_WRITE);
PreparedOperation<?> preparedOperation = mapper.getMappedObject(selectSpec);
assertThat(preparedOperation.toQuery()).isEqualTo("SELECT table.* FROM table FOR UPDATE OF table");
}
} }

36
spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java

@ -23,6 +23,8 @@ import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.Value; import lombok.Value;
import org.springframework.data.relational.core.sql.LockMode;
import org.springframework.data.relational.repository.Lock;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Hooks; import reactor.core.publisher.Hooks;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -58,6 +60,7 @@ import org.springframework.transaction.reactive.TransactionalOperator;
* *
* @author Mark Paluch * @author Mark Paluch
* @author Manousos Mathioudakis * @author Manousos Mathioudakis
* @author Diego Krupitza
*/ */
public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport { public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport {
@ -380,6 +383,33 @@ public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcInteg
.verifyComplete(); .verifyComplete();
} }
@Test
void getAllByNameWithWriteLock() {
shouldInsertNewItems();
repository.getAllByName("SCHAUFELRADBAGGER") //
.as(StepVerifier::create) //
.consumeNextWith(actual -> {
assertThat(actual.getName()).isEqualTo("SCHAUFELRADBAGGER");
}) //
.verifyComplete();
}
@Test
void findByNameAndFlagWithReadLock() {
shouldInsertNewItems();
repository.findByNameAndFlag("SCHAUFELRADBAGGER", true) //
.as(StepVerifier::create) //
.consumeNextWith(actual -> {
assertThat(actual.getName()).isEqualTo("SCHAUFELRADBAGGER");
assertThat(actual.isFlag()).isTrue();
}) //
.verifyComplete();
}
private static Object getCount(Map<String, Object> map) { private static Object getCount(Map<String, Object> map) {
return map.getOrDefault("count", map.get("COUNT")); return map.getOrDefault("count", map.get("COUNT"));
} }
@ -393,6 +423,12 @@ public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcInteg
@NoRepositoryBean @NoRepositoryBean
interface LegoSetRepository extends ReactiveCrudRepository<LegoSet, Integer> { interface LegoSetRepository extends ReactiveCrudRepository<LegoSet, Integer> {
@Lock(LockMode.PESSIMISTIC_WRITE)
Flux<LegoSet> getAllByName(String name);
@Lock(LockMode.PESSIMISTIC_READ)
Flux<LegoSet> findByNameAndFlag(String name, Boolean flag);
Flux<LegoSet> findByNameContains(String name); Flux<LegoSet> findByNameContains(String name);
Flux<LegoSet> findFirst10By(); Flux<LegoSet> findFirst10By();

44
spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/query/PartTreeR2dbcQueryUnitTests.java

@ -36,7 +36,6 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness; import org.mockito.quality.Strictness;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
@ -48,8 +47,10 @@ import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.dialect.DialectResolver; import org.springframework.data.r2dbc.dialect.DialectResolver;
import org.springframework.data.r2dbc.dialect.R2dbcDialect; import org.springframework.data.r2dbc.dialect.R2dbcDialect;
import org.springframework.data.r2dbc.mapping.R2dbcMappingContext; import org.springframework.data.r2dbc.mapping.R2dbcMappingContext;
import org.springframework.data.relational.repository.Lock;
import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.relational.core.mapping.Table;
import org.springframework.data.relational.core.sql.LockMode;
import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor; import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
@ -64,6 +65,7 @@ import org.springframework.r2dbc.core.binding.BindTarget;
* @author Mark Paluch * @author Mark Paluch
* @author Mingyuan Wu * @author Mingyuan Wu
* @author Myeonghyeon Lee * @author Myeonghyeon Lee
* @author Diego Krupitza
*/ */
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT) @MockitoSettings(strictness = Strictness.LENIENT)
@ -660,8 +662,38 @@ class PartTreeR2dbcQueryUnitTests {
dataAccessStrategy); dataAccessStrategy);
PreparedOperation<?> query = createQuery(queryMethod, r2dbcQuery, "John"); PreparedOperation<?> query = createQuery(queryMethod, r2dbcQuery, "John");
assertThat(query.get()) assertThat(query.get()) //
.isEqualTo("SELECT COUNT(users.id) FROM " + TABLE + " WHERE " + TABLE + ".first_name = $1"); .isEqualTo("SELECT COUNT(users.id) FROM " + TABLE + " WHERE " + TABLE + ".first_name = $1");
}
@Test // gh-1041
void createQueryWithPessimisticWriteLock() throws Exception {
R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameAndLastName", String.class, String.class);
PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, operations, r2dbcConverter, dataAccessStrategy);
String firstname = "Diego";
String lastname = "Krupitza";
PreparedOperation<?> query = createQuery(queryMethod, r2dbcQuery, firstname, lastname);
assertThat(query.get()).isEqualTo(
"SELECT users.id, users.first_name, users.last_name, users.date_of_birth, users.age, users.active FROM users WHERE users.first_name = $1 AND (users.last_name = $2) FOR UPDATE OF users");
}
@Test // gh-1041
void createQueryWithPessimisticReadLock() throws Exception {
R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameAndAge", String.class, Integer.class);
PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, operations, r2dbcConverter, dataAccessStrategy);
String firstname = "Diego";
Integer age = 22;
PreparedOperation<?> query = createQuery(queryMethod, r2dbcQuery, firstname, age);
assertThat(query.get()).isEqualTo(
"SELECT users.id, users.first_name, users.last_name, users.date_of_birth, users.age, users.active FROM users WHERE users.first_name = $1 AND (users.age = $2) FOR SHARE OF users");
} }
private PreparedOperation<?> createQuery(R2dbcQueryMethod queryMethod, PartTreeR2dbcQuery r2dbcQuery, private PreparedOperation<?> createQuery(R2dbcQueryMethod queryMethod, PartTreeR2dbcQuery r2dbcQuery,
@ -687,6 +719,12 @@ class PartTreeR2dbcQueryUnitTests {
@SuppressWarnings("ALL") @SuppressWarnings("ALL")
interface UserRepository extends Repository<User, Long> { interface UserRepository extends Repository<User, Long> {
@Lock(LockMode.PESSIMISTIC_WRITE)
Flux<User> findAllByFirstNameAndLastName(String firstName, String lastName);
@Lock(LockMode.PESSIMISTIC_READ)
Flux<User> findAllByFirstNameAndAge(String firstName, Integer age);
Flux<User> findAllByFirstName(String firstName); Flux<User> findAllByFirstName(String firstName);
Flux<User> findAllByLastNameAndFirstName(String lastName, String firstName); Flux<User> findAllByLastNameAndFirstName(String lastName, String firstName);

35
spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethodUnitTests.java

@ -18,6 +18,8 @@ package org.springframework.data.r2dbc.repository.query;
import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.*;
import kotlin.Unit; import kotlin.Unit;
import org.springframework.data.relational.repository.Lock;
import org.springframework.data.relational.core.sql.LockMode;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
@ -47,6 +49,7 @@ import org.springframework.data.repository.core.support.DefaultRepositoryMetadat
* *
* @author Mark Paluch * @author Mark Paluch
* @author Stephen Cohen * @author Stephen Cohen
* @author Diego Krupitza
*/ */
class R2dbcQueryMethodUnitTests { class R2dbcQueryMethodUnitTests {
@ -141,6 +144,32 @@ class R2dbcQueryMethodUnitTests {
assertThat(method.getEntityInformation().getJavaType()).isAssignableFrom(Contact.class); assertThat(method.getEntityInformation().getJavaType()).isAssignableFrom(Contact.class);
} }
@Test // GH-1041
void returnsQueryMethodWithCorrectLockTypeWriteLock() throws Exception {
R2dbcQueryMethod queryMethodWithWriteLock = queryMethod(PersonRepository.class, "queryMethodWithWriteLock");
assertThat(queryMethodWithWriteLock.getLock()).isPresent();
assertThat(queryMethodWithWriteLock.getLock().get().value()).isEqualTo(LockMode.PESSIMISTIC_WRITE);
}
@Test // GH-1041
void returnsQueryMethodWithCorrectLockTypeReadLock() throws Exception {
R2dbcQueryMethod queryMethodWithReadLock = queryMethod(PersonRepository.class, "queryMethodWithReadLock");
assertThat(queryMethodWithReadLock.getLock()).isPresent();
assertThat(queryMethodWithReadLock.getLock().get().value()).isEqualTo(LockMode.PESSIMISTIC_READ);
}
@Test // GH-1041
void returnsQueryMethodWithCorrectLockTypeNoLock() throws Exception {
R2dbcQueryMethod queryMethodWithWriteLock = queryMethod(SampleRepository.class, "methodReturningAnInterface");
assertThat(queryMethodWithWriteLock.getLock()).isEmpty();
}
private R2dbcQueryMethod queryMethod(Class<?> repository, String name, Class<?>... parameters) throws Exception { private R2dbcQueryMethod queryMethod(Class<?> repository, String name, Class<?>... parameters) throws Exception {
Method method = repository.getMethod(name, parameters); Method method = repository.getMethod(name, parameters);
@ -150,6 +179,12 @@ class R2dbcQueryMethodUnitTests {
interface PersonRepository extends Repository<Contact, Long> { interface PersonRepository extends Repository<Contact, Long> {
@Lock(LockMode.PESSIMISTIC_WRITE)
Mono<Contact> queryMethodWithWriteLock();
@Lock(LockMode.PESSIMISTIC_READ)
Mono<Contact> queryMethodWithReadLock();
Mono<Contact> findMonoByLastname(String lastname, Pageable pageRequest); Mono<Contact> findMonoByLastname(String lastname, Pageable pageRequest);
Mono<Page<Contact>> findMonoPageByLastname(String lastname, Pageable pageRequest); Mono<Page<Contact>> findMonoPageByLastname(String lastname, Pageable pageRequest);

Loading…
Cancel
Save