Skip to content

Introduced pessimistic locks for derived queries. #1158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<Lock> lockMode) {
super(context, tree, converter, dialect, entityMetadata, accessor, isSliceQuery, returnedType, lockMode);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -57,6 +58,7 @@
* @author Mark Paluch
* @author Jens Schauder
* @author Myeonghyeon Lee
* @author Diego Krupitza
* @since 2.0
*/
class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
Expand All @@ -69,6 +71,7 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
private final RenderContextFactory renderContextFactory;
private final boolean isSliceQuery;
private final ReturnedType returnedType;
private final Optional<Lock> lockMode;

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

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

/**
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -47,6 +47,7 @@
* @author Kazuki Shimizu
* @author Moises Cisneros
* @author Hebert Coelho
* @author Diego Krupitza
*/
public class JdbcQueryMethod extends QueryMethod {

Expand Down Expand Up @@ -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}
*
Expand Down Expand Up @@ -245,6 +245,22 @@ Optional<Query> 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<Lock> lookupLockAnnotation() {
return doFindAnnotation(Lock.class);
}

@SuppressWarnings("unchecked")
private <A extends Annotation> Optional<A> doFindAnnotation(Class<A> annotationType) {

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -50,6 +50,7 @@
*
* @author Mark Paluch
* @author Jens Schauder
* @author Diego Krupitza
* @since 2.0
*/
public class PartTreeJdbcQuery extends AbstractJdbcQuery {
Expand Down Expand Up @@ -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(),
Expand All @@ -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));
}

Expand Down Expand Up @@ -231,7 +232,7 @@ static class PageQueryExecution<T> implements JdbcQueryExecution<Slice<T>> {
private final LongSupplier countSupplier;

PageQueryExecution(JdbcQueryExecution<? extends Collection<T>> delegate, Pageable pageable,
LongSupplier countSupplier) {
LongSupplier countSupplier) {
this.delegate = delegate;
this.pageable = pageable;
this.countSupplier = countSupplier;
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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() {

Expand Down Expand Up @@ -574,7 +583,11 @@ private Instant createDummyBeforeAndAfterNow() {

interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {

@Lock(LockMode.PESSIMISTIC_WRITE)
List<DummyEntity> findAllByName(String name);

List<DummyEntity> findAllByNamedQuery();

@Query(name = "DummyEntity.customQuery")
List<DummyEntity> findAllByCustomNamedQuery();

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

List<DummyEntity> findByRef(int ref);

List<DummyEntity> findByRef(AggregateReference<DummyEntity, Long> ref);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -41,6 +42,7 @@
* @author Oliver Gierke
* @author Moises Cisneros
* @author Mark Paluch
* @author Diego Krupitza
*/
public class JdbcQueryMethodUnitTests {

Expand Down Expand Up @@ -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() {}

Expand Down
Loading