javaspringspring-bootspring-transactionshttp-put

@Transactional does not work as expected, because the "save" method is needed to save to the database


@Transactionalshould itself reflect the changes made to the entity in the database. I'm creating an application where the client can create a Car entity that looks like this (the update method is later used by PUT, do not pay attention to the brand property):

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

In my application, the client can create a new Car or update an existing one with the PUT method.

My controller:

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

When I create a new Car, it works. However as described in the code, when there is no save method the entity is not changed although I get the status 204 (no content). When there is a save method, it works fine. Do you know why this is so?

One of the users asked me for a Brand entity. I haven't created any Brand object so far but essentially Car can belong to a specific Brand in my app. So far, no Car belongs to any Brand. Here is this entity:

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

Solution

  • I tried your entities with same use case locally and found out everything is working fine, I am writing here my findings and configurations so that you can verify what's going on wrong for you.

    So, when I issue a PUT call providing id but Car entity doesn't exist into table, it gets created and I receive 201 response (I guess you are getting the same) enter image description here

    you can see that row with value got inserted into table as well

    enter image description here

    and these are the query logs printed

    - [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 (?, ?, ?, ?)
    

    Now, let's come to updating the same entity, when issued PUT request for same id with changed values notice that values changes in table and update queries in log enter image description here

    You can see that got same 204 response with empty body, let's look the table entry enter image description here

    So changes got reflected in DB, let's look at the SQL logs for this operation

     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=?
    
    

    So, I am not sure, how you verified and what you verified but your entities must work, I have used same controller function as yours

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

    Possible differences from your source code could be as follow:

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

    However, these differences should not matter.

    application.properties To print query logs to verify if queries are fired or not

    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