@Transactional не работает должным образом, поскольку для сохранения в базе данных необходим метод сохранения.

@Transactionalдолжен сам отражать изменения, внесенные в сущность в базе данных. Я создаю приложение, в котором клиент может создать сущность Car, которая выглядит так (метод update позже используется PUT, не обращайте внимания на свойство brand):

@Entity
@Table(name = "cars")
public class Car {
    @Id
    @GeneratedValue(generator = "inc")
    @GenericGenerator(name = "inc", strategy = "increment")
    private int id;
    @NotBlank(message = "car name`s must be not empty")
    private String name;
    private LocalDateTime productionYear;
    private boolean tested;

    public Car() {
    }

    public Car(@NotBlank(message = "car name`s must be not empty") String name, LocalDateTime productionYear) {
        this.name = name;
        this.productionYear = productionYear;
    }

    @ManyToOne
    @JoinColumn(name = "brand_id")
    private Brand brand;

    public int getId() {
        return id;
    }

    void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public LocalDateTime getProductionYear() {
        return productionYear;
    }

    public void setProductionYear(LocalDateTime productionYear) {
        this.productionYear = productionYear;
    }

    public boolean isTested() {
        return tested;
    }

    public void setTested(boolean tested) {
        this.tested = tested;
    }

    public Brand getBrand() {
        return brand;
    }
   
    void setBrand(Brand brand) {
        this.brand = brand;
    }

    public Car update(final Car source) {
        this.productionYear = source.productionYear;
        this.brand = source.brand;
        this.tested = source.tested;
        this.name = source.name;
        return this;
    }
}

В моем приложении клиент может создать новый Car или обновить существующий с помощью метода PUT.

Мой контроллер:

    @RestController
public class CarController {
    private Logger logger = LoggerFactory.getLogger(CarController.class);
    private CarRepository repository;

    public CarController(CarRepository repository) {
        this.repository = repository;
    }

    //The client can create a new resource or update an existing one via PUT
    @Transactional
    @PutMapping("/cars/{id}")
    ResponseEntity<?> updateCar(@PathVariable int id, @Valid @RequestBody Car source) {
        //update
        if(repository.existsById(id)) {
            repository.findById(id).ifPresent(car -> {
                car.update(source); //it doesn`t work
                //Snippet below works
                //var updated = car.update(source);
                //repository.save(updated);
            });
            return ResponseEntity.noContent().build();
        }
        //create
        else {
            var result = repository.save(source);
            return ResponseEntity.created(URI.create("/" + id)).body(result);
        }
    }
}

Когда я создаю новую машину, она работает. Однако, как описано в коде, при отсутствии метода сохранения объект не изменяется, хотя я получаю статус 204 (нет содержимого). Когда есть метод сохранения, он работает нормально. Вы знаете, почему это так?

Один из пользователей попросил у меня сущность Brand. Я пока не создал ни одного объекта Brand, но по существу Car может принадлежать определенному Brand в моем приложении. Пока ни один Car не принадлежит ни одному Brand. Вот эта сущность:

@Entity
@Table(name = "brands")
public class Brand {
    @Id
    @GeneratedValue(generator = "i")
    @GenericGenerator(name = "i", strategy = "increment")
    private int id;
    @NotBlank(message = "brand name`s must be not empty")
    private String name;
    private LocalDateTime productionBrandYear;

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "brand")
    private Set<Car> cars;

    @ManyToOne
    @JoinColumn(name = "factory_id")
    private Factory factory;

    public Brand() {
    }

    public int getId() {
        return id;
    }

    void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public LocalDateTime getProductionBrandYear() {
        return productionBrandYear;
    }

    public void setProductionBrandYear(LocalDateTime productionBrandYear) {
        this.productionBrandYear = productionBrandYear;
    }

    public Set<Car> getCars() {
        return cars;
    }

    public void setCars(Set<Car> cars) {
        this.cars = cars;
    }

    public Factory getFactory() {
        return factory;
    }

    public void setFactory(Factory factory) {
        this.factory = factory;
    }
}

person Monoxyd    schedule 28.03.2021    source источник
comment
Вы можете также добавить Brand сущность?   -  person code_mechanic    schedule 29.03.2021
comment
Да, я отредактировал свой вопрос   -  person Monoxyd    schedule 29.03.2021
comment
Вам нужно Каскадировать свои изменения в родительском объекте, но это может вызвать ошибку, потому что здесь объект бренда является отдельным объектом (исходящим из запроса), поэтому вы должны сначала сохранить его и обновить в автомобиле. В настоящее время вы не выполняете каскадные обновления от автомобиля к бренду.   -  person code_mechanic    schedule 29.03.2021
comment
Извините, но я не совсем вас понимаю. Почему я должен каскадировать свои изменения с автомобиля на марку? Имеет ли это значение, когда речь идет о методе PUT?   -  person Monoxyd    schedule 29.03.2021
comment
Да, это имеет значение, ваш объект бренда является управляемым объектом, и когда у вас есть такие отношения, и вы хотите, чтобы изменения выполнялись для обоих объектов при обновлении одного, поэтому спящий режим знает, что если есть отдельный объект, то сохранить его или обновить существующий.   -  person code_mechanic    schedule 29.03.2021
comment
Это имеет смысл, но в настоящее время я создаю автомобили, где свойство brand имеет значение null, поэтому это немного странно для меня, поскольку я даже не создал никакого объекта бренда, и моя единственная цель на данный момент — обновить мой автомобиль, который не связан с Бренд. Я не хочу вносить какие-либо изменения в сущность "Бренд", поскольку ее не существует.   -  person Monoxyd    schedule 29.03.2021
comment
Вы сказали, что ваш объект бренда является управляемым объектом. Скорее, я бы сказал, что Автомобиль — это управляемая сущность. Но, может быть, я не совсем понимаю фазу управляемого объекта   -  person Monoxyd    schedule 29.03.2021
comment
Хорошо, вы можете не захотеть изменять или сохранять бренд, но вы сказали, что сущность не обновляется, что не изменилось в сущности, другие значения, которые вы установили? Как вы проверили? Также проверьте, вызывается ли ifPresent или нет, добавьте журналы для гибернации и проверьте, выполняются ли запросы.   -  person code_mechanic    schedule 29.03.2021
comment
Проверяю через Postman. Когда я пытаюсь обновить существующий автомобиль, я отправляю тело запроса с новым значением через PUT для полей: год выпуска, название, тестировалось (только марка неизменна и указывает на ноль). Затем я отправляю метод GET, который дает мне каждую машину, и машина, которую я пытался обновить, остается неизменной. ifPresent вызывается. Я пытаюсь добавить журналы для спящего режима, но мне это не удается. В любом случае, обычно запросы запускаются, потому что у меня проблема только с обновлением через PUT.   -  person Monoxyd    schedule 29.03.2021
comment
Вы включили @EnableTransactionManagement, чтобы аннотация @Transactional заработала?   -  person Nikolai Shevchenko    schedule 30.03.2021


Ответы (2)


Я попробовал ваши объекты с тем же вариантом использования локально и обнаружил, что все работает нормально, я пишу здесь свои выводы и конфигурации, чтобы вы могли проверить, что у вас происходит не так.

Итак, когда я выполняю вызов PUT, предоставляющий id, но объект Car не существует в таблице, он создается, и я получаю ответ 201 (полагаю, вы получаете то же самое) введите здесь описание изображения

вы можете видеть, что строка со значением также была вставлена ​​​​в таблицу

введите здесь описание изображения

и это журналы запросов, напечатанные

- [nio-8080-exec-8] org.hibernate.SQL: select count(*) as col_0_0_ from car car0_ where car0_.id=?
[nio-8080-exec-8] org.hibernate.SQL: select car0_.id as id1_1_0_, car0_.brand_id as brand_id5_1_0_, car0_.name as name2_1_0_, car0_.production_year as producti3_1_0_, car0_.tested as tested4_1_0_ from car car0_ where car0_.id=?
[nio-8080-exec-8] org.hibernate.SQL: insert into car (brand_id, name, production_year, tested) values (?, ?, ?, ?)

Теперь давайте перейдем к обновлению того же объекта, когда выдается запрос PUT для того же id с измененными значениями, обратите внимание, что значения изменяются в таблице и обновляют запросы в журнале введите здесь описание изображения

Вы можете видеть, что получен тот же ответ 204 с пустым телом, давайте посмотрим на запись в таблице введите здесь описание изображения

Итак, изменения отразились в БД, давайте посмотрим на логи SQL для этой операции.

 select count(*) as col_0_0_ from car car0_ where car0_.id=?
[nio-8080-exec-1] org.hibernate.SQL: select car0_.id as id1_1_0_, car0_.brand_id as brand_id5_1_0_, car0_.name as name2_1_0_, car0_.production_year as producti3_1_0_, car0_.tested as tested4_1_0_, brand1_.id as id1_0_1_, brand1_.name as name2_0_1_, brand1_.production_year as producti3_0_1_ from car car0_ left outer join brand brand1_ on car0_.brand_id=brand1_.id where car0_.id=?
[nio-8080-exec-1] org.hibernate.SQL: update car set brand_id=?, name=?, production_year=?, tested=? where id=?

Итак, я не уверен, как вы проверили и что вы проверили, но ваши объекты должны работать, я использовал ту же функцию контроллера, что и ваша.

@RestController
class CarController {
    private final CarRepository repository;

    public CarController(CarRepository repository) {
        this.repository = repository;
    }

    @PutMapping("/car/{id}")
    @Transactional
    public ResponseEntity<?> updateCar(@PathVariable Integer id, @RequestBody Car source) {

        if(repository.existsById(id)) {
            repository.findById(id).ifPresent(car -> car.update(source));
            return ResponseEntity.noContent().build();
        }else {
            Car created = repository.save(source);
            return ResponseEntity.created(URI.create("/" + created.getId())).body(created);
        }
    }
}

Возможные отличия от вашего исходного кода могут быть следующими:

  • Я использовал генератор IDENTITY для генерации ПЕРВИЧНОГО КЛЮЧА вместо того, который у вас есть на вашем объекте, так как мне было легко его протестировать.
  • Я предоставил bean-компонент ObjectMapper для сериализации/десериализации тела запроса в объект Car для поддержки преобразования Java 8 LocalDateTime, у вас может быть свой способ отправки значений даты и времени, чтобы он преобразовывался в объект Car.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;

// And Object mapper bean
    @Bean
    public static ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        return mapper;
    }

Однако эти различия не должны иметь значения.

application.properties Чтобы распечатать журналы запросов, чтобы проверить, выполняются ли запросы или нет.

spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.username=test
spring.datasource.password=test
spring.datasource.jpa.show-sql=true
spring.jpa.open-in-view=false

logging.level.org.hibernate.SQL=DEBUG
person code_mechanic    schedule 30.03.2021
comment
Спасибо за ваше время и внимание! Проблема в моем коде была действительно тривиальной. Я не сделал метод updateCar общедоступным, поэтому @Transactional не работал должным образом. - person Monoxyd; 30.03.2021
comment
ааа!!! Не заметил, хороший улов. - person code_mechanic; 30.03.2021

Тот факт, что вы обновляете объект автомобиля, не означает, что он обновляет значение в БД. Вам всегда нужно вызывать метод relay.save(), чтобы сохранить ваши изменения в БД.

person ruba    schedule 29.03.2021
comment
Я не согласен с вами. Посмотрите это. Существует аннотация @Transactional, поэтому она должна сохранять любые изменения, внесенные в управляемый объект. - person Monoxyd; 29.03.2021
comment
Вы правы, извините. Вы пытались перенести логику в другой класс? Возможно, из-за того, как работает контроллер, аннотация транзакции не работает и поэтому не сбрасывает изменения. Проверьте это stackoverflow.com/a/1099284. - person ruba; 29.03.2021