Search code examples
hibernatejpaspring-boothql

Spring boot JPA doesn't attach entities to the session


I have a project that uses Spring Boot JPA (spring-boot-starter-data-jpa dependency) which uses Hibernate as a JPA implementation.

I have my container auto configured (@EnableAutoConfiguration) and I use an EntityManager for CRUD operations.

Issue: I load my entities at startup via HQL queries using this EntityManager, but when I want to edit or delete any of them I get the following error

org.springframework.dao.InvalidDataAccessApiUsageException: Removing a detached instance com.phistory.data.model.car.Car#2; nested exception is java.lang.IllegalArgumentException: Removing a detached instance com.phistory.data.model.car.Car#2

org.springframework.dao.InvalidDataAccessApiUsageException: Entity not managed; nested exception is java.lang.IllegalArgumentException: Entity not managed

Libraries:

  • spring-boot-starter-data-jpa 1.4.4.RELEASE (Hibernate 5.0.11.Final)

Main:

@SpringBootApplication
@EnableAutoConfiguration
@Slf4j
public class Main {    
    public static void main(String[] args) {
        try {
            SpringApplication.run(Main.class, args);
        } catch (Exception e) {
            log.error(e.toString(), e);
        }
    }

Database config (no beans explicitly declared, EntityManager gets automatically autowired):

@Configuration
@ComponentScan("com.phistory.data.dao")
@EntityScan("com.phistory.data.model")
@EnableTransactionManagement
@PersistenceUnit
public class SqlDatabaseConfig {
}

DAO

@Transactional
@Repository
public class SqlCarDAOImpl extends SqlDAOImpl<Car, Long> implements SqlCarDAO {

    @Autowired
    public SqlCarDAOImpl(EntityManager entityManager) {
        super(entityManager);
    }

    @Override
    public List<Car> getAll() {
        return super.getEntityManager()
                    .createQuery("FROM Car AS car")
                    .getResultList();
    }
}

Parent DAO

@Transactional
@Repository
@Slf4j
@NoArgsConstructor
public abstract class SqlDAOImpl<TYPE extends GenericEntity, IDENTIFIER> implements SqlDAO<TYPE, IDENTIFIER> {

    @Getter
    @PersistenceContext
    private EntityManager entityManager;

    public SqlDAOImpl(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    public void saveOrEdit(TYPE entity) {
        if (entity != null) {
            if (entity.getId() == null) {
                log.info("Saving new entity: " + entity.toString());
                this.entityManager.persist(entity);
            } else {
                log.info("Editing entity: " + entity.toString());
                this.entityManager.refresh(entity);
            }
        }
    }

    public void delete(TYPE entity) {
        if (entity != null) {
            log.info("Deleting entity: " + entity.toString());
            this.entityManager.remove(entity);
        }
    }

    public Session getCurrentSession() {
        return this.entityManager.unwrap(Session.class);
    }
}

Why are the entities I load not attached to the Session? Saving a new entity obviously works fine since the entity must not be managed at that point.

Thanks a lot Greetings


Solution

  • Why are you using the EntityManager directly if you actually want to use Spring Data JPA?

    Everything you posted above is available "out of the box":

    Application Starter:

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class Main {
    
        public static void main(String[] args) {
            SpringApplication.run(Main.class, args);
        }
    
    }
    

    Spring Data JPA Repository:

    import org.springframework.data.repository.CrudRepository;
    import org.springframework.stereotype.Repository;
    
    import com.example.model.CollectionParent;
    import java.lang.String;
    import java.util.List;
    
    @Repository
    public interface CarRepository extends CrudRepository<Car, Long> {
         // nearly everything you need comes for free
    }
    

    The CarRepository now offers you the methods findAll() : Iterable<Car> and delete(Car car) out of the box.

    The DAO / Service could look like this:

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    public class CarService {
    
        @Autowired
        private CarRepository carRepository;
    
        @Transactional
        public Iterable<Car> findCars() {
            return carRepository.findAll();
        }
    
        @Transactional
        public void updateCar(final Long carId, ...some other params ...) {
            final Car car = carRepository.findOne(carId);
            car.setXXX ...use some other params here ...
            car.setYYY ...use some other params here ...
        }
    
        @Transactional
        public void updateCar(final Car detachedAndModifiedCar) {
             carRepository.save(detachedAndModifiedCar);
        }
    
        ...
    }
    

    The update is simply realized if you load an Car Entity into the current Persistence Context, modify it and let Hibernate store the changes on flush time, preferable within a database transaction. That can be easily realized with a Service / DAO method annotated with @Transactional.

    A detached Entity can also be passed to the Service, just reattach and save it with carRepository.save(detachedAndModifiedCar).

    Even your saveOrEdit method is just calling entityManager.refresh(entity) in case of an existing entity. That means there is no updating code at all, or did I get it wrong? The Entity will just be updated with the data from the database.