I have a similar problem getting the same error message as described above, first let me explain my case:
I have a visual force page with an extension "without sharing" that list cases that the current logged in customer community user has not access to.
I use a apex:dataTable with this column:
<apex:column>
<apex:commandLink value="{!c.CaseNumber}" action="{!SetCaseSharing}">
<apex:param name="setCaseSharing" value="{!c.Id}" assignTo="{!caseIdSelected}"/>
</apex:commandLink>
</apex:column>
When the user selects a case clicking on the commandLink this method runs (a bit simplified):
public Id caseIdSelected {get; set;}
public SetCaseSharing()
{
Case c = [SELECT Id FROM Case WHERE Id = :caseIdSelected];
CaseShare caseShare = new CaseShare();
caseShare.CaseId = c.Id;
caseShare.UserOrGroupId = UserInfo.getUserId();
caseShare.RowCause = 'Manual';
caseShare.CaseAccessLevel = 'Edit';
upsert(caseShare);
}
I know about the limitation that the current record owner cannot be added using manual apex sharing, and you cannot limit the access granted using sharing rules only add permission. These issues are not the problem in my case.
When logging in as a customer commmunity user using "Manage External Users" and then select a case from the list the problem occurs.
My code worked perfectly however when logging into the community as an administrator or "normal" user (instead of customer community user) and then adding a "normal" user (not a customer community user) to the manual sharing.
PROBLEM 1
I was not allowed to set manual sharing to a customer community user, even logged in as administrator, returning:
Upsert failed. First exception on row 0; first error: FIELD_INTEGRITY_EXCEPTION, field integrity exception: unknown (invalid user or group: 00511000002Hgs3): [unknown]
I guess the first problem might be solved using the more expensive "partner community license" or "customer community plus license" instead of "customer community license".
PROBLEM 2
I was not allowed to set manual sharing to a "normal" user when logged in as a customer community user, returning:
Upsert failed. First exception on row 0; first error: INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY, insufficient access rights on cross-reference id: []
Not sure if it is possible to add sharing rules from the community when the logged in user is a customer community user not having access to the case in the first place. Moving this to a trigger or a scheduled job running separate from the community might solve it.
I ran into a similar issue for a project that I was on last year in which I was dealing with managing the blocks of IP Addresses that my company controls. In my case, the hierarchy could be up to 17 levels deep (with some 2^16 possible records indirectly related to a single block of IP Addresses).
You really have 2 issues to tackle in this question:
- How do I get a count of all
CustObjRecords
directly related to this account?
- How do I get a count of all
CustObjRecords
related to child accounts?
For the sake of this answer, I'll assume that the relationship between accounts is a field called Parent_Account__c
(self-relationships must always be lookup relationships), and the relationship between your CustObjRecord
and Account
is a master-detail relationship on CustObjRecord
called Account__c
.
I'll also assume that you have 3 fields on Account
, Direct_Count__c
, Indirect_Count__c
, and Total_Count__c
. Total_Count__c
would simply be a formula field that adds Direct_Count__c
and Indirect_Count__c
.
In this situation, you can make a rollup summary field to count the number of CustObjRecords
directly related to each Account
.
If Account
and CustObjRecord
have a lookup relationship, you could accomplish this step by using Andrew Fawcett's Declarative Lookup Rollup Summary Tool
That takes care of the first part. The second part is a bit...thornier.
If you have the Master-Detail relationship between CustObjRecord
and Account
, or if you're using the declarative rollup summary tool, you will have a trigger fired on Account
before/after update.
Supporting a hierarchy of arbitrary height means that traversing up the hierarchy using Parent_Account__r
multiple times is not viable (limited to 5 levels). Using additional queries for each level of the hierarchy is also ill-advised (queries are a precious resource!).
You could possibly use the declarative rollup summary tool for this part as well, but that would still use 1 query per level of your hierarchy.
To allow for a hierarchy of arbitrary height, you'll need an additional piece of information. You'll need to, on every Account
record, keep track of the Id of the root of the hierarchy. This could be a lookup field, or it could simply be a text field. I'll call this field Root_Account__c
.
This will allow you to use a single query to grab all of the records that will need to be updated (and potentially several that don't).
The general structure of your Account
trigger will look something like this:
- Loop through trigger.new, and build a
Set<Id>
containing the Ids of the hierarchy roots involved
Map<Id, Account> accountsWithChildren = new Map<Id, Account>([SELECT Id, (SELECT Id, Direct_Count__c FROM Child_Accounts__r) FROM Account WHERE Id IN <set of root ids> OR Root_Account__c IN <set of root ids>]);
- Use some good, old-fashioned recursion to calculate the
Indrect_Count__c
for each level in a top-down manner (not using recursion, and working this bottom-up is possible, but a lot more work)
The recursive bit could look like this:
public Integer calculateIndirectTotals(Account startingAccount){
Integer indirect = 0;
for(Account child : accountsWithChildren.get(startingAccount.Id).Child_Accounts__r){
indirect += calculateIndirectTotals(child);
}
// The map<Id, Account> below is declared outside of this method
accountsToUpdate.put(startingAccount.Id, new Account(Id = startingAccount.Id, Indirect_Total__c = runningTotal));
return runningTotal + startingAccount.Direct_Count__c;
}
Now, as is, this code would likely run into some issues (accountsToUpdate
probably includes the account(s) that are part of the update trigger that kicked off this method, which would lead to a SELF_REFERENCE_FROM_TRIGGER exception), but the basic idea should be sound.
+edit:
After I had written my answer, it was revealed that there are some additional constraints:
- A rollup summary field can't be used on
Account
- We can only touch the trigger for
CustObjRecord
The constraint that only the trigger for CustObjRecord
can be touched is the most disruptive. If Count__c
needs to be kept up to date in near real-time, then the Declarative Lookup Rollup Summary tool is no longer a viable option (as it needs its own trigger on the child object to function in real-time mode, and order of trigger execution, when there are multiple triggers on an object, is not guaranteed).
If you can live with updating Count__c
on a looser schedule
That is to say, not real-time (or near real-time). In this case, you're probably best served by the bottom-up approach using batchable (or queueable) apex as mentioned by crop1645 and Ratan.
The idea here is that you have a field on Account
that keeps track of whether or not that Account
needs to be updated. Initially, the trigger on CustObjRecord
(for newly inserted, or updated, records) will aggregate the Accounts
related to the CustObjRecords
taking part in your trigger, and DML update those Accounts
.
From there, you schedule a batch job. The batch Apex needs to end up running two queries
[SELECT Account__c, COUNT(Id) FROM CustObjRecord WHERE Account__r.Update_Account_Count__c = TRUE GROUP BY Account__c]
and
[SELECT Parent_Account__c, SUM(Total_Count__c) FROM Account WHERE Parent_Account__r.Update_Account_Count__c = TRUE GROUP BY Parent_Account__c]
The first query takes care of calculating Direct_Count__c
, and the second query takes care of calculating Indirect_Count__c
.
Finally, you update the Accounts
taking part in the current batch job, setting their direct and indirect counts and, very importantly, unset Update_Account_Count__c
. You'll also need to set Update_Account_Count__c
for the parent accounts of the Accounts
taking part in the current batch job. Then, you chain the next batch job in the finish()
method of your batch apex class.
If everything goes right, the batch jobs will continue to chain until you reach the top of your hierarchy. Each new batch in the chain is in its own execution context, so you won't run into the 100 SOQL query limit by virtue of the batch apex alone.
If you need the counts updated as soon as possible
Then you'll need to expend a few more queries to get things set up.
CustObjRecord's
trigger will need to do the work of updating the Direct_Count__c
of the Accounts
related to the records in your CustObjRecord
trigger. That particular query is simple (gather affected accounts, use COUNT(Id)
to query/count all the CustObjRecords
related to those accounts, dml update).
The code to calculate Indirect_Count__c
can easily be put into CustObjRecord's
trigger instead of Account's
trigger. The only difference is that you'll need to query for the Root_Account__c
of the affected accounts instead of being able to grab that information from a trigger context variable.
+edit2:
That's all well and good, but how can I identify the Account at the top of the hierarchy?
In my original answer, I mentioned having a field Root_Account__c
on every Account
. This field isn't automatically populated, you'll need to have a plan to maintain the integrity of this field going forward, and you'll need to figure out a way to update your existing accounts so that they contain this information.
If Root_Account__c
is a text field, a simple workflow rule set to run on creation every new Account
with a field update to set Root_Account__c
to Parent_Account__r.Root_Account__c
(or to its own Id if it doesn't have a parent) would take care of adding new leaf nodes and roots. You still have other cases to worry about (deleting the root, deleting another non-leaf node, adding a new node above the current root, etc...), but handling those cases would be a separate question. This doesn't populate Root_Account__c
on your existing accounts, however.
Without touching Account's
trigger, your best bet to accomplish both tasks is probably a batch job that is called via another class that implements the Scheduleable
interface (so that your batch job is being regularly run). You can schedule a batch job to run once via system.ScheduleBatch()
, which I imagine can be done through executing anonymous apex in the developer console.
The batch class here would work top-down. The query for the batch could look something like this:
[SELECT Id, Parent_Account__r.Root_Account__c FROM Account WHERE (Parent_Account__c = null AND Root_Account__c = null) OR (Root_Account__c = null AND Parent_Account__r.Root_Account__c != null)]
This would pull the Accounts
at the top of your hierarchies which haven't been set up to support the other group of Accounts
that this query also pulls, the set of Accounts
which don't have Root_Account__c
set but have a parent that does.
Those null values in the query are problematic though. It will make the query slow, and it will outright fail from not being a selective query if you have enough Accounts
(>= 100,000)
A better way to do this would be to do a little more prep work.
- create one more field on
Account
, a checkbox field which I'll call Needs_Root_Populated__c
, and defaulted to true.
- With anonymous apex, query for all the root
Accounts
(those with no parent), set Root_Account__c
to their own Ids, and unset Needs_Root_Populated__c
The query can then become
[SELECT Id, Parent_Account__r.Root_Account__c FROM Account WHERE Needs_Root_Populated__c = true AND Parent_Account__r.Needs_Root_Populated__c = false]
This won't pull new root accounts, but the query should be much more selective.
The last thing left to do is to use that query to update the Accounts
that were pulled, and then to chain another batch so this process continues down to the last leaf node in your hierarchies.
This makes the Root_Account__c
information available at all levels of your hierarchy. You simply need to query for this field on the Accounts
whose Direct_Count__c
is being updated in CustObjRecord's
trigger.
Best Answer
Put your DML statement inside the "Try" section of a Try Catch block and then handle the DML error like this:
Be aware that this will fail every record in the trigger batch with the first record's error message - possibly desirable but if not you can use the Database.update() method to receive detailed information about which records fail and then map these back to the trigger records and only fail those that you want to.
Update
The code inside each Catch block will only run if that type of Exception is thrown - so only one Catch block will ever run. That's why the fact they are both called "e" doesn't matter.
When performing DML if an exception occurs (e.g. validation rule) then a DMLException will be thrown which has a special method on it called getDmlMessage(). The other catch block (catch (Exception e)) is optional you can remove it seeing as you are only doing DML in the Try section.