spring-projects / spring-data-r2dbc

Provide support to increase developer productivity in Java when using Reactive Relational Database Connectivity. Uses familiar Spring concepts such as a DatabaseClient for core API usage and lightweight repository style data access.
Apache License 2.0
708 stars 132 forks source link

Spring Data R2DBC Repository wrongly decides to use UPDATE instead of INSERT when a new Entity with (externally) provided id is saved using save() #824

Closed TheBlackTomcat closed 1 year ago

TheBlackTomcat commented 1 year ago

The code:

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.0.5'
    id 'io.spring.dependency-management' version '1.1.0'
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
    runtimeOnly 'org.postgresql:postgresql'
    runtimeOnly 'org.postgresql:r2dbc-postgresql'
}

============

@Configuration
@EnableR2dbcRepositories
public class DatabaseConfiguration {

    @Bean
    ConnectionFactoryInitializer database(ConnectionFactory connectionFactory) {

        ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer();
        initializer.setConnectionFactory(connectionFactory);
        initializer.setDatabasePopulator(new ResourceDatabasePopulator(new ClassPathResource("one.sql")));

        return initializer;
    }
}

============

  import org.springframework.data.annotation.Id;

  public record One(@Id Integer id, String value) {
  }

============

  import org.springframework.data.r2dbc.repository.R2dbcRepository;

  public interface OneRepository extends R2dbcRepository<One, Integer> {
  }

============

 @Override
  public Mono<OneData> createOrReplaceOne(String id, Mono<OneData> dataMono) {
      Mono<One> entityMono = dataMono.map(data -> OneMapper.toEntity(data, id));
      return entityMono.flatMap(oneRepository::save)
              .map(OneMapper::toData);
  }

============

The problem seems to be when deciding it's a new object to save:

in class:

package org.springframework.data.r2dbc.repository.support;

public class SimpleR2dbcRepository<T, ID> implements R2dbcRepository<T, ID> {

    @Override
    @Transactional
    public <S extends T> Mono<S> save(S objectToSave) {

        Assert.notNull(objectToSave, "Object to save must not be null");

     >      if (this.entity.isNew(objectToSave)) {
            return this.entityOperations.insert(objectToSave);
        }

        return this.entityOperations.update(objectToSave);
    }

and in class:

package org.springframework.data.mapping.model;

class PersistentEntityIsNewStrategy implements IsNewStrategy

    @Override
    public boolean isNew(Object entity) {

        Object value = valueLookup.apply(entity);

        if (value == null) {
            return true;
        }

        if (valueType != null && !valueType.isPrimitive()) {
     >          return false;
        }

        if (value instanceof Number) {
            return ((Number) value).longValue() == 0;
        }

        throw new IllegalArgumentException(
                String.format("Could not determine whether %s is new; Unsupported identifier or version property", entity));
    }

and the database (Postgres) error:

2023-04-09 21:08:09 2023-04-09T18:08:09.158Z ERROR 1 --- [actor-tcp-nio-2] a.w.r.e.AbstractErrorWebExceptionHandler : [e579f03d-5] 500 Server Error for HTTP PUT "/ones/3" 2023-04-09 21:08:09

2023-04-09 21:08:09 org.springframework.dao.TransientDataAccessResourceException: Failed to update table [one]; Row with Id [3] does not exist

2023-04-09 21:08:09 at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.lambda$doUpdate$13(R2dbcEntityTemplate.java:639) ~[spring-data-r2dbc-3.0.4.jar!/:3.0.4] 2023-04-09 21:08:09 Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 2023-04-09 21:08:09 Assembly trace from producer [reactor.core.publisher.MonoHandleFuseable] : 2023-04-09 21:08:09 reactor.core.publisher.Mono.handle(Mono.java:3206)

2023-04-09 21:08:09 org.springframework.data.r2dbc.core.R2dbcEntityTemplate.doUpdate(R2dbcEntityTemplate.java:628)

2023-04-09 21:08:09 Error has been observed at the following site(s): 2023-04-09 21:08:09 _____Mono.handle ⇢ at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.doUpdate(R2dbcEntityTemplate.java:628) 2023-04-09 21:08:09 Mono.then ⇢ at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.doUpdate(R2dbcEntityTemplate.java:641) 2023-04-09 21:08:09 *__Mono.flatMap ⇢ at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.lambda$doUpdate$12(R2dbcEntityTemplate.java:592) 2023-04-09 21:08:09 ____Mono.flatMap ⇢ at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.doUpdate(R2dbcEntityTemplate.java:574) 2023-04-09 21:08:09 __Mono.usingWhen ⇢ at org.springframework.data.repository.core.support.RepositoryMethodInvoker$ReactiveInvocationListenerDecorator.decorate(RepositoryMethodInvoker.java:225) 2023-04-09 21:08:09 *____Mono.flatMap ⇢ at [...] DefaultOneService.createOrReplaceOne(DefaultOneService.java:32) 2023-04-09 21:08:09 | Mono.map ⇢ at [...]DefaultOneService.createOrReplaceOne(DefaultOneService.java:33) 2023-04-09 21:08:09 | Mono.log ⇢ at [...]DefaultOneService.createOrReplaceOne(DefaultOneService.java:33) 2023-04-09 21:08:09 | Mono.flatMap ⇢ at [...] OneRestHandler.putOne(OneRestHandler.java:36) 2023-04-09 21:08:09 | Mono.map ⇢ at org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter.handle(HandlerFunctionAdapter.java:62) 2023-04-09 21:08:09 | Mono.flatMap ⇢ at org.springframework.web.reactive.DispatcherHandler.handleRequestWith(DispatcherHandler.java:184) 2023-04-09 21:08:09 *Mono.flatMap ⇢ at org.springframework.web.reactive.DispatcherHandler.handle(DispatcherHandler.java:154) 2023-04-09 21:08:09 __Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:106) 2023-04-09 21:08:09 | Mono.doOnEach ⇢ at org.springframework.web.filter.reactive.ServerHttpObservationFilter.filter(ServerHttpObservationFilter.java:109) 2023-04-09 21:08:09 | Mono.doOnCancel ⇢ at org.springframework.web.filter.reactive.ServerHttpObservationFilter.filter(ServerHttpObservationFilter.java:119) 2023-04-09 21:08:09 |_ Mono.contextWrite ⇢ at org.springframework.web.filter.reactive.ServerHttpObservationFilter.filter(ServerHttpObservationFilter.java:123) 2023-04-09 21:08:09 Mono.transformDeferred ⇢ at org.springframework.web.filter.reactive.ServerHttpObservationFilter.filter(ServerHttpObservationFilter.java:102) 2023-04-09 21:08:09 |_ checkpoint ⇢ org.springframework.web.filter.reactive.ServerHttpObservationFilter [DefaultWebFilterChain] 2023-04-09 21:08:09 __Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:106) 2023-04-09 21:08:09 |_ Mono.onErrorResume ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler.handle(ExceptionHandlingWebHandler.java:77) 2023-04-09 21:08:09 ____Mono.error ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler$CheckpointInsertingHandler.handle(ExceptionHandlingWebHandler.java:98) 2023-04-09 21:08:09 |_ checkpoint ⇢ HTTP PUT "/ones/3" [ExceptionHandlingWebHandler]

mp911de commented 1 year ago

This is a duplicate of #738.

Spring Data detects the isNew state either by the presence of an identifier or the version property. If you choose to provide an Id, then please implement the Persistable interface to add a hint to Spring Data whether the entity should be considered new. See also the reference documentation for further details.