How do I update an attribute in Parent entity once children records have been modified?

How do I update an attribute in Parent entity once children records have been modified ?

I have a parent entity named “Job” which has ONE_TO_MANY link to child entity named “JobStatusHistory”.
JobStatusHistory keeps track of all the times a job has had it’s status changed.

All the fields are stored in the database.
In the Job entity, “currentJobStatus” should always be the status value of the most recent (latest date) JobStatusHistory record associated with the Job.

I already created a function in JobServiceBean to return the latest Job status for a job…so that part is working

But I am having serious difficulty in updating the particular Job record with the latest status and committing it to the database.
I have one screen “job-edit.xml” that edits the Job and has a nested table of related JobStatusHistory records.
Once there is a CRUD operation on items in the nested table, the “currentJobStatus” attribute should be updated in the Job entity and persisted to the database.

Please suggest the best approach and what events should I rely on.
I have tried using “aftercommit” within JobStatusHistoryEntityListener but getting an error that Job has been updated by another transaction.
I have tried so many things but not getting it to work. Please see my approache below and tell me if I am on the right track :

Job entity:

jobNo
clientName
currentJobStatus (type: jobStatus)

JobStatusHistory entity:

job (type: Job)
date
jobStatus (type: jobStatus)

JobStatus:

name

JobStatusHistoryEntityListener :

package com.company.lms.listener;

import com.company.lms.entity.Job;
import com.company.lms.entity.JobStatusHistory;
import com.company.lms.service.JobService;
import com.haulmont.cuba.core.EntityManager;
import com.haulmont.cuba.core.Persistence;
import com.haulmont.cuba.core.Query;
import com.haulmont.cuba.core.Transaction;
import com.haulmont.cuba.core.entity.contracts.Id;
import com.haulmont.cuba.core.global.DataManager;
import com.haulmont.cuba.core.listener.BeforeDeleteEntityListener;
import com.haulmont.cuba.core.listener.BeforeInsertEntityListener;
import com.haulmont.cuba.core.listener.BeforeUpdateEntityListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import javax.inject.Inject;
import java.sql.Connection;

@Component(JobStatusHistoryEntityListener.NAME)
public class JobStatusHistoryEntityListener
    implements BeforeInsertEntityListener<JobStatusHistory>,
        BeforeUpdateEntityListener<JobStatusHistory>,
        BeforeDeleteEntityListener<JobStatusHistory>
{
    public static final String NAME = "lms_JobStatusHistoryEntityListener";

    @Inject
    private JobService jobService;
    @Inject
    private DataManager dataManager;
    @Inject
    private Persistence persistence;

    @Override
    public void onBeforeDelete(JobStatusHistory entity, EntityManager entityManager) {
        updateJobStatus(entity.getJob());
    }

    @Override
    public void onBeforeInsert(JobStatusHistory entity, EntityManager entityManager) {
        updateJobStatus(entity.getJob());
    }

    @Override
    public void onBeforeUpdate(JobStatusHistory entity, EntityManager entityManager) {
        updateJobStatus(entity.getJob());
    }

    // We need to update jobStatus field in Job entity but have to do so after successful transaction commit
    private void updateJobStatus(Job job) {
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {

            @Override
            public void afterCommit() {
                job.setCurrentJobStatus(jobService.getJobCurrentStatus(job));
                dataManager.commit(job);
            }
        });
    }
}

The problem is that you mix different mechanisms in a single flow.

If you choose EntityListener, use EntityManager which is supplied in the listener’s methods to read and update entities. And preferably do it in the same transaction (just don’t start new ones and don’t use DataManager that creates transactions internally). The TransactionSynchronizationManager.registerSynchronization method is provided in the docs only for the case when you need some actions after guaranteed saving of all entities participating in the transaction, like if you need to send an email about successfully changed data. So in your case, remove this “after commit” block, make sure your JobService does not create transactions, and remove call to dataManager.commit(job), because everything will be saved on transaction commit anyway.

If you are on CUBA 6.10 or above, consider using EntityChangedEvent together with TransactionalDataManager which allows you to work with changed entities before and after transaction commit in the simple paradigm: load / change / save. Studio 10+ can generate EntityChangedEvent listeners for you.

2 Likes

Thank you Konstantin, I will try that.