Why does hibernate need to save the parent when saving the child and cause a OptimisticLockException even if there no change to the parent?

Viewed 896

We are trying to save many child in a short amount of time and hibernate keep giving OptimisticLockException. Here a simple exemple of that case:

University
id
name
audit_version

Student 
id
name 
university_id
audit_version

Where university_id can be null.

The java object look like:

@Entity
@Table(name = "university")
@DynamicUpdate
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class University {

    @Id
    @SequenceGenerator(name = "university_id_sequence_generator", sequenceName = "university_id_sequence", allocationSize = 1)
    @GeneratedValue(strategy = SEQUENCE, generator = "university_id_sequence_generator")
    @EqualsAndHashCode.Exclude
    private Long id;

    @Column(name = "name")
    private String name;
    @Version
    @Column(name = "audit_version")
    @EqualsAndHashCode.Exclude
    private Long auditVersion;

    @OptimisticLock(excluded = true)
    @OneToMany(mappedBy = "student")
    @ToString.Exclude
    private List<Student> student;
}

@Entity
@Table(name = "student")
@DynamicUpdate
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class Student {

    @Id
    @SequenceGenerator(name = "student_id_sequence_generator", sequenceName = "student_id_sequence", allocationSize = 1)
    @GeneratedValue(strategy = SEQUENCE, generator = "student_id_sequence_generator")
    @EqualsAndHashCode.Exclude
    private Long id;

    @Column(name = "name")
    private String name;

    @Version
    @Column(name = "audit_version")
    @EqualsAndHashCode.Exclude
    private Long auditVersion;

    @OptimisticLock(excluded = true)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "university_id")
    @ToString.Exclude
    private University university;
}

It seem when we assign university and then save Student, if we do more than 4 in a short amount of time we will get the OptimisticLockException. It seem hibernate is creating update version on the University table even though the University didn't change at the db level.

UPDATE: code that save the student

    Optional<University> universityInDB = universidyRepository.findById(universtityId);
    universityInDB.ifPresent(university -> student.setUniversity(university);
    Optional<Student> optionalExistingStudent = studentRepository.findById(student);
    if (optionalExistingStudent.isPresent()) {
        Student existingStudent = optionalExistingStudent.get();
        if (!student.equals(existingStudent)) {
            copyContentProperties(student, existingStudent);
            studentToReturn = studentRepository.save(existingStudent);
        } else {
            studentToReturn = existingStudent;
        }
    } else {
        studentToReturn = studentRepository.save(student);
    }

private static final String[] IGNORE_PROPERTIES = {"id", "createdOn", "updatedOn", "auditVersion"};
public void copyContentProperties(Object source, Object target) {
    BeanUtils.copyProperties(source, target, Arrays.asList(IGNORE_PROPERTIES)));
}

We tried the following

@OptimisticLock(excluded = true) Doesn't work, still give the optimistic lock exception.

@JoinColumn(name = "university_id", updatable=false) only work on a update since we don't save on the update

@JoinColumn(name = "university_id", insertable=false) work but don't save the relation and university_id is always null

Change the Cascade behaviour. The only one value that seem to made sense was Cascade.DETACH, but give a org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing.

Other solution we though of but are not sure what to pick

  1. Give the client a 409 (Conflict) error

After the 409 the client must retry his post. for a object sent via the queue the queue will retry that entry later. We don't want our client to manage this error

  1. Retry after a OptimisticLockException

It's not clean since when the entry come from the queue we already doing it but might be the best solution so far.

  1. Make the parent owner of the relationship

This might be fine if there are not a big number of relation, but we have case that might go in the 100 even in the 1000, which will make the object to big to be sent on a queue or via a Rest call.

  1. Pessimistic Lock

Our whole db is currently in optimisticLocking and we managed to prevent these case of optimisticLocking so far, we don't want to change our whole locking strategy just because of this case. Maybe force pessimistic locking for that subset of the model but I haven't look if it can be done.

2 Answers

It does NOT need it unless you need it. Do this:

    University universityProxy = universidyRepository.getOne(universityId);
    student.setUniversity(universityProxy);

In order to assign a University you don't have to load a University entity into the context. Because technically, you just need to save a student record with a proper foreign key (university_id). So when you have a university_id, you can create a Hibernate proxy using the repository method getOne().


Explanation

Hibernate is pretty complex under the hood. **When you load an entity to the context, it creates a snapshot copy of its fields and keeps track if you change any of it**. It does much more... So I guess this solution is the simplest one and it should help (unless you change the `university` object somewhere else in the scope of the same session). It's hard to say when other parts are hidden.

Potential issues

  • wrong @OneToMany mapping
    @OneToMany(mappedBy = "student") // should be (mappedBy = "university")
    @ToString.Exclude
    private List<Student> student;
  • the collection should be initialized. Hibernate uses it's own impls of collections, and you should not set fields manually. Only call methods like add() or remove(), or clear()
    private List<Student> student; // should be ... = new ArrayList<>();

*overall some places are not clear, like studentRepository.findById(student);. So if you want to have a correct answer it's better to be clear in your question.

If you enable your query logs from Hibernate, it would be worthwhile to see the queries that your ORM is performing. You'll likely realize that your ORM is doing too much.

In your application properties or config file enable hibernate.show_sql=true

I wouldn't be surprised if your single update to a Student becomes an update to a University which becomes an update to all of its containing Students. Everything gets a version bump.

ORM and entity mappings are for strategically retrieving data. They should not be used to actually define object relationships.

You'll want to visit strategies and design your entities based on how they are used in their REST endpoints.

You specified in your question that you are trying to save a Student but you're noticing that the University also gets updated along with every Student update.

Likely there would never be a time when a Student should ever update a University

Keep your entities lean!

You can structure your entity in such a way that supports this unidirectional relationship. I removed some of the annotation just to demonstrate the structure. You will want to keep in mind that when creating entities, you are writing them for how they are retrieved...

public class University {

    @Id
    private Long id;
    private String name;
    private Long auditVersion;
    @OneToMany
    private List<Student> student;
}

public class Student {

    @Id
    private Long id;
    private String name;
    private Long auditVersion;
    private Long universityId;

}

This will ensure that updates to the student remains targeted and clean. You are simply assigning a university id to the student therefore establishing that relationship.

You typically want to respect LockExceptions. Retrying upon a LockException is simply bullying your database into submission and will cause more headaches as your application scales.

You always have the option to work with lean entities and create custom response or message objects that would zip the results together.

ORMs are not to be used to create shortcuts

The performance consequence of a SELECT on an indexed/foreign key is roughly the same cost of grabbing them joined... you only introduce a little extra network latency. A second trip to the database is not always a bad idea. (Often times, this is exactly how Hibernate fetches your entities)

You won't have to write queries, but you will still need to understand the retrieval and update strategies.

You're sacrificing database performance and introducing complexity for a convenient .getChild() method. You'll find that you resolve more performance/locking issues by removing annotations, not adding them.

Related