If you have a field on Pricebook2
marked as an "external Id", and there are no duplicate values, then it is possible to accomplish this in a single loop and a single upsert1.
In general, the pattern would look something like this:
List<Account> accountsToUpsert = new List<Account>();
List<Account> children = new List<Account>();
for(<something>){
// Just trying to convey that all the external ids need to be unique
String externalId = 'myId' + i;
Account parent = new Account(Name = 'Parent-X', My_External_Id__c = externalId);
// This is what allows Salesforce to auto-relate the child record
// It must be an instance of the SObject with only the external Id populated)
Account child = new Account(Name = 'Child-X', Parent = new Account(My_External_Id__c = externalId));
// The other part is that the corresponding parent record needs to be inserted before
// the child record.
// Using two separate lists like this to achieve that isn't strictly necessary
// but it is easy/safe.
accountsToUpsert.add(parent);
children.add(child);
}
// The end result is that our list contains all of the parents first, then all of the
// children.
accountsToUpsert.addAll(children);
upsert accountsToUpsert My_External_Id__c;
If that's not an option for you, then there's no way to avoid needing to perform two dml (outside of the composite api) and two loops. It is possible to do most of the work in the first loop though.
// The change here is that we have a map to help us keep track of which parents a
// child record should be related to
// The key of the map being an SObject means we need to be careful
Map<SObject, List<SObject>> parentKeyToChildrenMap = new Map<SObject, List<SObject>>();
List<Account> parentsList = new List<Account>();
List<Account> childrenList = new List<Account>();
for(<something>){
// Create the parent record like usual
Account parent = new Account(Name = 'Parent-X', field1__c = 'value1', field2__c = 'value2');
// The "key" account we use for the map should be something we can derive from
// the parent record alone.
// Because it's a key, it needs to be unique to the transaction (but it's not an
// issue if there's already a stored record with the same value(s) like it is
// when using an external id)
Account parentKey = new Account(Name = parent.Name);
Account child = new Account(Name = 'Child-X');
// The child records don't get added to the list in this loop
parentsList.add(parent);
// Standard map initialization
if(!parentKeyToChildrenMap.containsKey(parentKey)){
parentKeyToChildrenMap.put(parentKey, new List<Account>());
}
parentKeyToChildrenMap.get(parentKey).add(child);
}
// First upsert takes care of parents
upsert parentsList;
// Then we loop again to assign the parent ids that we now have
for(parent :parentsList){
// This is why it's important to be able to derive the key from the parent (and
// only the parent).
// We need to reconstruct it so we can get at the child records
// We can't just use the parent record itself, since it'll now have an Id
// (and thus results in a different hash)
Account key = new Account(Name = parent.Name);
for(child :parentKeyToChildrenMap.get(key)){
child.ParentId = parent.Id;
childrenList.add(child);
}
}
upsert chilrenList;
1: After some additional testing, this is possible for inserts, updates, and upserts. It's possible even if the parent and child records are of the same SObject. The catch here is that the parent and child records need to be in separate trigger "chunks" (triggered records are chunked into groups of 200, with the final chunk possibly containing fewer records. AllOrNone = false complicates that statement). This means that you cannot use a single DML statement for this if you have fewer than 200 parent records.
It's similar for different parent-child SObjects. The parent SObject needs to be DML'd before the child SObject. As long as your List<SObject>
contains all of the parent records first, then you add all of the child records, it should turn out fine. You can have fewer than 200 parent records if this is the case.