Skip to content
Open
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
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-2209-SNAPSHOT</version>
<packaging>pom</packaging>

<name>Spring Data Relational Parent</name>
Expand Down
2 changes: 1 addition & 1 deletion spring-data-jdbc-distribution/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-2209-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
4 changes: 2 additions & 2 deletions spring-data-jdbc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<modelVersion>4.0.0</modelVersion>

<artifactId>spring-data-jdbc</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-2209-SNAPSHOT</version>

<name>Spring Data JDBC</name>
<description>Spring Data module for JDBC repositories.</description>
Expand All @@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-2209-SNAPSHOT</version>
</parent>

<properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,9 @@ private <T> RelationalPersistentEntity<T> getRequiredPersistentEntity(Class<T> t
}

private <T> void updateWithoutVersion(DbAction.UpdateRoot<T> update) {
accessStrategy.update(update.entity(), update.getEntityType());

boolean updated = accessStrategy.update(update.entity(), update.getEntityType());
accessStrategy.getDialect().getUpdateRowCountVerification().rowsModified(updated);
}

private <T> void updateWithVersion(DbAction.UpdateRoot<T> update) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.springframework.data.relational.core.dialect.LimitClause;
import org.springframework.data.relational.core.dialect.LockClause;
import org.springframework.data.relational.core.dialect.OrderByNullPrecedence;
import org.springframework.data.relational.core.dialect.UpdateRowCountVerification;
import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.relational.core.sql.SimpleFunction;
import org.springframework.data.relational.core.sql.render.SelectRenderContext;
Expand Down Expand Up @@ -268,6 +269,11 @@ public SimpleFunction getExistsFunction() {
public boolean supportsSingleQueryLoading() {
return delegate.supportsSingleQueryLoading();
}

@Override
public UpdateRowCountVerification getUpdateRowCountVerification() {
return delegate.getUpdateRowCountVerification();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.springframework.data.mapping.PersistentPropertyPaths;
import org.springframework.data.relational.core.conversion.DbAction;
import org.springframework.data.relational.core.conversion.IdValueSource;
import org.springframework.data.relational.core.dialect.AnsiDialect;
import org.springframework.data.relational.core.mapping.AggregatePath;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
Expand Down Expand Up @@ -120,12 +121,12 @@ public void idGenerationOfChildInList() {
assertThat(newRoot.list.get(0).id).isEqualTo(24L);
}

@Test
// GH-537
@Test // GH-537
void populatesIdsIfNecessaryForAllRootsThatWereProcessed() {

DummyEntity root1 = new DummyEntity().withId(123L);
when(accessStrategy.update(root1, DummyEntity.class)).thenReturn(true);
when(accessStrategy.getDialect()).thenReturn(AnsiDialect.INSTANCE);
DbAction.UpdateRoot<DummyEntity> rootUpdate1 = new DbAction.UpdateRoot<>(root1, null);
executionContext.executeUpdateRoot(rootUpdate1);
Content content1 = new Content();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@
import org.springframework.data.mapping.PersistentPropertyPaths;
import org.springframework.data.relational.core.conversion.DbAction;
import org.springframework.data.relational.core.conversion.IdValueSource;
import org.springframework.data.relational.core.dialect.AnsiDialect;
import org.springframework.data.relational.core.mapping.AggregatePath;
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.dialect.UpdateRowCountVerification;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.relational.core.sql.SqlIdentifier;
Expand Down Expand Up @@ -196,6 +200,7 @@ void updates_whenReferencesWithImmutableIdAreInserted() {

root.id = 123L;
when(accessStrategy.update(root, DummyEntity.class)).thenReturn(true);
when(accessStrategy.getDialect()).thenReturn(AnsiDialect.INSTANCE);
DbAction.UpdateRoot<DummyEntity> rootUpdate = new DbAction.UpdateRoot<>(root, null);
executionContext.executeUpdateRoot(rootUpdate);

Expand All @@ -219,6 +224,7 @@ void populatesIdsIfNecessaryForAllRootsThatWereProcessed() {
DummyEntity root1 = new DummyEntity();
root1.id = 123L;
when(accessStrategy.update(root1, DummyEntity.class)).thenReturn(true);
when(accessStrategy.getDialect()).thenReturn(AnsiDialect.INSTANCE);
DbAction.UpdateRoot<DummyEntity> rootUpdate1 = new DbAction.UpdateRoot<>(root1, null);
executionContext.executeUpdateRoot(rootUpdate1);
Content content1 = new Content();
Expand All @@ -242,6 +248,38 @@ void populatesIdsIfNecessaryForAllRootsThatWereProcessed() {
assertThat(content2.id).isEqualTo(12L);
}

@Test // GH-2209
void updateWithoutVersionThrowsWhenZeroRowsUpdatedAndDialectIsStrict() {

root.id = 123L;
when(accessStrategy.update(root, DummyEntity.class)).thenReturn(false);
Dialect dialect = mock(Dialect.class);
when(dialect.getUpdateRowCountVerification()).thenReturn(UpdateRowCountVerification.STRICT);
when(accessStrategy.getDialect()).thenReturn(dialect);

DbAction.UpdateRoot<DummyEntity> rootUpdate = new DbAction.UpdateRoot<>(root, null);

assertThatThrownBy(() -> executionContext.executeUpdateRoot(rootUpdate)) //
.isInstanceOf(IncorrectUpdateSemanticsDataAccessException.class) //
.hasMessageContaining("No rows were updated");
}

@Test // GH-2209
void updateWithoutVersionSucceedsWhenZeroRowsUpdatedAndDialectIsLenient() {

root.id = 123L;
when(accessStrategy.update(root, DummyEntity.class)).thenReturn(false);
Dialect dialect = mock(Dialect.class);
when(dialect.getUpdateRowCountVerification()).thenReturn(UpdateRowCountVerification.LENIENT);
when(accessStrategy.getDialect()).thenReturn(dialect);

DbAction.UpdateRoot<DummyEntity> rootUpdate = new DbAction.UpdateRoot<>(root, null);
executionContext.executeUpdateRoot(rootUpdate);

List<DummyEntity> newRoots = executionContext.populateIdsIfNecessary();
assertThat(newRoots).containsExactly(root);
}

DbAction.Insert<?> createInsert(DbAction.WithEntity<?> parent, String propertyName, Object value,
@Nullable Object key, IdValueSource idValueSource) {

Expand Down
4 changes: 2 additions & 2 deletions spring-data-r2dbc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<modelVersion>4.0.0</modelVersion>

<artifactId>spring-data-r2dbc</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-2209-SNAPSHOT</version>

<name>Spring Data R2DBC</name>
<description>Spring Data module for R2DBC</description>
Expand All @@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-2209-SNAPSHOT</version>
</parent>

<properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,11 @@ private <T> Mono<T> doUpdate(T entity, @Nullable Object version, SqlIdentifier t
if (persistentEntity.hasVersionProperty()) {
sink.error(OptimisticLockingUtils.updateFailed(entity, version, persistentEntity));
}
try {
dataAccessStrategy.getDialect().getUpdateRowCountVerification().rowsModified(rowsUpdated);
} catch (DataAccessException ex) {
sink.error(ex);
}
}).then(maybeCallAfterSave(entity, outboundRow, tableName));
}

Expand Down
4 changes: 2 additions & 2 deletions spring-data-relational/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
<modelVersion>4.0.0</modelVersion>

<artifactId>spring-data-relational</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-2209-SNAPSHOT</version>

<name>Spring Data Relational</name>
<description>Spring Data Relational support</description>

<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-2209-SNAPSHOT</version>
</parent>

<properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,16 @@ default SimpleFunction getExistsFunction() {
default boolean supportsSingleQueryLoading() {
return true;
}

/**
* How to verify the result of an UPDATE (e.g. whether zero rows updated is considered an error). Database and
* driver behavior differs (affected vs matched rows). Override in dialect implementations to reflect
* database-specific semantics.
*
* @return the update row count verification for this dialect. Default is {@link UpdateRowCountVerification#LENIENT}.
* @since 4.1
*/
default UpdateRowCountVerification getUpdateRowCountVerification() {
return UpdateRowCountVerification.LENIENT;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2026-present 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.relational.core.dialect;

import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;

/**
* Defines whether the result of a save/update is considered an error.
* <p>
* Database and driver behavior differs: some report <em>affected</em> rows (e.g. MySQL/InnoDB can report 0 for a no-op
* update), others report <em>matched</em> rows. Use {@link #LENIENT} when the database may legitimately report 0 rows
* for a successful no-op update; use {@link #STRICT} when you want to detect missing rows or failed updates.
*
* @since 4.1
*/
@FunctionalInterface
public interface UpdateRowCountVerification {

/**
* Do not throw when an UPDATE affects 0 rows. Use when the database or driver may report 0 for a no-op update (e.g.
* MySQL/InnoDB with affected-rows semantics, Vitess).
*/
UpdateRowCountVerification LENIENT = (rowsModified) -> {};

/**
* Throw {@link org.springframework.dao.IncorrectUpdateSemanticsDataAccessException} when an UPDATE affects 0 rows.
* Use to detect missing rows, RLS-blocked updates, or stale identifiers.
*/
UpdateRowCountVerification STRICT = (rowsModified) -> {
throw new IncorrectUpdateSemanticsDataAccessException("No rows were updated");
};

/**
* @param rowsModified flag to indicate whether the update affected any rows.
* @throws IncorrectUpdateSemanticsDataAccessException in case the update did not affect any rows and this is
* considered a failed operation.
*/
void rowsModified(boolean rowsModified);

/**
* @param nrRows number of rows affected by the update.
* @throws IncorrectUpdateSemanticsDataAccessException in case the update did not affect any rows and this is
* considered a failed operation.
*/
default void rowsModified(long nrRows) {
rowsModified(nrRows > 0);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2026-present 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.relational.core.dialect;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;

/**
* Unit tests for {@link UpdateRowCountVerification} and {@link Dialect#getUpdateRowCountVerification()}.
*
* @since 4.1
*/
class UpdateRowCountVerificationUnitTests {

@Test
void dialectDefaultIsLenient() {
assertThat(AnsiDialect.INSTANCE.getUpdateRowCountVerification()).isEqualTo(UpdateRowCountVerification.LENIENT);
}
}
Loading