Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bugfix for Issue 221 #449

Open
wants to merge 14 commits into
base: master
Choose a base branch
from

Conversation

jprichter
Copy link

@jprichter jprichter commented Apr 28, 2023

fix for #221 from @dsurfleet


This change is Reviewable

Copy link
Contributor

@stohn777 stohn777 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test covering this scenario for regression-prevention sake.

@jprichter
Copy link
Author

Please add a test covering this scenario for regression-prevention sake.

@stohn777 unit test added here

Copy link
Contributor

@stohn777 stohn777 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jprichter
The new test DOES NOT fail with the preceding version of fflib_SObjectUnitOfWork.

@jprichter
Copy link
Author

jprichter commented Apr 29, 2023

@jprichter The new test DOES NOT fail with the preceding version of fflib_SObjectUnitOfWork.

Correct. One way to get it to fail before the change and pass after is to commit to the database instead of using the MockDML. Figured that wouldn't fly.

@jprichter jprichter closed this Apr 29, 2023
@jprichter jprichter reopened this Apr 29, 2023
@ImJohnMDaniel
Copy link
Contributor

ImJohnMDaniel commented Apr 30, 2023

@jprichter The new test DOES NOT fail with the preceding version of fflib_SObjectUnitOfWork
Correct. One way to get it to fail before the change and pass after is to commit to the database instead of using the MockDML. Figured that wouldn't fly.

@jprichter, I am a bit confused then by your comment. I would expect that the test case, applied to the current version of the codebase (not this PR branch) would fail. Is that a correct assumption or am I missing something?

@jprichter
Copy link
Author

@jprichter, I am a bit confused then by your comment. I would expect that the test case, applied to the current version of the codebase (not this PR branch) would fail. Is that a correct assumption or am I missing something?

@ImJohnMDaniel , the scenario is relating an existing record to a new record. One way to test this is to commit to the database so we have an Id from the newly created record. If you run the test like this, we'll see that the unit test fails on the version that's in master and passes in the version that's in the PR.

The mocking framework does not create fake Ids after mocking the insert. I'm open to guidance on how to better test this scenario using mocking.

@stohn777
Copy link
Contributor

stohn777 commented May 2, 2023

Does this test add value then? My initial guess is that it does not since the errant code passes the test. Generally we try to avoid actual DML in tests, but some times, it's needed to properly execute the code being tested. However some gray exists between that and a test of Salesforce functionality exclusively.

If we can craft a test that properly recreates the problem, using actual DML to add value through testing this feature, without simply testing Salesforce functionality. Otherwise then adding a test that does NOT add value is pointless.

@jprichter @ImJohnMDaniel

public void dmlInsert(List<SObject> objList)
{
for (SObject obj : objList) {
obj.Id = fflib_IDGenerator.generate(obj.getSObjectType());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ClayChipps and @jprichter -- Please add a null check on obj before trying to generate the mock Id.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -848,6 +905,43 @@ private with sharing class fflib_SObjectUnitOfWorkTest
}
}

private class MockDMLWithInsertIds implements fflib_SObjectUnitOfWork.IDML
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ClayChipps and @jprichter -- Any reason why you did not just simply modify the existing MockDML??

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use the existing MockDML.

@ClayChipps
Copy link
Contributor

ClayChipps commented Apr 4, 2024

This bug only presents itself when the Unit of Work has SObjects registered in a certain order.

Here is a sample scenario where the current implementation fails:

Lets assume we have two SObjects that we are about to register to a Unit of Work.

Account: {
   Name: My New Account
}
Opportunity: {
  Id = 0063000000D8cuI,
  Name = 'Existing Opportunity',
  StageName = 'Closed',
  CloseDate = System.today()
}

We have registered the Unit of Work such that Opportunity is registered before Account.

fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(
     new List<Schema.SObjectType>{
         Opportunity.SObjectType,
         Account.SObjectType
     }
);

We then attempt to perform the following operation:

uow.registerNew(newAccount);
uow.registerDirty(existingOpportunity, Opportunity.AccountId, newAccount);
uow.commitWork();

This will fail as when the Unit of Work reaches the insertDmlByType, it will first process all Opportunity relationship resolutions (even though no Opportunity has been registered new), and then process the insert and resolution of all Account relationships.

Then when the Unit of Work reaches the updateDmlByType, the relationship resolution is not invoked again, and thus the relationship that was registered has never been resolved after the Account insert.

The initial fix was to add a call to m_relationships.get(sObjectType.getDescribe().getName()).resolve(); inside of the updateDmlByType method so that the relationships would be resolved once again once getting to the update phase, at which point the Account.Id will have been populated in the insertDmlByType.

This causes a separate issue that was exposed by the InvoicingServiceTest. The PricebookEntry.Product2Id field can only be set once, and after it is set it cannot be updated, even in memory. Thus, when the second call to m_relationships.get(sObjectType.getDescribe().getName()).resolve(); is made within the updateDmlByType method, the relationship resolution happens again and attempts to update the PricebookEntry.Product2Id field only to fail with an exception as that field is now read-only.

The solution here was to modify the Relationship resolver to remove relationships from the registered relationships list as they are successfully resolved, so that previously resolved relationships are not resolved a second time.

@ImJohnMDaniel
Copy link
Contributor

@ClayChipps and @jprichter -- After more discussion on this PR, we would like to conduct a test of your changes in a real world series of codebases to assess any functional or performance impacts. I will reach out to you two directly to discuss this in detail. Thanks again for the help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants