Is this a correct solution to the Spring Data JDBC problem of the insert/update?

2.7k Views Asked by At

PROBLEM:

when I tried to create a new entity 'Customer' with Spring Data JDBC (in a Spring-boot application)

@Data
public class Customer {
    @Id
    private String identifier;
    private String name;
}

Using a customerRepository:

@Repository
public interface CustomerRepository extends CrudRepository<Customer, String> {
}

For the test so:

@Test
public void givenNewCustomer_shouldSaveCustomerInDataBase() {
    //given
    final Customer newCustomer = new Customer();
    newCustomer.setIdentifier("0002");
    newCustomer.setName("juan");
    //when
    Customer customerSaved = repository.save(newCustomer);
    //then
    then(customerSaved).isNotNull();
}

I got this error:

Caused by: org.springframework.dao.IncorrectUpdateSemanticsDataAccessException: Failed to update entity [Customer(identifier=0002, name=juan)]. Id [0002] not found in database.
    at org.springframework.data.jdbc.core.JdbcAggregateChangeExecutionContext.updateWithoutVersion(JdbcAggregateChangeExecutionContext.java:370)
    at org.springframework.data.jdbc.core.JdbcAggregateChangeExecutionContext.executeUpdateRoot(JdbcAggregateChangeExecutionContext.java:115)
    at org.springframework.data.jdbc.core.AggregateChangeExecutor.execute(AggregateChangeExecutor.java:70)

I'm using testcontainer for postgresql data base with this initial script:

create table customer(
    identifier varchar(30) primary key,
    name varchar(39));
insert into customer(identifier,name) values ('0001','kevin');

I would like the persistence way to behave like EntityManager.merge () from JPA automatically, is there a way to do it? I have one but I don't know if it's the most correct.

(The full source code (very basic and straightforward) is available https://github.com/FabianSR/spring_data_jdbc_example)

PROPOSED SOLUTION:

I have implemented a possible solution to this problem, but I don't know if it's the correct way (without using JPA).

I have made that the Customer class extends a class that implements the Persistable interface, I have implemented its getId () method so that it searches in the Customer fields for the one that is annotated with @Id and returns its value. Also I have added the flag isNew with the default value to true.

package com.example.model.core;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.domain.Persistable;
import java.util.stream.Stream;

@Data
public abstract class AbstractEntity<I> implements Persistable<I> {

    @Transient
    public boolean isNew = true;

    @Override
    public I getId() {
        return Stream.of(this.getClass().getDeclaredFields())
                .filter(field -> field.isAnnotationPresent(Id.class))
                .map(field -> {
                    field.setAccessible(true);
                    return field;
                }).findFirst().map(field ->
                {
                    try {
                        return (I) field.get(this);
                    } catch (IllegalAccessException e) {
                        throw new RuntimeException(e);
                    }
                }).orElse(null);
    }
}

And the customer is as follows:

package com.example.model;

import com.example.model.core.AbstractEntity;
import lombok.Data;
import org.springframework.data.annotation.Id;

@Data
public class Customer extends AbstractEntity<String> {
    @Id
    private String identifier;
    private String name;
}

Now I have added a new method to the CustomerRepository interface, before saving a Customer, it first searches if it exists, modifies its isNew attribute and saves it (else just saves it)

 package com.example.repository;

import com.example.model.Customer;
import org.springframework.data.jdbc.repository.query.Modifying;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface CustomerRepository extends CrudRepository<Customer, String> {

    @Modifying
    default Customer merge(final Customer customer) {
        return this.findById(customer.getId()).map(
                c -> {
                    customer.setNew(false);
                    return this.save(customer);
                }
        ).orElse(this.save(customer));
    }
}

Finally, after changing the tests, calling the new 'merge' method instead of 'save', it already works:

   @Test
    public void givenNewCustomer_shouldSaveCustomerInDataBase() {
        //given
        final Customer newCustomer = new Customer();
        newCustomer.setIdentifier("0002");
        newCustomer.setName("juan");
        //when
        Customer customerSaved = repository.merge(newCustomer);
        //then
        then(customerSaved).isNotNull();
    }

    @Test
    public void givenOldCustomer_shouldUpdateCustomerInDataBase() {
        //Given
        final Customer oldCustomer = repository.findById("0001").orElseThrow(AssertionError::new);
        oldCustomer.setName(oldCustomer.getName() + " hall");
        //when
        repository.merge(oldCustomer);
        //then
        then(repository.findById("0001").map(Customer::getName).map("kevin hall"::equals).orElse(false)).isTrue();
    }

(Code in branch https://github.com/FabianSR/spring_data_jdbc_example/tree/proposed_solution)

Is this the best solution or is there a simpler one?

1

There are 1 best solutions below

2
On BEST ANSWER

The error you got could be caused by you setting the ID manually before saving. Try to save the data without an identifier so that the value is assigned automatically later on you will be able to update without any problem using repository.save().