greyfox
greyfox

Reputation: 6606

JPA PersistentObjectException: detached entity passed to persist

I have having issues trying to do a batch insert operation using Spring Data JPA with Hibernate as the JPA provider.

I have the following method in my service. This is where the exception is being thrown.

    @Transactional
    private void saveAppointments() {

        // create StageFile reference object
        StageFile file = new StageFile();
        file.setFileName("3312_APPOINTMENT.API");
        file.setDeleteFlag('N');
        file.setInstitution(institution);

        for (StageAppointment appointment : appointments) {
            appointment.setStageFile(file);
            stageAppointmentRepository.save(appointment);
        }
    }

    @Transactional
    private void saveDepartments() {

        // create StageFile reference object
        StageFile file = new StageFile();
        file.setFileName("3312_DEPARTMENT.API");
        file.setDeleteFlag('N');
        file.setInstitution(institution);

        for (StageDepartment department : departments) {
            department.setStageFile(file);
            stageDepartmentRepository.save(department);
        }
    }

The institution is an instance variable and fetched ahead of time.

Institution institution = institutionRepository.findByActCode(3312);

I also have the Entity set to cascade PERSIST and MERGE.

@Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "stgAppointmentSeq")
    @SequenceGenerator(name = "stgAppointmentSeq", sequenceName = "T_STG_APPOINTMENT_SEQ", allocationSize = 50)
    @Column(name = "ID")
    private Long id;

    @ManyToOne(cascade = {CascadeType.PERSIST,CascadeType.MERGE}, fetch = FetchType.EAGER, optional = false)
    @JoinColumn(name = "FILE_ID", referencedColumnName = "ID")
    private StageFile stageFile;

    @ManyToOne(cascade = {CascadeType.PERSIST,CascadeType.MERGE}, fetch = FetchType.EAGER, optional = false)
    @JoinColumn(name = "STATUS_ID", referencedColumnName = "ID")
    private StageStatus stageStatus;

What am I doing wrong?

Also I'm sure the answer to this question is Yes, but when I persist an Entity that has a required foreign key reference do I have to save the full associated object or just the ID? Seems counter to the purpose of JPA though.

Update:

Per comments I updated the code to perform everything within the single transaction but it made no difference.

@Transactional
private void saveAppointments() {

    Institution institution = institutionRepository.findByActCode(3312);
    StageStatus stageStatus = stageStatusRepository.findOne(1L);

    // create StageFile reference object
    StageFile file = new StageFile();
    file.setFileName("3312_APPOINTMENT.API");
    file.setDeleteFlag('N');
    file.setInstitution(institution);

    for (StageAppointment appointment : appointments) {
        appointment.setStageFile(file);
        appointment.setStageStatus(stageStatus);
        stageAppointmentRepository.save(appointment);
    }
}

UPDATE 2:

Why does this code work

    @Transactional
    private void saveUsingTransaction() {

        Institution institution = institutionRepository.findByActCode(3312);
        StageStatus status = stageStatusRepository.findOne(1L);

        StageFile file =  new StageFile();
        file.setDeleteFlag('N');
        file.setFileName("3312_DIRECTORY.API");
        file.setInstitution(institution);

        StageDirectory directory = new StageDirectory();
        directory.setLocalId("11111111111111111");
        directory.setFirstName("Joe");
        directory.setLastName("Joe");
        directory.setPrimaryEmail("[email protected]");
        directory.setStageFile(file);
        directory.setStageStatus(status);

        stageDirectoryRepository.save(directory);
    }

And this code does not

@Transactional
private void savePassingDirectory(StageDirectory directory) {

    Institution institution = institutionRepository.findByActCode(3312);
    StageStatus stageStatus = stageStatusRepository.findOne(1L);

    // create StageFile reference object
    StageFile file = new StageFile();
    file.setFileName("3312_DIRECTORY.API");
    file.setInstitution(institution);
    file.setDeleteFlag('N');

    directory.setStageFile(file);
    directory.setStageStatus(stageStatus);
    stageDirectoryRepository.save(directory);
}

Upvotes: 0

Views: 2272

Answers (2)

geert3
geert3

Reputation: 7321

@Transactional only works for method calls originating from an external object into the managed bean.

Intra-class method calls (and by definition private methods can only be called that way) are not intercepted by the container hence for these the annotation @Transactional has no effect. So I think you need to check where your transaction boundaries actually are.

My guess is that saveAppointments currently runs outside of any transaction. Your institutionRepository.findByActCode() is likely a correct call to a managed bean so that uses a transaction. When that method returns you again do stuff outside of any transaction to finally enter a new transaction when you call stageAppointmentRepository.save().

First you have to make sure the method saveAppointments itself actually runs in a transaction (e.g. make it public and call it directly from whereever you @Inject it), then you have to make sure the subsequent method calls reuse that same transaction, not start a new one (i.e. not have @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW))

Upvotes: 3

Hannes
Hannes

Reputation: 2073

Funny that everytime someone uses an entity manager for some time, this problem pops up...

So you got your database hooked up to your persistence layer and now you are wondering, why your enities break if they leave their ecosystem.

The reason for that is simply because once an entity has left its bounds, i.e. the transactional context, it gets detached from the entity manager. If you want to store its changes, you have to attach it again.

How this is done you can read at What is the proper way to re-attach detached objects in Hibernate?

In your case appointments got detached. So you have to check in your .save(...) method, if the passed object is in the current entity manager's context, and if not, re-attach it.

Upvotes: 1

Related Questions