Home > database >  Can't get the updated entity with spring jpa and unique constrain doesn't stop the update
Can't get the updated entity with spring jpa and unique constrain doesn't stop the update

Time:08-16

My situation: a public method is trying to update the entity in the beginning, and at last, it is trying to query the updated entity and send a message via SNS. I got two issues with this.

The first issue is I can get the updated entity if I call the DeviceDao.save() but can't get the updated entity if I call the customized update method updateAsset in the UpdateClass.

The second issue is the code still goes through to the messagePublisher and I can get the updated entity in 'MessagePublisher.doRealthing()' even the UpdateClass.doSth() throw a org.hibernate.exception.ConstraintViolationException(I have an unique constraint on asset id), but eventually, the update is rollbacked.

My question is, why did I get that two issues? For the second issue, How can I avoid it except by querying the asset Id in advance?

Here are my codes.

public interface ExampleDeviceDao extends JpaRepository<Device, UUID>, JpaSpecificationExecutor<Device> {

    @Modifying
    @Query("UPDATE device a SET a.asset = ?1 WHERE a.device = ?2")
    int updateAsset(UUID asset, UUID device);

}

My public service and method:


@Component
public class Service {
    @Autowired
    UpdateClass updateClass;
    @Autowired
    MiddleClass middleClass;
    @Autowired
    MessagePublisher messagePublisher;

    @org.springframework.transaction.annotation.Transactional
    public void updateAsset(UUID deviceId, UUID assetId) {
        updateClass.doSth(deviceId, assetId);
        middleClass.doSth(deviceId, assetId);
        messagePublisher.doSth(deviceId, assetId);
    }
}
public abstract class AbstractClass {

    protected abstract void doRealThing(UUID deviceId, UUID assetId);

    public void doSth(UUID deviceId, UUID assetId) {
        doRealThing(deviceId, assetId);
    }
}

@Component
public class UpdateClass extends AbstractClass{

    @Autowired
    ExampleDeviceDao deviceDao;

    protected void doRealThing(UUID deviceId, UUID assetId) {
        Optional<Device> device = deviceDao.findById(deviceId);
        if (device.isPresent()) {
            device.get().setAsset(assetId);
            /** approach 1:*/
            deviceDao.save(device.get());
            /**
             * approach 2:
             * deviceDao.updateAsset(assetId, deviceId);
             */
        }
    }
}
@Component
public class MiddleClass extends AbstractClass{


    protected void doRealThing(UUID deviceId, UUID assetId) {
        //do other things, not db update or query.
    }
}
@Component
public class MessagePublisher extends AbstractClass{

    @Autowired
    ExampleDeviceDao deviceDao;
    @Autowired
    SNSMessagePublisher snsMessagePublisher;

    protected void doRealThing(UUID deviceId, UUID assetId) {
        Optional<Device> device = deviceDao.findById(deviceId);
        if (device.isPresent()) {
            snsMessagePublisher.publishMessage(device.get());
        }
    }
}

CodePudding user response:

The problem probably is that your first level cache (a.k.a. persistence context) contains the entity which you are executing a DML statement (update device ...) against. Hibernate has no knowledge though, that your DML statement will change the state of an entity in the persistence context and can also not do anything sensible about this. So the changes are executed on the database, but the entity in the persistence context still has the old state. The only sensible things you can do are to refresh the entity (if you need it), or remove it from the persistence context by calling EntityManager.detach().

As far as I understand your second question, your publisher code publishes the SNS message even though the database transaction fails. In this case you will always have a possible issue as the SNS client is not part of the transaction and also can't be.

You can send messages after a transaction completes by

This has the possibility that you might miss sending some messages if the JVM shuts down after a transaction completes but didn't send the SNS message yet, or if the SNS message sending fails for some reason.

To solve such issues, you'd usually use some sort of job queuing that has retry mechanisms. If you are interested in a solution for this, you can take a look at Blaze-Notify which is a tool kit that can be used to implement this efficiently. Instead of sending the SNS message immediately, you persist an entity that represents the message, which will then later be processed asynchronously and sent with a retry mechanism.

CodePudding user response:

For the first issue see the @ChristianBeikov answer.

The second issue has the same reason as the first one. You do

begin of updateAsset() method

device.get().setAsset(assetId); (1)
/** approach 1:*/
deviceDao.save(device.get()); (2)

Optional<Device> device = deviceDao.findById(deviceId); (3)
snsMessagePublisher.publishMessage(device.get()); (4)


end of updateAsset() method (5)

  1. You change device in the memory (persistent context).

  2. You save device to a database. Actually Hibernate does nothing at this point. You can enable SQL logging and see that nothing happens.

  3. You get Device from a database. Hibernate again does nothing with the database here. It just gets it from the memory with the changes you have made at (1) step. Please verify it using SQL logs.

  4. Changed Device is sent to SNS.

  5. The end of @Transactional method. After return from the method Hibernate flushes all changes from the point (1) to the database (see SQL logs) and validation exception happens. But SNS message has already sent with new changes. Database has old changes because the transaction was rolled back.

Possible solution

You can flush database changes before publishing SNS message

deviceDao.save(device.get());
deviceDao.flushChanges();
snsMessagePublisher.publishMessage(device.get());
  • Related