Transactional Saves Without Calling Update Method

Transactional saves without calling update method

Because hibernate will automatically detect changes made to persistent entities and update the database accordingly. This behaviour is documented in chapter 11 of the hibernate reference manual. The relevant part reads:

Hibernate defines and supports the following object states:

  • Transient - an object is transient if it has just been instantiated using the new operator, and it is not associated with a Hibernate Session. It has no persistent representation in the database and no identifier value has been assigned. Transient instances will be destroyed by the garbage collector if the application does not hold a reference anymore. Use the Hibernate Session to make an object persistent (and let Hibernate take care of the SQL statements that need to be executed for this transition).

  • Persistent - a persistent instance has a representation in the database and an identifier value. It might just have been saved or loaded, however, it is by definition in the scope of a Session. Hibernate will detect any changes made to an object in persistent state and synchronize the state with the database when the unit of work completes. Developers do not execute manual UPDATE statements, or DELETE statements when an object should be made transient.

  • Detached - a detached instance is an object that has been persistent, but its Session has been closed. The reference to the object is still valid, of course, and the detached instance might even be modified in this state. A detached instance can be reattached to a new Session at a later point in time, making it (and all the modifications) persistent again. This feature enables a programming model for long running units of work that require user think-time. We call them application transactions, i.e., a unit of work from the point of view of the user.

SpringBoot JPA need no .save() on @Transactional?

If you retrieve an entity, for example using the findOne method call within a transactional method it has become managed from that point by the persistence provider.

Now if you make any changes to that entity (which is actually a proxy object), upon transaction commit, those changes will be persisted to the database, regardless of the fact of invoking the save or update methods.

save or persist has to be used when you are creating a new entity from scratch and persistence provider does not know of its existence yet.

Remember that you can prevent making any changes upon commit, if you use detach or evict methods on that particular entity before those changes occur.

Why does @Transactional save automatically to database

This behaviour is one of the main purposes of transactionality.

Before the transactional method is about to return, the transaction commits, meaning all changes to the managed entities are flushed to the database.

If an error occurs, the transaction will be rolled back, meaning that no changes will be committed to the database.

You are probably getting the LazyInitializationException when trying to access a lazily loaded property, probably a collection from an entity. Lazily loded properties do not get instantiated when you fetch an entitiy from DB.

If you access a lazily loaded property in a transaction, the persistence provider will create a query, instantiate the result and attach it to the 'parent' entity.

EDIT: If you want to have the lazy properties loaded AND be able to change your entity without the changes being persisted to the DB, you can fetch the entity with fetch joins for the lazy properties.

em.createQuery("SELECT e FROM MyEntity e JOIN FETCH e.lazyProp");

Then proceed with one of the methods described by @orid.

If you are not using fetch joins, you will need to access the lazily loaded properties while still inside the transaction:

myEntity.getLazyProp().size();

Note the call to size(). Calling the getter is not enough as you will get a proxy. You need to perform an operation that needs the actual data from the property.

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

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)
Sample Image

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

Sample Image

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
Sample Image

You can see that got same 204 response with empty body, let's look the table entry
Sample Image

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

Why can I save without @Transactional?

The implementation of CrudRepository#save provided by Spring creates a transaction by default. Please see the documentation for further details.

Update list of entities transactional method

Yes, all the elements in the List will be updated to "black" into your database after the method ends.
You're most probably looking for the answer of why is doesn't require any update or merge method call.

@Transactional makes it a transaction and at the end of the transaction, the changes are flushed into database since transactions must be committed or rollbacked at the end of a transaction.

@Transactional doesn't rollback by calling an External method that throw an RuntimeException

Spring 's @Transactional is just a declarative way to manage the underlying JPA transaction. So it depends on the rollback behaviour of JPA which is defined by the spec. as follows :

For both transaction-scoped persistence contexts and for extended
persistence contexts that are joined to the current transaction,
transaction rollback causes all pre-existing managed instances and
removed instances[29] to become detached. The instances' state will be
the state of the instances at the point at which the transaction was
rolled back.
Transaction rollback typically causes the persistence
context to be in an inconsistent state at the point of rollback.

Hibernate docs also mentions the same :

Rolling back the database transaction does not put your business objects back into the state they were at the start of the transaction.
This means that the database state and the business objects will be
out of sync.
Usually, this is not a problem because exceptions are
not recoverable and you will have to start over after rollback anyway.

So it is the expected rollback behaviour which the Counter will still have the state just before the transaction is rollback (i.e count=1).

For the entity that is already exist in the DB , you can restore its state back to the same as DB manually by using entityManager.refresh() or simply use entityManager.get() or JPQL to retrieve it again from the DB.

For the entity that does not exist in the DB , simply re-execute the codes to create it again.

So rollback here just means nothing will be updated in DB. It does not mean that it will restore the object state to the moment just before executing a @Transactional method.

Glassfish/Hibernate save without calling save explicitely

If the entity you've fetched is in Managed state then at the end of transaction, changes made to the entity will be persisted. That's the contract that the JPA provider must obey to.

This question might be helpful for you: Transactional saves without calling update method



Related Topics



Leave a reply



Submit