1

I have a project using spring-boot-starter-data-jpa version 2.2.6.RELEASE with an aOracle 12c database. I have a version field in my entity annotated with javax.persistance @Version annotation. When I save an entity for the first time, the query returns a version of 0, but in the database I can see it has been set to 1. It is like the transaction ends before the updated version is returned.

I've tried with both CrudRepository's save() method and JpaRepository's saveAndFlush() method, but neither work. Note, subsequent updates to the entity do return the correct version. This problem only happens when creating a new record.

When I use the EntityManager directly with saveAndRefresh(), it works as it should, but I'd like to avoid doing that if possible. Can someone please help?

UPDATE

Here are the entity, repository and service classes. I've tried with JpaRepository and saveAndFlush() too, but the outcome is the same:

@Getter
@Setter
@NoArgsConstructor
@ToString
@EqualsAndHashCode
@Entity
@Table(name = "TD_INCIDENTS")
public class Incident {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "INCIDENT_ID", nullable = false)
    private Long incidentId;

    @Column(name = "INCIDENT_STATUS")
    private Integer statusCode;

    @Version
    @Column(name = "INCIDENT_VER_NUM")
    private Long version;
}

@Repository
public interface IncidentRepository extends CrudRepository<Incident, Long> {
}

@Service
public class IncidentServiceImpl implements IncidentService {
    
    private final IncidentRepository incidentRepository;

    public IncidentServiceImpl(IncidentRepository incidentRepository) 
    {
        this.caseRepository = caseRepository;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Incident createIncident(String statusCode) {
        var newIncident = new Incident();
        newIncident.setStatusCode(1);
        // the line below returns an incident with version 0, but in db it is 1
        return incidentRepository.save(newIncident);
    }

    @Override
    @Transactional
    public Incident getIncident(Long incidentId) {
        return incidentRepository.findById(incidentId);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Incident updateIncident(Long incidentId, Integer statusCode, Long version) {
        var incident = this.getIncident(incidentId);
        if (incident != null) {
            if (incident.getVersion().equals(version)) {
                incident.setStatusCode(statusCode);
                // the line below returns an incident with an updated version the same as in the db
                return incidentRepository.save(incident);
            }else {
                throw new OptimisticLockException("Expected versions do not match");
            }
        }
        return null;
    }
}

UPDATE

Here is the log output from the insert:

2020-07-26 13:54:09.675 DEBUG 9140 --- [nio-8080-exec-1] org.hibernate.SQL                        : 
    insert 
    into
        td_incidents
        (incident_id, incident_status, incident_ver_num) 
    values
        (default, ?, ?)
2020-07-26 13:54:09.701 TRACE 9140 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [INTEGER] - [1]
2020-07-26 13:54:09.703 TRACE 9140 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [0]

And here is the log output from the update. Note, I'm sending the version number as 1 in the request, as that's what it is in the db. If I send it as 0, the application will throw the OptimisticLockException

2020-07-26 13:56:29.346 DEBUG 9140 --- [nio-8080-exec-3] org.hibernate.SQL                        : 
    select
        incident0_.incident_id as incident_id1_0_0_,
        incident0_.incident_status as incident_status_2_0_0_,
        incident0_.incident_ver_num as incident_ver_num_3_0_0_ 
    from
        td_incidents incident0_ 
    where
        incident0_.incident_id_id=?
2020-07-26 13:56:29.347 TRACE 9140 --- [nio-8080-exec-3] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1044]
2020-07-26 13:56:29.401 DEBUG 9140 --- [nio-8080-exec-3] org.hibernate.SQL                        : 
    update
        td_incidents 
    set
        incident_status=?,
        incident_ver_num=? 
    where
        incident_id=? 
        and incident_ver_num=?
2020-07-26 13:56:29.402 TRACE 9140 --- [nio-8080-exec-3] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [INTEGER] - [2]
2020-07-26 13:56:29.402 TRACE 9140 --- [nio-8080-exec-3] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [2]
2020-07-26 13:56:29.403 TRACE 9140 --- [nio-8080-exec-3] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [1044]
2020-07-26 13:56:29.404 TRACE 9140 --- [nio-8080-exec-3] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [BIGINT] - [1]
CeeTee
  • 778
  • 1
  • 9
  • 17
  • Is your entity contains manually assigned Id and also can you add the method that calls `JpaRepository's saveAndFlush() `? – Kavithakaran Kanapathippillai Jul 25 '20 at 17:37
  • The ids are autogenerated. I've added the entity and repository classes above and the service class that calls the save/saveAndFlush() methods – CeeTee Jul 26 '20 at 11:36
  • It should return 0 after save that is the correct behaviour. Now the question is who updated it after `save` returned. Can you put a break point in service class at line that calls `incidentRepository.createIncident` line and add this to `application.properties` `spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true` and check the sql sent by hibernate – Kavithakaran Kanapathippillai Jul 26 '20 at 11:53
  • This may be helpful as well: https://stackoverflow.com/questions/2572566/java-jpa-version-annotation. – Konrad Botor Jul 26 '20 at 11:58
  • There is no update statement from JPA it says `binding parameter [2] as [BIGINT] - [1]` issued by JPA for version number and it means it never issued that statement. May be you created the table in oracle manually and have an auto increment on insert or something? Did you create the table manually and what is the table definition? – Kavithakaran Kanapathippillai Jul 26 '20 at 13:41
  • No, someone from the DBA team did it. And I can see the incident_ve_num field is defined to autoincrement. So when 0 gets inserted with the first crate, it is incremnted to 1. But then I don't understand the update behaviour. Because it looks like from the logs hibernate is already incrementing the field to 2. So why doesn't te db then increment it to 3? – CeeTee Jul 26 '20 at 13:44
  • So that is the problem. Hibernate does not expect someone to update a column without it knowing it. It shouldn't have auto increment. I am not familiar with oracle's behaviour and is a separate question. But JPA is behaving consistently in both inserts and updates. I.e it assumes no one else updated that column. – Kavithakaran Kanapathippillai Jul 26 '20 at 13:48
  • I will put an answer, Try that, It may be work and is normally used for database generated fields but not sure if it will work with `@Version` – Kavithakaran Kanapathippillai Jul 26 '20 at 13:53
  • Yes, I agree, this problem is at the database level. Or at least in the way the table is defined in relation to what hibernate expects. I've run manual sql for insert and update. If version is 0 in the query, the db increments it to 1. But if you supply the increment in the update query (e.g. 2), the db is accepting that and doesn't bother incrementing it. If you leave it out, the db will incrment to 2. Thanks for your help @Kavithakaran Kanapathippillai – CeeTee Jul 26 '20 at 13:57
  • No worries, I would be interested in knowing why oracle behaves so, Can you please put a comment here if you find a solution or if you raise a separate oracle question? – Kavithakaran Kanapathippillai Jul 26 '20 at 14:00
  • Thanks @Kavithakaran Kanapathippillai, that worked – CeeTee Jul 26 '20 at 14:15

1 Answers1

1
  • INCIDENT_VER_NUM field should not have autoincrement column but you seems to have an autoincrecment column as it is managed and incremented by JPA

  • If you can't remove autocrement, try this option and it could work. But I have used this option so far other fields that are generated by database not for @Version

    @Version
    @Column(name = "INCIDENT_VER_NUM", insertable = false, updatable = false)
    @org.hibernate.annotations.Generated(value = GenerationTime.ALWAYS)
    private Long version;