Optimistic locking in a RESTful application

2019-03-28 18:56发布

问题:

At work, we're developing a RESTful application where the data layer will be handled by Hibernate. But we're not sure how to handle updates on entities.

We're planning to do the following:

1) client requests an entity by id
2) Hibernate loads the entity, the requested fields (always with the version) are copied to a DTO that is converted to JSON and sent to the client
3) Client manages some fields and sends the entity (with version number) back to the server.
4) Server receives the JSON that is converted to a DTO.
5) Corresponding entity is loaded from Hibernate and the DTO's props are copied to the entity.

=> The entity is always overwritten even if the version number of the client was set. Does this mean that we always have to check the version number of the client against the version number of the loaded instance by ourselves instead of Hibernate doing this?

In a regular application with sessions, the detached instance is saved in the HttpSession. Whenever the client updates the entity, the instance is retrieved from the HttpSession and some attributes are updated. Whenever Hibernate commits the update, a ObjectStaleException will be thrown if the version number is < the current version number.

The problem here is that we don't have any Http session because we're trying to be RESTful.

Is there a generic solution for handling optimistic locking in RESTful applications instead of checking version numbers by ourselves?

回答1:

Your strategy is good. Simply copy the version number coming from the client into the loaded entity (or use merge(), which will do the same), and when Hibernate flushes the entity, if the version number has been incremented, you'll have an optimistic lock exception.

You don't need to check anything by yourself. Hibernate does the check for you.



回答2:

The alternative to checking the version yourself is to just construct the entity object and invoke entityManager.merge which should also trigger optimistic lock exceptions if the version changed in the meantime, but it's won't throw if the object was deleted in the meantime. To handle that properly, you will have to check it yourself.

Instead of implementing this by loading entity state separately, you could make use of Blaze-Persistence Updatable Entity Views which is a library for developing DTOs on top of JPA that also implements support for optimistic locking. Your use case should be supported already, though I don't have a very good integration for Spring WebMvc yet, so you will have to do some plumbing yourself for now. I have something in mind for this though, it's just a matter of time and interested parties until the integration will be smoother.

Updatable entity views allow to map a subset of entities and also only flush that subset back. Thanks to the use of dirty tracking, it knows exactly what changed, allowing for fine grained flushing.

So the idea for PATCH support, which seems to be what you want here, is to just get an empty reference by id for an object. Being empty means, that it has no data i.e. all null values. The dirty tracking assumes the initial state is all null then. You can simply map a request payload onto this object, if a value is null, it won't recognize it as being changed, thus ignore it. If anything non-null was set, it determines that such a field is dirty and on flush, only flushes dirty values.

I haven't tried it myself yet, but you could do something like this

// Create reference for the id, the object is empty i.e. all null except for the id
CustomerDTO dto = entityViewManager.getReference(CustomerDTO.class, someId);
// Map the payload on the DTO which will call setFoo(null) but that's ok, because that isn't considered being dirty
jsonMapper.map(requestPayload, dto);
// Flush dirty changes i.e. non-null values
entityViewManager.update(entityManager, dto);

The executed update query when using the PARTIAL flush mode will only contain set clauses for properties with non-null values. The DTO would look like this

@EntityView(Customer.class)
@UpdatableEntityView(mode = FlushMode.PARTIAL)
public interface CustomerDTO {
  @IdMapping Integer getId();
  String getName();
  void setName(String name);
  String getDesc();
  void setDesc(String desc);
}

If nothing is dirty, it won't even execute a query.