diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcCountQueryCreator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcCountQueryCreator.java index da89bc75aa..58c9158839 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcCountQueryCreator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcCountQueryCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,18 +30,21 @@ import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.PartTree; +import java.util.Optional; + /** * {@link JdbcQueryCreator} that creates {@code COUNT(*)} queries without applying limit/offset and {@link Sort}. * * @author Mark Paluch + * @author Diego Krupitza * @since 2.2 */ class JdbcCountQueryCreator extends JdbcQueryCreator { JdbcCountQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect, RelationalEntityMetadata entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery, - ReturnedType returnedType) { - super(context, tree, converter, dialect, entityMetadata, accessor, isSliceQuery, returnedType); + ReturnedType returnedType, Optional lockMode) { + super(context, tree, converter, dialect, entityMetadata, accessor, isSliceQuery, returnedType, lockMode); } @Override diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java index d86549f141..e21248ca5d 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -57,6 +58,7 @@ * @author Mark Paluch * @author Jens Schauder * @author Myeonghyeon Lee + * @author Diego Krupitza * @since 2.0 */ class JdbcQueryCreator extends RelationalQueryCreator { @@ -69,6 +71,7 @@ class JdbcQueryCreator extends RelationalQueryCreator { private final RenderContextFactory renderContextFactory; private final boolean isSliceQuery; private final ReturnedType returnedType; + private final Optional lockMode; /** * Creates new instance of this class with the given {@link PartTree}, {@link JdbcConverter}, {@link Dialect}, @@ -85,7 +88,7 @@ class JdbcQueryCreator extends RelationalQueryCreator { */ JdbcQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect, RelationalEntityMetadata entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery, - ReturnedType returnedType) { + ReturnedType returnedType, Optional lockMode) { super(tree, accessor); Assert.notNull(converter, "JdbcConverter must not be null"); @@ -102,6 +105,7 @@ class JdbcQueryCreator extends RelationalQueryCreator { this.renderContextFactory = new RenderContextFactory(dialect); this.isSliceQuery = isSliceQuery; this.returnedType = returnedType; + this.lockMode = lockMode; } /** @@ -168,7 +172,12 @@ protected ParametrizedQuery complete(@Nullable Criteria criteria, Sort sort) { whereBuilder); selectOrderBuilder = applyOrderBy(sort, entity, table, selectOrderBuilder); - Select select = selectOrderBuilder.build(); + SelectBuilder.BuildSelect completedBuildSelect = selectOrderBuilder; + if (this.lockMode.isPresent()) { + completedBuildSelect = selectOrderBuilder.lock(this.lockMode.get().value()); + } + + Select select = completedBuildSelect.build(); String sql = SqlRenderer.create(renderContextFactory.createRenderContext()).render(select); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java index 4893418f29..9c979bcce9 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,6 +47,7 @@ * @author Kazuki Shimizu * @author Moises Cisneros * @author Hebert Coelho + * @author Diego Krupitza */ public class JdbcQueryMethod extends QueryMethod { @@ -168,7 +169,6 @@ public String getNamedQueryName() { return StringUtils.hasText(annotatedName) ? annotatedName : super.getNamedQueryName(); } - /** * Returns the class to be used as {@link org.springframework.jdbc.core.RowMapper} * @@ -245,6 +245,22 @@ Optional lookupQueryAnnotation() { return doFindAnnotation(Query.class); } + /** + * @return is a {@link Lock} annotation present or not. + */ + public boolean hasLockMode() { + return lookupLockAnnotation().isPresent(); + } + + /** + * Looks up the {@link Lock} annotation from the query method. + * + * @return the {@link Optional} wrapped {@link Lock} annotation. + */ + Optional lookupLockAnnotation() { + return doFindAnnotation(Lock.class); + } + @SuppressWarnings("unchecked") private Optional doFindAnnotation(Class annotationType) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/Lock.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/Lock.java new file mode 100644 index 0000000000..0ef95ca0fb --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/Lock.java @@ -0,0 +1,39 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.query; + +import org.springframework.data.annotation.QueryAnnotation; +import org.springframework.data.relational.core.sql.LockMode; + +import java.lang.annotation.*; + +/** + * Annotation to provide a lock mode for a given query. + * + * @author Diego Krupitza + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@QueryAnnotation +@Documented +public @interface Lock { + + /** + * Defines which type of {@link LockMode} we want to use. + */ + LockMode value() default LockMode.PESSIMISTIC_READ; + +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java index fccbe0a00c..94eff0ae58 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,6 +50,7 @@ * * @author Mark Paluch * @author Jens Schauder + * @author Diego Krupitza * @since 2.0 */ public class PartTreeJdbcQuery extends AbstractJdbcQuery { @@ -163,7 +164,7 @@ private JdbcQueryExecution getQueryExecution(ResultProcessor processor, RelationalEntityMetadata entityMetadata = getQueryMethod().getEntityInformation(); JdbcCountQueryCreator queryCreator = new JdbcCountQueryCreator(context, tree, converter, dialect, - entityMetadata, accessor, false, processor.getReturnedType()); + entityMetadata, accessor, false, processor.getReturnedType(), getQueryMethod().lookupLockAnnotation()); ParametrizedQuery countQuery = queryCreator.createQuery(Sort.unsorted()); Object count = singleObjectQuery((rs, i) -> rs.getLong(1)).execute(countQuery.getQuery(), @@ -181,7 +182,7 @@ protected ParametrizedQuery createQuery(RelationalParametersParameterAccessor ac RelationalEntityMetadata entityMetadata = getQueryMethod().getEntityInformation(); JdbcQueryCreator queryCreator = new JdbcQueryCreator(context, tree, converter, dialect, entityMetadata, accessor, - getQueryMethod().isSliceQuery(), returnedType); + getQueryMethod().isSliceQuery(), returnedType, this.getQueryMethod().lookupLockAnnotation()); return queryCreator.createQuery(getDynamicSort(accessor)); } @@ -231,7 +232,7 @@ static class PageQueryExecution implements JdbcQueryExecution> { private final LongSupplier countSupplier; PageQueryExecution(JdbcQueryExecution> delegate, Pageable pageable, - LongSupplier countSupplier) { + LongSupplier countSupplier) { this.delegate = delegate; this.pageable = pageable; this.countSupplier = countSupplier; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index 66201877e3..70391a68a2 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,6 +51,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jdbc.core.mapping.AggregateReference; +import org.springframework.data.jdbc.repository.query.Lock; import org.springframework.data.jdbc.repository.query.Modifying; import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; @@ -61,6 +62,7 @@ import org.springframework.data.relational.core.mapping.event.AbstractRelationalEvent; import org.springframework.data.relational.core.mapping.event.AfterConvertEvent; import org.springframework.data.relational.core.mapping.event.AfterLoadEvent; +import org.springframework.data.relational.core.sql.LockMode; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; @@ -331,6 +333,13 @@ public void findAllByQueryName() { assertThat(repository.findAllByNamedQuery()).hasSize(1); } + @Test + void findAllByFirstnameWithLock() { + DummyEntity dummyEntity = createDummyEntity(); + repository.save(dummyEntity); + assertThat(repository.findAllByName(dummyEntity.getName())).hasSize(1); + } + @Test // GH-1022 public void findAllByCustomQueryName() { @@ -574,7 +583,11 @@ private Instant createDummyBeforeAndAfterNow() { interface DummyEntityRepository extends CrudRepository { + @Lock(LockMode.PESSIMISTIC_WRITE) + List findAllByName(String name); + List findAllByNamedQuery(); + @Query(name = "DummyEntity.customQuery") List findAllByCustomNamedQuery(); @@ -624,6 +637,7 @@ interface DummyEntityRepository extends CrudRepository { List findByFlagTrue(); List findByRef(int ref); + List findByRef(AggregateReference ref); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethodUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethodUnitTests.java index 9089ba5fcb..d30a3cdb24 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethodUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethodUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.relational.core.sql.LockMode; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; @@ -41,6 +42,7 @@ * @author Oliver Gierke * @author Moises Cisneros * @author Mark Paluch + * @author Diego Krupitza */ public class JdbcQueryMethodUnitTests { @@ -120,6 +122,37 @@ public void returnsNullIfNoQueryIsFound() throws NoSuchMethodException { assertThat(queryMethod.getDeclaredQuery()).isEqualTo(null); } + @Test // GH-1041 + void returnsQueryMethodWithLock() throws NoSuchMethodException { + + JdbcQueryMethod queryMethodWithWriteLock = createJdbcQueryMethod("queryMethodWithWriteLock"); + JdbcQueryMethod queryMethodWithReadLock = createJdbcQueryMethod("queryMethodWithReadLock"); + + assertThat(queryMethodWithWriteLock.hasLockMode()).isTrue(); + assertThat(queryMethodWithReadLock.hasLockMode()).isTrue(); + } + + @Test // GH-1041 + void returnsQueryMethodWithCorrectLockType() throws NoSuchMethodException { + + JdbcQueryMethod queryMethodWithWriteLock = createJdbcQueryMethod("queryMethodWithWriteLock"); + JdbcQueryMethod queryMethodWithReadLock = createJdbcQueryMethod("queryMethodWithReadLock"); + + assertThat(queryMethodWithWriteLock.lookupLockAnnotation()).isPresent(); + assertThat(queryMethodWithReadLock.lookupLockAnnotation()).isPresent(); + + assertThat(queryMethodWithWriteLock.lookupLockAnnotation().get().value()).isEqualTo(LockMode.PESSIMISTIC_WRITE); + assertThat(queryMethodWithReadLock.lookupLockAnnotation().get().value()).isEqualTo(LockMode.PESSIMISTIC_READ); + } + + @Lock(LockMode.PESSIMISTIC_WRITE) + @Query + private void queryMethodWithWriteLock() {} + + @Lock(LockMode.PESSIMISTIC_READ) + @Query + private void queryMethodWithReadLock() {} + @Query(value = QUERY, rowMapperClass = CustomRowMapper.class) private void queryMethod() {} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java index b0f5bf4a02..eded809c00 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,6 +42,8 @@ import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.data.relational.core.mapping.MappedCollection; import org.springframework.data.relational.core.mapping.Table; +import org.springframework.data.relational.core.sql.In; +import org.springframework.data.relational.core.sql.LockMode; import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.Repository; @@ -58,6 +60,7 @@ * @author Mark Paluch * @author Jens Schauder * @author Myeonghyeon Lee + * @author Diego Krupitza */ @ExtendWith(MockitoExtension.class) public class PartTreeJdbcQueryUnitTests { @@ -85,7 +88,7 @@ public void createQueryByAggregateReference() throws Exception { PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); Hobby hobby = new Hobby(); hobby.name = "twentythree"; - ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] {hobby}), returnedType); + ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { hobby }), returnedType); assertSoftly(softly -> { @@ -96,6 +99,47 @@ public void createQueryByAggregateReference() throws Exception { }); } + @Test // GH-922 + void createQueryWithPessimisticWriteLock() throws Exception { + + JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameAndLastName", String.class, String.class); + PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); + + String firstname = "Diego"; + String lastname = "Krupitza"; + ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { firstname, lastname }), + returnedType); + + assertSoftly(softly -> { + + softly.assertThat(query.getQuery().toUpperCase()).endsWith("FOR UPDATE"); + + softly.assertThat(query.getParameterSource().getValue("first_name")).isEqualTo(firstname); + softly.assertThat(query.getParameterSource().getValue("last_name")).isEqualTo(lastname); + }); + } + + @Test // GH-922 + void createQueryWithPessimisticReadLock() throws Exception { + + JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameAndAge", String.class, Integer.class); + PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); + + String firstname = "Diego"; + Integer age = 22; + ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { firstname, age }), + returnedType); + + assertSoftly(softly -> { + + // this is also for update since h2 dialect does not distinguish between lockmodes + softly.assertThat(query.getQuery().toUpperCase()).endsWith("FOR UPDATE"); + + softly.assertThat(query.getParameterSource().getValue("first_name")).isEqualTo(firstname); + softly.assertThat(query.getParameterSource().getValue("age")).isEqualTo(age); + }); + } + @Test // DATAJDBC-318 public void shouldFailForQueryByList() throws Exception { @@ -116,7 +160,7 @@ public void createQueryForQueryByAggregateReference() throws Exception { JdbcQueryMethod queryMethod = getQueryMethod("findViaReferenceByHobbyReference", AggregateReference.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); AggregateReference hobby = AggregateReference.to("twentythree"); - ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] {hobby}), returnedType); + ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { hobby }), returnedType); assertSoftly(softly -> { @@ -133,7 +177,7 @@ public void createQueryForQueryByAggregateReferenceId() throws Exception { JdbcQueryMethod queryMethod = getQueryMethod("findViaIdByHobbyReference", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); String hobby = "twentythree"; - ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] {hobby}), returnedType); + ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { hobby }), returnedType); assertSoftly(softly -> { @@ -213,7 +257,6 @@ public void createsQueryToFindAllEntitiesByOneOfTwoStringAttributes() throws Exc + ".\"FIRST_NAME\" = :first_name)"); } - @Test // DATAJDBC-318 public void createsQueryToFindAllEntitiesByDateAttributeBetween() throws Exception { @@ -644,6 +687,12 @@ private RelationalParametersParameterAccessor getAccessor(JdbcQueryMethod queryM @NoRepositoryBean interface UserRepository extends Repository { + @Lock(LockMode.PESSIMISTIC_WRITE) + List findAllByFirstNameAndLastName(String firstName, String lastName); + + @Lock(LockMode.PESSIMISTIC_READ) + List findAllByFirstNameAndAge(String firstName, Integer age); + List findAllByFirstName(String firstName); List findAllByHated(Hobby hobby); @@ -758,7 +807,6 @@ static class AnotherEmbedded { } static class Hobby { - @Id - String name; + @Id String name; } }