apex-enterprise-patterns / fflib-apex-common

Common Apex Library supporting Apex Enterprise Patterns and much more!
BSD 3-Clause "New" or "Revised" License
903 stars 514 forks source link

How to call uow.commit() more than once in the same transaction? and should we do it? #371

Closed AllanOricil closed 2 years ago

AllanOricil commented 2 years ago

I have a feature which I have to do an Update after an Insertion, on the same Transaction.

Basically I insert a Record with Status = 'New' then I relate a bunch of other Objects to this Record then I finally need to update this Record Status to 'Publish'

I was previously doing this Update in a Batch, but I must remove this batch and do it in the same transaction as I don't care if it will work or not.

I tried the code below and it didnt work

fflib_ISObjectUnitOfWork uow = DSP_Application.UnitOfWork.newInstance();

MYCUSTOMOBJECT c = new MYCUSTOMOBJECT(Status__c = 'New');
uow.registerNew(c);
...
uow.commit();

uow.registerDirty(new MYCUSTOMOBJECT(Status__c = 'Publish')); 
uow.commit();

then I tried creating a second Unit of Work (uow2) and it worked.

fflib_ISObjectUnitOfWork uow1 = DSP_Application.UnitOfWork.newInstance();

MYCUSTOMOBJECT c = new MYCUSTOMOBJECT(Status__c = 'New');
uow1.registerNew(c);
...
uow1.commit();

fflib_ISObjectUnitOfWork uow2 = DSP_Application.UnitOfWork.newInstance();
uow2.registerDirty(new MYCUSTOMOBJECT(Id = c.Id, Status__c = 'Publish')); 
uow2.commit();

I think the problem is that after a commit, the State is not reset, which means if I call commit again it will try to commit everything that was registered as new or dirty again. Maybe you could reset the UOW state after a Commit, so if I call commit again it will be like a brand new UOW.

wimvelzeboer commented 2 years ago

Hi @AllanOricil You would only want to call the commit method once. Do I understand it correctly that you want insert the record as new, which will cause a trigger to do some stuff based on the "new" status, and then after that you want that same record updated to "Published"?

Shouldn't you make one trigger action that takes any record with status "new" and perform a "Publish" operation on those records, which has a step at the end to set the status to "Published"? Wouldn't that be a cleaner soluation?

Alternatively you could add a separate IDoWork implementation, something like:

MYCUSTOMOBJECT c = new MYCUSTOMOBJECT(Status__c = 'New');
uow.registerNew(c);
uow.registerWork(new MakeMePublished(c));
...
uow.commit();
private class MakeMePublished implements fflib_SObjectUnitOfWork.IDoWork
{
   private SObject record;
   public MakeMePublished(SObject record)
   {
      this.record = record;
    }
   public void doWork()
   {
      record.put(MyCustomObject.Status__c, 'Published');
      update record;
    }
}
AllanOricil commented 2 years ago

@wimvelzeboer I start with Status = New because there are other Objects that are going to be inserted in the 1St Commit. I can only change it to "Publish" after these objects are inserted. I put a more complete example bellow. Would your suggestion still work?

fflib_ISObjectUnitOfWork uow = DSP_Application.UnitOfWork.newInstance();

MYFIRSTCUSTOMOBJECT first = new MYFIRSTCUSTOMOBJECT(Status__c = 'New');
uow.registerNew(first);

MYSECONDCUSTOMOBJECT  second = new MYSECONDCUSTOMOBJECT();
uow.registerNew(
    second,
    MYSECONDCUSTOMOBJECT.MYCUSTOMOBJECT__c,
    MYCUSTOMOBJECT
);

MYTHIRDDCUSTOMOBJECT  third = new MYTHIRDDCUSTOMOBJECT();
uow.registerNew(
    third,
    MYTHIRDDCUSTOMOBJECT.MYCUSTOMOBJECT__c,
    MYTHIRDDCUSTOMOBJECT  
);

//After insert MYSECONDCUSTOMOBJECT  AND MYTHIRDDCUSTOMOBJECT , MYFIRSTCUSTOMOBJECT  is updated with some changes. Then when I set MYFIRSTCUSTOMOBJECT.Status__c = 'Publish' a platform event is Dispatched
uow.commit();

fflib_ISObjectUnitOfWork uow2 = DSP_Application.UnitOfWork.newInstance();
uow2.registerDirty(new MYFIRSTCUSTOMOBJECT(Id = c.Id, Status__c = 'Publish')); 
uow2.commit();
wimvelzeboer commented 2 years ago

@AllanOricil the IDoWork is run at the very end of the UnitOfWork, so when all the insert & update operations (including the trigger logic) has been executed.

So, the implementation of IDoWork should work for you.

AllanOricil commented 2 years ago

@wimvelzeboer amazing. Thanks man for your help :D

AllanOricil commented 2 years ago

@wimvelzeboer one last question. Would you add this private class in the Service Class, where the transaction happens?

wimvelzeboer commented 2 years ago

@AllanOricil Yes, that would make the most sense to add the IDoWork implementation as a private sub-class.

AllanOricil commented 2 years ago

@wimvelzeboer it works! Thanks again

AllanOricil commented 2 years ago

@wimvelzeboer why on the History there is no change from New -> Publish ?

image

I was expecting

New -> Publish -> Published

or

New -> Publish -> Error

Like, there is a trigger that when we change to Publish, a Platform Event is dispatched and if it was published, the Status is changed to Published, if it wasn't, the Status is updated to Error.

ImJohnMDaniel commented 2 years ago

If you are changing status to publish and then to Published all in the same context, then it would show only the latest updated. Remember, the UOW does the commit work but the actual transaction boundary spans the entire process context; regardless of how many UOWs you have.

cropredyHelix commented 1 year ago

Looking at your original question, I would do the following (and changing to use standard objects)

Account a = new Acount(Name = 'foo');
uow.registerNew(a);

Contact c = new Contact(lastName = 'bar')
uow.registerNew(c,Contact.AccountId,a);

a.Status__c = 'updated';
uow.commitWork();

If you have the object references available, you can update the values at any point before the commitWork()

Otherwise, the two {uow object : commitWork()} pairs is perfectly fine as it resolves to effectively this:

insert a;
// set c.AccountId = a.Id;
insert c;
update a;

all in one transaction