Josef Josef - 2 months ago 13
Java Question

Spring Boot method fails when I add @Transactional

edit:

Maybe this description is too complicated. I try to summarize simpler what I want:
I just want one MySQL transaction for the whole method execution. Just like if I would manually execute

START TRANSACTION
at the beginning of the method in MySQL and at the end
COMMIT
. On any error, it should
ROLLBACK
and throw the error.

Either all changes during the method execution are persisted, or none.

Details:

I have a method that has to change several objects in a database which depend on each other.
Depending on the data, multiple objects in the database are created/deleted which all have to be consistent.
I have the code in place and tests, and it works.

Of course, if this method is used in multiple threads or something else changes the database while the method runs, everything will break.

I need to have a single database transaction around the data, so that either all of the changes are done or none.

From what I gathered, it seams that @Transactional (org.springframework.transaction.annotation.Transactional) should do that, but once I add this, the method doesn't work anymore.

Here is some pseudocode how my method looks like:

@RestController
public class SomeAPI {
private SomeCrudRepository repository;


@Autowired
public SomeAPI(SomeCrudRepository repository) {
Assert.notNull(repository);
this.repository = repository;
}

@RequestMapping(value = "/complicated-change-something", method = RequestMethod.POST)
@Transactional
public Thing changeData(Thing someEntry, SomeJson changes) throws JsonProcessingException {
ThingEntity metadataEntity = repository.getMetadata(someEntry.getEntityID(), someEntry.getSomethingElse());

if (cond1) {
if (cond2) {
//get all db entries that might change
List<ThingEntity> metadataEntries = repository.getThings(someEntry.getEntityID(), something);

ThingEntity entityBefore = metadataEntries.get(0);

ThingEntityKey entityBeforeKey = new ThingEntityKey(entityBefore.getPk());

entityBefore.setChangedData(data);

repository.delete(entityBeforeKey);

entityBefore = repository.save(entityBefore);

}

//set current entry
ThingEntityKey metadataEntityKey = new ThingEntityKey(metadataEntity.getPk());
metadataEntity.setChangedData(data);

repository.delete(metadataEntityKey);
metadataEntity = repository.save(metadataEntity);
}
...
...

return metadataEntity.toThing();
}

}


So there are some
repository.delete()
and
repository.save()
calls.
Without the @Transactional, the result in the DB is correct, if no concurrent access.

With the
@Transactional
, all the entries deleted by
repository.delete()
are gone but nothing stored with
repository.save()
is there!
If I remove all the
repository.delete()
, nothing in the DB is changed (probably because a primary key in the DB would collide, which is why I delete them before. But there is no error thrown)

Am I doing something wrong here?
In any case, no error is ever thrown, just the tests fail.

Answer

This happens because you delete an entry from the database (using its key) and then try to save the object that refers to that entry.

You where right when you found out, that you cannot edit the data of the key of an object. You have to delete the entity in the database and insert a new one. But after you delete a entity, you cannot use the Java object referring to that entity anymore.

So what you have to do is first create a copy of the Java object (using a copy constructor or clone, for example), then delete the entity from the database and insert the new object.

Sadly, Spring Boot has no helpful error messages if you reuse an object referring to a deleted entity.

Here is the working code:

@RestController
public class SomeAPI {
    private SomeCrudRepository repository;


    @Autowired
    public SomeAPI(SomeCrudRepository repository) {
        Assert.notNull(repository);
        this.repository = repository;
    }

    @RequestMapping(value = "/complicated-change-something", method = RequestMethod.POST)
    @Transactional
    public Thing changeData(Thing someEntry, SomeJson changes) throws JsonProcessingException {
        ThingEntity metadataEntity = repository.getMetadata(someEntry.getEntityID(), someEntry.getSomethingElse());

        if (cond1) {
            if (cond2) {
                //get all db entries that might change
                List<ThingEntity> metadataEntries = repository.getThings(someEntry.getEntityID(), something);

                   ThingEntity entityBefore = metadataEntries.get(0);

                    ThingEntity newEntity = new ThingEntity(entityBefore);

                    newEntity.setChangedData(data);

                    repository.delete(entityBefore);

                    entityBefore = repository.save(newEntity);

            }

            //set current entry
            ThingEntity newEntity = new ThingEntity(metadataEntity);
            newEntity.setChangedData(data);

            repository.delete(metadataEntity);
            metadataEntity = repository.save(newEntity);
        }
...
...

        return metadataEntity.toThing();
    }

}

As mentioned in the comments, please remove the logic from your REST controller and put it in a separate class.

Comments