Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to properly handle the version field in a JPA entity with Spring Data

I have a very simple domain model, one entity which has a version field in order to use the optimistic locking capabilities provided from JPA (api v2.2). The implementation i use is Hibernate v5.3.10.Final.

@Entity
@Data
public class Activity {

    @Id
    @SequenceGenerator(name = "gen", sequenceName = "gen_seq", allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "gen")
    private Long id;

    @Column
    private String state;

    @Version
    private int version;
}

Then there are simple operations with that entity, like for example temporarily changing its stage:

@Transactional
public Activity startProgress(Long id) {
    var entity = activityRepository.findById(id).orElseThrow(RuntimeException::new);

    if (entity.getState() == "this") { // or that, or else, etc
        // throw some exceptions in some cases
    }

    entity.setState("IN_PROGRESS");

    return activityRepository.saveAndFlush(entity);
}

The outcome of this, what i'd like to achieve, is to have a way to update the entity, and that update should also increment the version. If there is a mismatch, i'd expect a ObjectOptimisticLockingFailureException or OptimisticLockingException to be thrown. I'd also like to have the updated value of the version field in the object, as i'm returning it to the client.

Several options which i've tried:

  • simply call save - the version field gets updated, but the new value is not returned and the client gets the old one, which makes the next request hit the locking exception
  • call saveAndFlush - in this case i get the update statement on the client executed twice, which weirdly returns version X which in the database the version is X+1. Then again the next client request hits the locking exception.
  • Create a @Modifying query, mark it as clear automatically (to auto flush the change) and use the hql create versioned syntax. Then i get the following query executed: update activities set version=version+1, state=? where id=?, but this doesn't seem to do the optimistic lock check (where version = :version_from_entity). I also don't think it will raise the appropriate exception.

So, in the end what i'd like to achieve is quite simple and i don't think i have to write it on my own - have a way to update one or more fields on a versioned entity, rely on JPA for the optimistic locking and get the latest version so the client can do further operations with the entity. I read quite a lot of similar issues but most use the entity manager directly, which is not what i'm striving for.

like image 227
Milan Milanov Avatar asked Nov 07 '22 14:11

Milan Milanov


1 Answers

Your code looks pretty correct so it's difficult to say where is the problem...

I've created a simple working example with Spring Data JPA and Optimistic locking. I hope it will be helpful for you to solve the issue.

Create an entity:

@Transactional
public Model create(Model model) {
    return modelRepo.save(model);
}

Update the entity:

@Transactional
public Optional<Model> update(int id, Model source) {
    return modelRepo
            .findById(id)
            .map(model -> modelMapper.apply(model, source));
}

Get entities:

@Transactional(readOnly = true)
public List<Model> getAll() {
    return modelRepo.findAll();
}

Just clone the project, run it (for example, with mvn spring-boot:run), and check the log:

15:44:35.905  INFO 2800 --- [ main] i.g.c.d.Application : [i] Creating...
15:44:35.925  INFO 2800 --- [ main] jdbc.sqltiming      : batching 1 statements:
1:  insert into model (name, version, id) values ('model', 0, 1); {executed in 1 msec}
15:44:35.930  INFO 2800 --- [ main] i.g.c.d.Application : [i] Updating...
15:44:35.934  INFO 2800 --- [ main] jdbc.sqltiming      : select model0_.id as id1_0_0_, model0_.name as name2_0_0_, model0_.version as version3_0_0_ from model model0_ where model0_.id=1; {executed in 0 msec}
15:44:35.939  INFO 2800 --- [ main] jdbc.resultsettable : 
|---------|------|--------|
|id       |name  |version |
|---------|------|--------|
|[unread] |model |0       |
|---------|------|--------|
15:44:35.944  INFO 2800 --- [ main] jdbc.sqltiming      : batching 1 statements:
1:  update model set name='model_updated', version=1 where id=1 and version=0; {executed in 1 msec}
15:44:36.010  INFO 2800 --- [ main] i.g.c.d.Application : [i] Getting...
15:44:36.015  INFO 2800 --- [ main] jdbc.sqltiming      : select model0_.id as id1_0_, model0_.name as name2_0_, model0_.version as version3_0_ from model model0_; {executed in 0 msec}
15:44:36.017  INFO 2800 --- [ main] jdbc.resultsettable : 
|---|--------------|--------|
|id |name          |version |
|---|--------------|--------|
|1  |model_updated |1       |
|---|--------------|--------|

A few recomendations, if you don't mind )

Use an object instead of a simple type for version property (eg Integer, Long, etc). It's useful, for example, when you create entity identifiers yourself. In this case, Spring Data/Hibernate check that version is null and won't do an extra select query to the database.

When you update an entity you don't have to explicitly call save methods of the repo - since you are in a transaction the Hibernate update the changed entity itself.

like image 53
Cepr0 Avatar answered Nov 12 '22 16:11

Cepr0