45

I am using spring boot and hibernate over jpa. I am using JpaRepository interface to implement my repositories. As with following UserRepository

public interface UserRepository extends JpaRepository<User, Long> {
}

I want to achieve following

  1. Load a User entity.
  2. Change the state of entity object e.g. user.setName("foo")
  3. Do an external system webservice call. Save the call result in DB
  4. Only on successful response of this webservice call, save the new state of user in repository.

All above steps are not happening in one transaction i.e. the external service call is out of transaction.

When I save my webservice result in DB via its repository, my changes in User entity are also saved. As per my understanding this is due to the flushing of underlaying persistence context at step # 3. After some google, I think I can achieve my purpose, if I can detach my user entity at step one and reattach it at step 4. Please confirm if my understanding is correct and how I can achieve this? There is not method in JpaRepository interface to detach an entity.

Following is the code to illustrate

public void updateUser(int id, String name, int changeReqId){
    User mUser = userRepository.findOne(id); //1
    mUser.setName(name); //2

    ChangeRequest cr = changeRequestRepository.findOne(changeReqId);
    ChangeResponse rs = userWebService.updateDetails(mUser); //3

    if(rs.isAccepted()){
        userRepository.saveAndFlush(mUser); //4
    }

    cr.setResponseCode(rs.getCode());
    changeRequestRepository.saveAndFlush(cr); //this call also saves the changes at step 2
}

Thanks

Jonik
  • 80,077
  • 70
  • 264
  • 372
amique
  • 2,176
  • 7
  • 34
  • 53

4 Answers4

74

If you are using JPA 2.0, you can use EntityManager#detach() to detach a single entity from persistence context. Also, Hibernate has a Session#evict() which serves the same purpose.

Since JpaRepository doesn't provide this functionality itself, you can add a custom implementation to it, something like this

public interface UserRepositoryCustom {
    ...
   void detachUser(User u);
    ...
}

public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
    ...
}

@Repository
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
    ...
    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public void detachUser(User u) {
        entityManager.detach(u);
    }
    ...
}

I haven't tried this code, but you should be able to make it work. You might even try to get a hold on EntityManager in your service class (where updateUser() is) with @PersistenceContext, and avoid the hustle of adding custom implementation to repository.

Jonik
  • 80,077
  • 70
  • 264
  • 372
Predrag Maric
  • 23,938
  • 5
  • 52
  • 68
12

Using custom implementation as @Predrag Maric suggest is clearly the right answer for this question. However, I find doing detach in Service layer is much better as normally it knows that the entity should be detached or not.

Just wire it with @PersistenceContext in Service.

@Service
class ConsumerServiceImpl {

    @PersistenceContext
    private EntityManager entityManager
...

    entityManager.detach(en)

Chayne P. S.
  • 1,558
  • 12
  • 17
7

entityManager.clear() will disconnect all the JPA objects, so that might not be an appropriate solution in all the cases, if you have other objects you do plan to keep connected.

clear

/**
 * Clear the persistence context, causing all managed
 * entities to become detached. Changes made to entities that
 * have not been flushed to the database will not be
 * persisted.
 */
public void clear();

entityManager.detach(entity); Remove the given entity from the persistence context

detach

/**
 * Remove the given entity from the persistence context, causing
 * a managed entity to become detached.  Unflushed changes made
 * to the entity if any (including removal of the entity),
 * will not be synchronized to the database.  Entities which
 * previously referenced the detached entity will continue to
 * reference it.
 * @param entity  entity instance
 * @throws IllegalArgumentException if the instance is not an
 *         entity
 * @since Java Persistence 2.0
 */
public void detach(Object entity);
Xstian
  • 8,184
  • 10
  • 42
  • 72
2

While the accepted, Predrag Maric's answer is correct and will work, I found it not really flexible when you want to add such a feature to all your repository interfaces, hence I'm using following approach, with my custom repository factory bean:

  1. Create intermediate interface for detach feature:
@NoRepositoryBean // this annotation is important here if the package you are putting this interface in is scanned for repositories (is in basePackage)
public interface DetachableJpaRepository<T, TID> extends JpaRepository<T, TID> { // you can also extend other repositories here, like JpaSpecificationExecutor
    void detach(T entity);
}
  1. Create implementation of the intermediate interface:
public class DetachableJpaRepositoryImpl<T, TID> extends SimpleJpaRepository<T, TID> implements DetachableJpaRepository<T, TID> {
    private final EntityManager entityManager;

    public DetachableJpaRepositoryImpl(Class<T> domainClass, EntityManager entityManager) {
        super(domainClass, entityManager);
        this.entityManager = entityManager;
    }

    @Override
    public void detach(T entity) {
        entityManager.detach(entity);
    }
}
  1. Create your custom repository factory:
public class DetachableJpaRepositoryFactory<T, TID> extends JpaRepositoryFactory {
    public DetachableJpaRepositoryFactory(EntityManager entityManager) {
        super(entityManager);
    }

    @Override
    protected JpaRepositoryImplementation<?, ?> getTargetRepository(RepositoryInformation information, EntityManager entityManager) {
        return new DetachableJpaRepositoryImpl<T, TID>((Class<T>) information.getDomainType(), entityManager);
    }

    @Override
    protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
        return DetachableJpaRepository.class;
    }
}
  1. Create custom repository factory bean, to declare usage of factory you created above:
public class DetachableJpaRepositoryFactoryBean<R extends JpaRepositoryImplementation<T, TID>, T, TID> extends JpaRepositoryFactoryBean<R, T, TID> {
    public DetachableJpaRepositoryFactoryBean(Class<? extends R> repositoryInterface) {
        super(repositoryInterface);
    }

    @Override
    protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
        return new DetachableJpaRepositoryFactory<T, TID>(entityManager);
    }
}
  1. Declare the custom factory bean to spring configuration:
@EnableJpaRepositories(repositoryFactoryBeanClass = DetachableJpaRepositoryFactoryBean.class)
public class MyApplication {...}
  1. Now you can use new intermediate interface on your repositories:
@Repository
public interface UserRepository extends DetachableJpaRepository<User, Long> {
}
  1. And detach method is available for you on these repositories:
@Autowired
private UserRepository userRepository;
...
userRepository.detach(user);

I found this approach very flexible, also to add other features to your custom repositories.

Additionaly I do not agree with Chayne P. S. answer, imo you should not use entity manager in service layer, this is dao layer responsibility to manage the entities state, what Chayne P. S proposes is bad design imo, service layer should not be aware of entity manager.

bladekp
  • 1,529
  • 22
  • 29
  • 2
    I agree with you that the dao-layer implementation details should not pulled up into the service layer. However, your solution does a lot of work just to break encapsulation in the same way by pulling up detach(Entity). – David Avendasora Jul 15 '22 at 07:56
  • This seems the most proper one. Thanks! – Donnie Jun 26 '23 at 09:10