0

@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;
    }
}
Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
Monoxyd
  • 406
  • 1
  • 7
  • 18
  • can you add `Brand` entity as well? – code_mechanic Mar 29 '21 at 02:57
  • Yes, I have edited my question – Monoxyd Mar 29 '21 at 10:17
  • You need to Cascade your changes to parent entity, but that might throw error, because here the brand object is a detached object (coming from request), so you have to save it first and update that in car. Currently, you are not cascading updates from car to brand. – code_mechanic Mar 29 '21 at 13:02
  • I'm sorry, but I don't quite understand you. Why do I have to Cascade my changes from car to brand? Does it really matter when it comes to the PUT method? – Monoxyd Mar 29 '21 at 14:02
  • Yes it matters, your brand object is managed entity and when you have such relationships and you want changes to be done for both entities while updating one, so hibernate knows that if there is a detached object then to persist it or update existing. – code_mechanic Mar 29 '21 at 14:14
  • It makes sense, but I am currently creating Cars where the `brand` property is null so that's a bit weird for me as i haven't even created any brand object and my only goal for now is to update my Car that is not associated with a Brand. I don't want any changes to the Brand entity as it doesn't exist – Monoxyd Mar 29 '21 at 14:59
  • You said that "your brand object is managed entity". Rather, I would say Car is managed entity. But maybe I don't quite understand the "managed entity" phase – Monoxyd Mar 29 '21 at 15:05
  • 1
    Ok, you may not want to change or save brand, but you said entity is not updated, what is not changed in entity, other values you set? How did you verify? Also do check that `ifPresent` is being called or not, add logs for hibernate and check if queries are fired. – code_mechanic Mar 29 '21 at 15:34
  • I check it through Postman. When I try to update an existing car, I send a request body with a new value via PUT for the fields: productionYear, name, tested (only brand is unchanged and points to null). Then I send a GET method which gives me every car and the car I tried to update remains unchanged. `ifPresent` is being called. I try to add logs for hibernate but I fail to do this. Anyway, generally queries are fired because I only have a problem with updating via PUT – Monoxyd Mar 29 '21 at 18:56
  • Did you turn on `@EnableTransactionManagement` to make `@Transactional` annotation work? – Nikolai Shevchenko Mar 30 '21 at 10:22

2 Answers2

1

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:

  • I used IDENTITY generator to generate the PRIMARY KEY, instead of the one you have on your entity as it was easy for me to test.
  • I provided ObjectMapper bean to serialize/deserialize the request body to Car object to support Java 8 LocalDateTime conversion, you may have your way to send datetime values, so that it converts to Car Object.
@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
code_mechanic
  • 1,097
  • 10
  • 16
  • 1
    Thank you for your time and attention! The problem in my code was really trivial. I have not made the `updateCar` method public so `@Transactional` did not work as expected. – Monoxyd Mar 30 '21 at 10:34
0

The fact that you are updating the car object doesn't mean it updates the value in the DB. You always need to call repository.save() method to persist your changes in the DB.

ruba
  • 120
  • 2
  • 9
  • I disagree with you. Look at [this](https://stackoverflow.com/questions/21552483/why-does-transactional-save-automatically-to-database#answer-21552670:~:text=Before%20the%20transactional%20method%20is%20about,entities%20are%20flushed%20to%20the%20database.). There is an `@Transactional` annotation, so it should save itself any changes made to the managed entity – Monoxyd Mar 29 '21 at 10:22
  • You are correct, sorry. Have you tried moving the logic into another class? Maybe it's because of how the controller works that the transaction annotation is not working and so it is not flushing the changes. Check this https://stackoverflow.com/a/1099284 – ruba Mar 29 '21 at 12:12