Skip to content

Commit 11a2364

Browse files
DiegoKrupitzaschauder
authored andcommitted
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 HSQLDB `Select ... FOR UPDATE`. See #1041 Original pull request #1158
1 parent c0cd813 commit 11a2364

File tree

8 files changed

+184
-21
lines changed

8 files changed

+184
-21
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcCountQueryCreator.java

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 the original author or authors.
2+
* Copyright 2021-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,18 +30,21 @@
3030
import org.springframework.data.repository.query.ReturnedType;
3131
import org.springframework.data.repository.query.parser.PartTree;
3232

33+
import java.util.Optional;
34+
3335
/**
3436
* {@link JdbcQueryCreator} that creates {@code COUNT(*)} queries without applying limit/offset and {@link Sort}.
3537
*
3638
* @author Mark Paluch
39+
* @author Diego Krupitza
3740
* @since 2.2
3841
*/
3942
class JdbcCountQueryCreator extends JdbcQueryCreator {
4043

4144
JdbcCountQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect,
4245
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery,
43-
ReturnedType returnedType) {
44-
super(context, tree, converter, dialect, entityMetadata, accessor, isSliceQuery, returnedType);
46+
ReturnedType returnedType, Optional<Lock> lockMode) {
47+
super(context, tree, converter, dialect, entityMetadata, accessor, isSliceQuery, returnedType, lockMode);
4548
}
4649

4750
@Override

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java

+12-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2021 the original author or authors.
2+
* Copyright 2020-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818
import java.util.ArrayList;
1919
import java.util.List;
2020
import java.util.Objects;
21+
import java.util.Optional;
2122

2223
import org.springframework.data.domain.Pageable;
2324
import org.springframework.data.domain.Sort;
@@ -57,6 +58,7 @@
5758
* @author Mark Paluch
5859
* @author Jens Schauder
5960
* @author Myeonghyeon Lee
61+
* @author Diego Krupitza
6062
* @since 2.0
6163
*/
6264
class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
@@ -69,6 +71,7 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
6971
private final RenderContextFactory renderContextFactory;
7072
private final boolean isSliceQuery;
7173
private final ReturnedType returnedType;
74+
private final Optional<Lock> lockMode;
7275

7376
/**
7477
* Creates new instance of this class with the given {@link PartTree}, {@link JdbcConverter}, {@link Dialect},
@@ -85,7 +88,7 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
8588
*/
8689
JdbcQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect,
8790
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery,
88-
ReturnedType returnedType) {
91+
ReturnedType returnedType, Optional<Lock> lockMode) {
8992
super(tree, accessor);
9093

9194
Assert.notNull(converter, "JdbcConverter must not be null");
@@ -102,6 +105,7 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
102105
this.renderContextFactory = new RenderContextFactory(dialect);
103106
this.isSliceQuery = isSliceQuery;
104107
this.returnedType = returnedType;
108+
this.lockMode = lockMode;
105109
}
106110

107111
/**
@@ -168,7 +172,12 @@ protected ParametrizedQuery complete(@Nullable Criteria criteria, Sort sort) {
168172
whereBuilder);
169173
selectOrderBuilder = applyOrderBy(sort, entity, table, selectOrderBuilder);
170174

171-
Select select = selectOrderBuilder.build();
175+
SelectBuilder.BuildSelect completedBuildSelect = selectOrderBuilder;
176+
if (this.lockMode.isPresent()) {
177+
completedBuildSelect = selectOrderBuilder.lock(this.lockMode.get().value());
178+
}
179+
180+
Select select = completedBuildSelect.build();
172181

173182
String sql = SqlRenderer.create(renderContextFactory.createRenderContext()).render(select);
174183

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2021 the original author or authors.
2+
* Copyright 2020-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -47,6 +47,7 @@
4747
* @author Kazuki Shimizu
4848
* @author Moises Cisneros
4949
* @author Hebert Coelho
50+
* @author Diego Krupitza
5051
*/
5152
public class JdbcQueryMethod extends QueryMethod {
5253

@@ -159,7 +160,6 @@ public String getNamedQueryName() {
159160
return StringUtils.hasText(annotatedName) ? annotatedName : super.getNamedQueryName();
160161
}
161162

162-
163163
/**
164164
* Returns the class to be used as {@link org.springframework.jdbc.core.RowMapper}
165165
*
@@ -236,6 +236,22 @@ Optional<Query> lookupQueryAnnotation() {
236236
return doFindAnnotation(Query.class);
237237
}
238238

239+
/**
240+
* @return is a {@link Lock} annotation present or not.
241+
*/
242+
public boolean hasLockMode() {
243+
return lookupLockAnnotation().isPresent();
244+
}
245+
246+
/**
247+
* Looks up the {@link Lock} annotation from the query method.
248+
*
249+
* @return the {@link Optional} wrapped {@link Lock} annotation.
250+
*/
251+
Optional<Lock> lookupLockAnnotation() {
252+
return doFindAnnotation(Lock.class);
253+
}
254+
239255
@SuppressWarnings("unchecked")
240256
private <A extends Annotation> Optional<A> doFindAnnotation(Class<A> annotationType) {
241257

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
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+
* https://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 org.springframework.data.jdbc.repository.query;
17+
18+
import org.springframework.data.annotation.QueryAnnotation;
19+
import org.springframework.data.relational.core.sql.LockMode;
20+
21+
import java.lang.annotation.*;
22+
23+
/**
24+
* Annotation to provide a lock mode for a given query.
25+
*
26+
* @author Diego Krupitza
27+
*/
28+
@Retention(RetentionPolicy.RUNTIME)
29+
@Target(ElementType.METHOD)
30+
@QueryAnnotation
31+
@Documented
32+
public @interface Lock {
33+
34+
/**
35+
* Defines which type of {@link LockMode} we want to use.
36+
*/
37+
LockMode value() default LockMode.PESSIMISTIC_READ;
38+
39+
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2021 the original author or authors.
2+
* Copyright 2020-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -50,6 +50,7 @@
5050
*
5151
* @author Mark Paluch
5252
* @author Jens Schauder
53+
* @author Diego Krupitza
5354
* @since 2.0
5455
*/
5556
public class PartTreeJdbcQuery extends AbstractJdbcQuery {
@@ -159,7 +160,7 @@ private JdbcQueryExecution<?> getQueryExecution(ResultProcessor processor,
159160
RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation();
160161

161162
JdbcCountQueryCreator queryCreator = new JdbcCountQueryCreator(context, tree, converter, dialect,
162-
entityMetadata, accessor, false, processor.getReturnedType());
163+
entityMetadata, accessor, false, processor.getReturnedType(), getQueryMethod().lookupLockAnnotation());
163164

164165
ParametrizedQuery countQuery = queryCreator.createQuery(Sort.unsorted());
165166
Object count = singleObjectQuery((rs, i) -> rs.getLong(1)).execute(countQuery.getQuery(),
@@ -177,7 +178,7 @@ protected ParametrizedQuery createQuery(RelationalParametersParameterAccessor ac
177178
RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation();
178179

179180
JdbcQueryCreator queryCreator = new JdbcQueryCreator(context, tree, converter, dialect, entityMetadata, accessor,
180-
getQueryMethod().isSliceQuery(), returnedType);
181+
getQueryMethod().isSliceQuery(), returnedType, this.getQueryMethod().lookupLockAnnotation());
181182
return queryCreator.createQuery(getDynamicSort(accessor));
182183
}
183184

@@ -227,7 +228,7 @@ static class PageQueryExecution<T> implements JdbcQueryExecution<Slice<T>> {
227228
private final LongSupplier countSupplier;
228229

229230
PageQueryExecution(JdbcQueryExecution<? extends Collection<T>> delegate, Pageable pageable,
230-
LongSupplier countSupplier) {
231+
LongSupplier countSupplier) {
231232
this.delegate = delegate;
232233
this.pageable = pageable;
233234
this.countSupplier = countSupplier;

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2021 the original author or authors.
2+
* Copyright 2017-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -51,6 +51,7 @@
5151
import org.springframework.data.domain.Pageable;
5252
import org.springframework.data.domain.Slice;
5353
import org.springframework.data.jdbc.core.mapping.AggregateReference;
54+
import org.springframework.data.jdbc.repository.query.Lock;
5455
import org.springframework.data.jdbc.repository.query.Modifying;
5556
import org.springframework.data.jdbc.repository.query.Query;
5657
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
@@ -61,6 +62,7 @@
6162
import org.springframework.data.relational.core.mapping.event.AbstractRelationalEvent;
6263
import org.springframework.data.relational.core.mapping.event.AfterConvertEvent;
6364
import org.springframework.data.relational.core.mapping.event.AfterLoadEvent;
65+
import org.springframework.data.relational.core.sql.LockMode;
6466
import org.springframework.data.repository.CrudRepository;
6567
import org.springframework.data.repository.core.NamedQueries;
6668
import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries;
@@ -331,6 +333,13 @@ public void findAllByQueryName() {
331333
assertThat(repository.findAllByNamedQuery()).hasSize(1);
332334
}
333335

336+
@Test
337+
void findAllByFirstnameWithLock() {
338+
DummyEntity dummyEntity = createDummyEntity();
339+
repository.save(dummyEntity);
340+
assertThat(repository.findAllByName(dummyEntity.getName())).hasSize(1);
341+
}
342+
334343
@Test // GH-1022
335344
public void findAllByCustomQueryName() {
336345

@@ -574,7 +583,11 @@ private Instant createDummyBeforeAndAfterNow() {
574583

575584
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
576585

586+
@Lock(LockMode.PESSIMISTIC_WRITE)
587+
List<DummyEntity> findAllByName(String name);
588+
577589
List<DummyEntity> findAllByNamedQuery();
590+
578591
@Query(name = "DummyEntity.customQuery")
579592
List<DummyEntity> findAllByCustomNamedQuery();
580593

@@ -624,6 +637,7 @@ interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
624637
List<DummyEntity> findByFlagTrue();
625638

626639
List<DummyEntity> findByRef(int ref);
640+
627641
List<DummyEntity> findByRef(AggregateReference<DummyEntity, Long> ref);
628642
}
629643

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethodUnitTests.java

+34-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2021 the original author or authors.
2+
* Copyright 2020-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@
2828

2929
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
3030
import org.springframework.data.projection.ProjectionFactory;
31+
import org.springframework.data.relational.core.sql.LockMode;
3132
import org.springframework.data.repository.core.NamedQueries;
3233
import org.springframework.data.repository.core.RepositoryMetadata;
3334
import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries;
@@ -41,6 +42,7 @@
4142
* @author Oliver Gierke
4243
* @author Moises Cisneros
4344
* @author Mark Paluch
45+
* @author Diego Krupitza
4446
*/
4547
public class JdbcQueryMethodUnitTests {
4648

@@ -120,6 +122,37 @@ public void returnsNullIfNoQueryIsFound() throws NoSuchMethodException {
120122
assertThat(queryMethod.getDeclaredQuery()).isEqualTo(null);
121123
}
122124

125+
@Test // GH-1041
126+
void returnsQueryMethodWithLock() throws NoSuchMethodException {
127+
128+
JdbcQueryMethod queryMethodWithWriteLock = createJdbcQueryMethod("queryMethodWithWriteLock");
129+
JdbcQueryMethod queryMethodWithReadLock = createJdbcQueryMethod("queryMethodWithReadLock");
130+
131+
assertThat(queryMethodWithWriteLock.hasLockMode()).isTrue();
132+
assertThat(queryMethodWithReadLock.hasLockMode()).isTrue();
133+
}
134+
135+
@Test // GH-1041
136+
void returnsQueryMethodWithCorrectLockType() throws NoSuchMethodException {
137+
138+
JdbcQueryMethod queryMethodWithWriteLock = createJdbcQueryMethod("queryMethodWithWriteLock");
139+
JdbcQueryMethod queryMethodWithReadLock = createJdbcQueryMethod("queryMethodWithReadLock");
140+
141+
assertThat(queryMethodWithWriteLock.lookupLockAnnotation()).isPresent();
142+
assertThat(queryMethodWithReadLock.lookupLockAnnotation()).isPresent();
143+
144+
assertThat(queryMethodWithWriteLock.lookupLockAnnotation().get().value()).isEqualTo(LockMode.PESSIMISTIC_WRITE);
145+
assertThat(queryMethodWithReadLock.lookupLockAnnotation().get().value()).isEqualTo(LockMode.PESSIMISTIC_READ);
146+
}
147+
148+
@Lock(LockMode.PESSIMISTIC_WRITE)
149+
@Query
150+
private void queryMethodWithWriteLock() {}
151+
152+
@Lock(LockMode.PESSIMISTIC_READ)
153+
@Query
154+
private void queryMethodWithReadLock() {}
155+
123156
@Query(value = QUERY, rowMapperClass = CustomRowMapper.class)
124157
private void queryMethod() {}
125158

0 commit comments

Comments
 (0)