Post

Spring Data JPA - flush (SQL Error 1062, SQLState 23000)

Spring Data JPA - flush (SQL Error 1062, SQLState 23000)

This post was migrated from Tistory. You can find the original here.

Example)

Vehicle table

   
idbigintPrimary key (Vehicle ID)
license_platevarchar(255)Vehicle’s license plate number
brandvarchar(255)Manufacturer of the vehicle
modelvarchar(255)Vehicle model
yearintYear of manufacture

Engine table

   
idbigintPrimary key (Engine ID)
engine_numbervarchar(255)Engine serial number
engine_modelvarchar(255)Engine model
statusenum(‘error’, ‘running’, ‘stopped’)Current engine status
vehicle_idbigintForeign key referencing Vehicle ID

Engine and Vehicle have a one-to-one relationship, and Engine owns the foreign key.

Say the Engine table has the following data.

     
idengine_numberengine_modelstatusvehicle_id
1xxxx-xxxxAQ31
2xxxx-xxxxDW22

Now, suppose we want to change Engine #1 (id 1) so its vehicle becomes Vehicle #2 (id 2).

Since Engine and Vehicle have a one-to-one relationship, the same vehicle_id can’t exist on more than one row.

So the order of operations needs to be:

  1. Null out vehicle_id on Engine #2, which currently holds the relationship with Vehicle #2 (breaking the existing relationship first)

  2. Update Engine #1’s vehicle_id to 2

That’s the plan.

Spring Data JPA - flush

The Problem

1
2
3
4
5
6
7
8
9
10
11
12
    @Transactional
    public void patchEngine(PatchVehicleDto dto) {
        Engine engine = engineRepository.findById(dto.getEngineId()).orElseThrow(() -> new IllegalArgumentException("Invalid engineId"));

        Vehicle vehicle = vehicleRepository.findById(dto.getVehicleId()).orElseThrow(() -> new IllegalArgumentException("Invalid vehicleId"));
        engineRepository.findByVehicle(vehicle).ifPresent((v) -> {
            v.setVehicle(null);
            engineRepository.save(v);
        });
        engine.setVehicle(vehicle);
        engineRepository.save(engine);
    }

Implementing the flow above with JPA produces this error: SQL Error: 1062, SQLState: 23000, Duplicate entry ‘2’ for key ~~.

The Fix

1
2
3
4
5
6
7
8
9
10
11
12
    @Transactional
    public void patchEngine(PatchVehicleDto dto) {
        Engine engine = engineRepository.findById(dto.getEngineId()).orElseThrow(() -> new IllegalArgumentException("Invalid engineId"));

        Vehicle vehicle = vehicleRepository.findById(dto.getVehicleId()).orElseThrow(() -> new IllegalArgumentException("Invalid vehicleId"));
        engineRepository.findByVehicle(vehicle).ifPresent((v) -> {
            v.setVehicle(null);
            engineRepository.saveAndFlush(v); // first update
        });
        engine.setVehicle(vehicle);
        engineRepository.save(engine); // second update
    }

Using saveAndFlush (or flush) like this makes it work as intended, without the error.

repository.flush() synchronizes the persistence context’s state with the DB, but nothing is finalized until the transaction actually ends.

In other words, it does not issue a COMMIT.

Investigating the question

Here’s how I approached the problem above.

Either the first update query wasn’t being sent at all,

or the first update query was sent but hadn’t been reflected in the DB yet when the second query went out.

1
2
3
4
5
6
7
8
    @Transactional
    public void test() {
        Engine engine = new Engine();
        Vehicle vehicle = new Vehicle();
        engine.setVehicle(vehicle);
        vehicleRepository.save(vehicle);  // #1
        engineRepository.save(engine);  // #2
    }

For example, take code like this.

If it runs in order #1 then #2, you get 2 inserts.

If it runs in order #2 then #1, you get 2 inserts and 1 update.

Either way, the vehicle insert query has to go out before the COMMIT can succeed.

Likewise, in the code from the problem above, the engine update query has to go out first for the COMMIT to succeed,

yet insert works fine without a flush, while update requires one.

What’s the difference?

Is it that updates on the same table simply don’t get applied without a flush?

References

2023.07.13 - [Spring/JPA] - Spring) JPA persistence / transactions / entity state (23-07-08)

I wrote this earlier while studying JPA persistence,

and it turns out that keeping good notes on the blog really does pay off in all sorts of ways.

This post is licensed under CC BY-NC 4.0 by the author.