1
votes

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

2

2 Answers

3
votes

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.

1
votes

When you call methods of dao outside of dao, the entity is detached. To keep the session, @Transactional annotation should be defined on the method that is call your dao methods.

for example)

public class CarService {

    @Transactional   <-- {here}
    public void doSomething(){
        Car car = sqlDao.getOne(1);
        sqlDao.update(car);
        sqlDao.delete(car);
    }
}