Является ли это правильным решением проблемы вставки/обновления Spring Data JDBC?

ПРОБЛЕМА:

когда я пытался создать новый объект «Клиент» с Spring Data JDBC (в приложении Spring-boot)

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

Использование клиентского репозитория:

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

Для теста так:

@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();
}

Я получил эту ошибку:

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)

Я использую testcontainer для базы данных postgresql с этим исходным скриптом:

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

Я хотел бы, чтобы способ сохранения автоматически вел себя как EntityManager.merge() из JPA, есть ли способ сделать это? У меня есть один, но я не знаю, самый ли он правильный.

(Полный исходный код (очень простой и понятный) доступен https://github.com/FabianSR/spring_data_jdbc_example)

ПРЕДЛОЖЕННОЕ РЕШЕНИЕ:

Я реализовал возможное решение этой проблемы, но не знаю, правильно ли это (без использования JPA).

Я сделал так, что класс Customer расширяет класс, реализующий интерфейс Persistable, я реализовал его метод getId(), чтобы он искал в полях Customer тот, который аннотирован @Id и возвращал его значение. Также я добавил флаг isNew со значением по умолчанию 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);
    }
}

А заказчик такой:

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;
}

Теперь я добавил новый метод в интерфейс CustomerRepository, перед сохранением клиента он сначала ищет, существует ли он, изменяет свой атрибут isNew и сохраняет его (иначе просто сохраняет его)

 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));
    }
}

Наконец, после изменения тестов, вызова нового метода 'merge' вместо 'save', он уже работает:

   @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();
    }

(Код в ветке https://github.com/FabianSR/spring_data_jdbc_example/tree/proposed_solution)

Это лучшее решение или есть более простое?


person FabianSR    schedule 29.01.2021    source источник
comment
Почему вы снова публикуете тот же вопрос? Вы уже получили ссылку на дубликат в своей предыдущей копии, и добавление — это просто одно из приведенных там решений. Следовательно, это все еще дубликат.   -  person Jens Schauder    schedule 29.01.2021
comment
Нет, я сейчас спрашиваю, действителен ли этот вариант или есть более простой..   -  person FabianSR    schedule 29.01.2021


Ответы (1)


Ошибка, которую вы получили, может быть вызвана тем, что вы установили идентификатор вручную перед сохранением. Попробуйте сохранить данные без идентификатора, чтобы значение присваивалось автоматически, в дальнейшем вы сможете без проблем обновиться с помощью repository.save().

person Cedric    schedule 29.01.2021
comment
Я понимаю, но у меня проблема в том, что идентификатор и информация о клиенте уже предоставлены внешней системой (используется событие, созданное другим приложением, в реальном приложении кода) :_( - person FabianSR; 29.01.2021
comment
Попробуйте использовать JpaRepository вместо CrudRepository в качестве базового класса для вашего репозитория. Если все еще не работает, есть saveAndFlush из репозитория Jpa, который может решить вашу проблему. - person Cedric; 29.01.2021