Commit sequence when using commitcontext

I have to update another entity (delete a set of rows and insert another set of rows) in the same transaction as the main entity from the current editor screen. For this purpose I’m using : getDsContext().addBeforeCommitListener - where I am using the 2 methods available for commitcontext: setRemoveInstances, followed by setCommitInstances.
The problem I encounter is that I don’t know in which order are the 2 methods invoked, I want first to delete some rows and after that to insert new ones. Because, otherwise, I get the PK violation error. Is it possible to specify a sequence ? Or the only solution for that is to build the 2 sets of instances as disjoint sets?
Thanks in advance.

Hi Tudor,

It’s impossible to specify a sequence of deletion/insertion from the client tier. It doesn’t matter how you invoke setRemoveInstances/setCommitInstances, this is just adding to collections that are passed to middleware together. When DataManager processes these collections, it inserts/updates first and then deletes.

You have the following options:

  • If you really need to save your entities together with modified entities from the screen datasources in one transaction, you can create your own implementation of DataSupplier by extending GenericDataSupplier and use this implementation in the screen by specifying it in the dataSupplier attribute:

public class MyDataSupplier extends GenericDataSupplier {

    @Override
    public Set<Entity> commit(CommitContext context) {
        return AppBeans.get(SomeService.class).commit(context);
    }
}

<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
        ...
        dataSupplier="com.company.sales.gui.order.MyDataSupplier">

</window>

SomeService.commit() should save all your entities and optionally return saved ones as DataManager does. Returning saved entities is needed only if your screen stays open after commit.

  • Much simpler solution would be saving your entities in a separate transaction from the screen, just by passing them to a service which implements your logic.
1 Like

Thank you Konstantin!
I prefer to do everything in a single transaction to ensure atomicity. But it is more simple if I stick to the datamanager and take care of building the exact commitcontext without repeating instances that are already persistent in the db.
But it is good to know that I can do it also by extending GenericDataSupplier. The SomeService.class would have a transaction in that case wich would iterate through all the modified instances of all the entities including the one used in the screen. After that: persistence.getEntityManager().persist(myEntity) is called. Finally tx.commit, right?

Right, if you are able to pre-process CommitContext in beforeCommit() of your listener, it will be much simpler solution.

And a service would do exactly what you described.

1 Like

After doing some tests, I realized that I really need to save my additional entity (I call it AdditionalEntity) together with the modified entities from the screen in one transaction. In that transaction block, I really need to save the screen entities first and after that AdditionalEntity. In this case unfortunately I cannot use addBeforeCommitListener :frowning: which saves AdditionalEntity first and after that the screen entities.
Assuming there is no simple solution to that, other than implementing DataSupplier, I’ve tried what you suggested:


public class ProdDataSupplier extends GenericDataSupplier {
    @Override
    public Set<Entity> commit(CommitContext context) {
        return AppBeans.get(ProdService.class).commit(context);
    }
}

The implementation of ProdService class (inspired by RdbmsStore.java):


    @Override
    public Set<Entity> commit(CommitContext context){
        Set<Entity> entitySet = new HashSet<>();

        try (Transaction tx = persistence.createTransaction()) {

            EntityManager em = persistence.getEntityManager();
            
            // insert
            for (Entity entity : context.getCommitInstances()) {
                if (entity instanceof AdditionalEntity)
                    continue;
                if (PersistenceHelper.isNew(entity)) {
                    em.persist(entity);
                    entitySet.add(entity);
                }
            }
            for (Entity entity : context.getCommitInstances()) {
                if (entity instanceof AdditionalEntity)
                    if (PersistenceHelper.isNew(entity)) {
                        em.persist(entity);
                        entitySet.add(entity);
                    }
            }
            

            // update
            for (Entity entity : context.getCommitInstances()) {
                if (entity instanceof AdditionalEntity)
                    continue;
                if (!PersistenceHelper.isNew(entity)) {
                    em.merge(entity);
                    entitySet.add(entity);
                }
            }
            for (Entity entity : context.getCommitInstances()) {
                if (entity instanceof AdditionalEntity)
                    if (!PersistenceHelper.isNew(entity)) {
                        em.merge(entity);
                        entitySet.add(entity);
                    }
            }

            // delete
            for (Entity entity : context.getRemoveInstances()) {
                Entity e = em.merge(entity);
                em.remove(e);
                entitySet.remove(entity);
            }

            tx.commit();
        }

        return entitySet;
    }

Now the commitcontext parameter above contains all modified screen instances. But I want to add to that commitcontext - the commitcontext of the AdditionalEntity, which I have manually built in the screen controller.
How can I achieve that?
On the save button: getDsContext.commit() is called. There are no commitcontext parameters. Investigating a little bit, I tried to extend DsContextImpl overriding createCommitContext method:


public class ProdDsContextImpl extends DsContextImpl {
    public ProdDsContextImpl(DataSupplier dataservice) {
        super(dataservice);
    }

    @Override
    protected CommitContext createCommitContext(DataSupplier dataservice, Map<DataSupplier, Collection<Datasource<Entity>>> commitData) {
        CommitContext commitContext = super.createCommitContext(dataservice, commitData);
        // commitContext.setCommitInstances(...); // here I set rows to add for AdditionalEntity
       // commitContext.setRemoveInstances(...); // here I set rows to remove from AdditionalEntity
        return commitContext;
    }
}

But I didn’t succeed forcing the app to call ProdDsContextImpl instead of the standard DsContextImpl .
I also assume there is a simpler solution to that. Maybe you can tell me if my solution is good or bad, or you may share an example on how to do it right, so that others can use this example for similar situations.

Thanks.

Hi Tudor,

There is a simple solution indeed - add your AdditionalEntity to the commit context in DsContext.BeforeCommitListener#beforeCommitmethod. It will go to the commit() of your bean together with all other changed entities. This listener is designed specifically to be able to save arbitrary entities together with automatically managed by datasources in one transaction.

It worked, thanks.
On the save-action, before calling getDsContext().commit(), I wrote:


getDsContext().addBeforeCommitListener(context -> {
           context.getCommitInstances().addAll(AdditionalEntityInstancesToAdd);
           context.getRemoveInstances().addAll(AdditionalEntityInstancesToRemove);
});

Is there any simpler solution to this issue, other than using a service? There are often situations when I need to delete first and insert after in order to avoid different constraint violation on the database level.
Thanks.