function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
SFDummySFDummy 

How to prevent object deletion using trigger

I have multiple related objects  associated to Product2  (Accounts and MycustomObj__c)

 

When I am deleting product I am checking if there is any (related lists)

but I do not know who to prevent deletion if I find related list items.

 

My conditions are when I mass delete products

if there are related items associated to product set the product to inactive do not delete (I do not know how to do this part)

 

If not related items delete product and all child objects. ( this works fine)

 

Any suggestions please?

 

Thanks

Starz26Starz26

Here is a trigger I use to check for children on a custom object and prevent delete if a child exists. It uses sets for ID's which are created earlier in the trigger so you will have to adjust to meet you needs.

 

//Delete Trigger WILL
    //  1. Check to see if there is an FSA associated with the records in the trigger
    //      a. For each record - if there is an FSA, do not allow delete
    //      b. For each record - If there is no FSA - Allow Delete


    
    //List of IHO's  for the Opportunity's  
    List<Implementation_Hand_Off__c> lIHO = [SELECT ID, Opportunity_Name__c FROM Implementation_Hand_Off__c WHERE Opportunity_Name__c IN :idOpp];
    
    
    //List and Map of FSA's for the IHO associated with the Opportunity's   
    Map<ID,FSA__c> mFSA = New MAP<ID,FSA__c>();
    List<FSA__c> lFSA = [SELECT Implementation_Hand_Off__c FROM FSA__c WHERE Implementation_Hand_Off__r.Opportunity_Name__c IN :idOpp];
        
    for(FSA__c oFSA :lFSA){
        
        mFSA.put((id)oFSA.Implementation_Hand_Off__c,oFSA);
        
    }

    //For each Opportunity in the trigger
    for(Opportunity oOpp : trigger.old){
        
        //Loop through the List of IHO's
        for(Implementation_hand_Off__c oIHO : lIHO){
            
            //If the Opp Name on the IHO are equal and there IS an associated FSA
            if(oIHO.Opportunity_Name__c == oOpp.id && mFSA.get(oIHO.id) != Null){
                
                system.debug(oOpp.id + ' was not deleted' );
                //Throw an error that the Opp cannot be deleted
                oOpp.addError('An FSA Exists for this Opportunity and it cannot be deleted');
    

                    
            }else{          
    
                system.debug('Opp id ' + oOpp.id + ' was deleted');
    
            }
        }//end for oIHO loop
    }//end for oOpp loop
}//End isDelete trigger 

 

SFDummySFDummy

Thanks for the prompt response

 

what is the event that cause the trigger,  my event is actual delete itself

if(Trigger.isDelete && Trigger.isBefore){

     Set<Id> prodIds = new Set<Id> ();     
    
     
     //1. collect all productIDS
     //2. find products that has account association
       for(account a: [SELECT product__c FROM account WHERE Product__C IN : prodIds]){
                  accPids.add(a.product__c);                                
             }
     //3      
     //Remove ProdIDs from delete list that has Account association
                  prodIds.removeAll(accPids);
      //4. Do not delete AccPids  do not know how to do this
      
      //5. Delte prodIds - Done

}

 

step 4 is what I do not know how to do... I am already in delete trigger so product gets deleted

 

when I print the following it is zero 0

if(!prodIds.isEmpty() || prodIds.size() != 0) 

 

 how to prevent deletion .....

 

Thanks


 

 

spraetzspraetz

How are you doing the mass delete?  is it via a standard mass delete functionality or a custom written one?

Starz26Starz26

I am doing this in a before Delete trigger...

 

This is the part that prevents the object from being delete and will continue with the rest of the records and delete if appropriate.

 

oOpp.addError('An FSA Exists for this Opportunity and it cannot be deleted');



SFDummySFDummy

I have to do other processing so I created another class that does that. So I pass Ids to that class

   productTriggerHandler.OnBeforeDeleteProdsProcess(prodIds)

 

@starz26

Does addError will display on screen. I do not want error message on UI  when I doing mass delete

 

does addError work for list looks like it work for only sObject? or should I delete individually..... (I will hit limits)

 

 

I will try, thanks clarification

Starz26Starz26

I have not tested it with anything with mass delete yet other than excel connector. Since the mass delete in the UI is not for Opportunity records or custom objects.....

 

The trigger is actually on the Opportunity and not the account. You did bring up an issue now as I can delete the account and it deletes the opportunity as well even though it should not. Need to add this trigger to account now.

 

As for single records, yes it add a message to the ui. For the test class, it adds them to the debug logs....

SFDummySFDummy

I tried the following just to check if it works

for(product2 p:[select id, name FROM product2 where Id IN: accpids]){
               p.addError('Account associtated. Cannot delete this product');
               }

 I get the following error in UI

 

I still do not know how to prevent deletion using trigger when I bulkify my trigger.

 

ProductsTriggers: execution of BeforeDelete
caused by: System.FinalException: SObject row does not allow errors
Trigger.ProductsTriggers: line 45, column 1

Starz26Starz26
The add error was on the trigger so try this for testing..


For(Product2__c p : trigger.old){

p.addError('test')

}

This will add an error for all record without checking conditions but should answer your question
SFDummySFDummy

changed the code

 

if(Trigger.isDelete && Trigger.isBefore){

      for(Product2 allProdIds:trigger.old){

            allProdIds.addError('test');       
           }


same error

ProductsTriggers: execution of BeforeDelete

caused by: System.FinalException: SObject row does not allow errors

Trigger.ProductsTriggers: line 46, column 1

 

Starz26Starz26

Need full code for context. What is Product2 a costom object or a variable?

 

The for loop needs to be using the object for which the trigger was running on i.e:

 

for(Account a : trigger.old) or for (CustomObject__c c :trigger.old)

 

 

pjcloudpjcloud

Sorry, but just coming into this after seeing it posted to the DF portal. 

 

I feel like I'm missing some information after having read through this thread.

 

First, what is the action that invokes the trigger? Is it your own code? Is it someone elses? Is it API?

 

Is the trigger caused by a delete occurring on the Product2 object in the first place? Or is the delete originated from some other object? 

 

In your pseudocode example earlier you list a step: "1. collect all productIDS". I don't understand what you mean by the verb "collect"? Is it a query? Are you pulling out some of the product IDs and not others? And why do you use this step. You already have them in Trigger.oldMap.keyset();.

 

Finally, I agree with Starz26, without more of your code and more context I don't know how much more help we can be. 

 

That being said, there are a few things I can glean from this conversation. You say you are passing a list of IDs to a helper method. ID data type is considered "primitive" (even though it is not by other prog language standards), but importantly this means it is passed by value. If you simply pass a list of IDs to a helper method, unless you pass something back to the calling context (like a new list of IDs that you should take/not take action on), there is no way for you to prevent deletion from that helper. Try passing the list of sObjects instead. This will pass by reference and then you might be able to use your addError() to prevent delete. 

 

In playing with this scenario in my own org, I found that calling sObject.addError('my message') from within anonymous block gave me the same error. But if I put it into a trigger context, it worked find. So don't know what is going on with you in that one...

SFDummySFDummy

Attached the complete code.  I am actually mass deleting product2 rows

 

I have Accounts as related list on Product2. Product2 has Lookup relation to product_Rate__c

Delete action invokes trigger. I create tigger on product2

                 trigger ProductsTriggers on Product2 (after update, before delete)

 in the triggger find product2 that has accounts related list - do not delete product2

(3) find all product_rate__c  for product2 and delete  (delete product2 and product_rate__C

I am deleting primarily using dataloader.

 

if(Trigger.isDelete && Trigger.isBefore){
           Set<Id> setRateID = new Set<id>();
           List<product2> pIds = New List<product2>();
           List<Id> accPids = new List<Id>();

           for(Product2 allProdIds:trigger.old){
             //pIds.add(allProdIds);   
             prodIds.add(allprodIds.Id);  
             allProdIds.addError('test');        
           }
           
          if(!prodIds.isEmpty())  {
             //check accounts associated
             //List<account> noDeleteAccIds = [SELECT product__c FROM account WHERE Product__C IN : prodIds];
             
             for(account a: [SELECT product__c FROM account WHERE Product__C IN : prodIds]){
                accPids.add(a.product__c);                                
             } 
              
             //Remove ProdIDs from delete list that has Account association
             prodIds.removeAll(accPids);                         
             
             for(product2 p:[select id, name FROM product2 where Id IN: accpids]){
               p.addError('Account associtated. Cannot delete this product');
               }
         
             if(!prodIds.isEmpty() || prodIds.size() != 0)  {
                 for(Product_Rate__c delRateObj :[SELECT Id from Product_Rate__c where ProductID__c IN : prodIds]) {
                    setRateID.add(delRateObj.id);
                 }                 
                                    
                if(!setRateId.isEmpty())
                    ProdTriggerHandler.OnBeforeDeleteProd(setRateID);
            }
          
         }
    }     

 

ShadowSightShadowSight

I'm guessing that there's a missing

Set<Id> prodIds = new Set<Id>();

somewhere in the code. 

 

 

The reason you're getting the "SObject row does not allow errors" is because you can only add an error to the object that is a part of the Trigger context, so when you do this: 

allProdIds.addError('test'); 

 

it works because you're iterating through trigger.old.

 

However, later on (presumably line 46) you have this: 

for(product2 p:[select id, name FROM product2 where Id IN: accpids]){
               p.addError('Account associtated. Cannot delete this product');
               }

 Which will not work, because "p" is not a part of the trigger context.

 

You should be able to do this, however:

 

for(product2 p:[select id, name FROM product2 where Id IN: accpids]){
               product2 errObj = Trigger.oldMap.get(p.Id);
               if(errObj != null){ // probably unnecessary to null check this, but just in case...
                   errObj.addError('Account associtated. Cannot delete this product');
               }
}

 

Here's my re-write of the code you've provided:

if(Trigger.isDelete && Trigger.isBefore){
	Set<Id> setRateID = new Set<id>();
	List<Id> accPids = new List<Id>();
	Set<Id> prodIds = trigger.oldMap.keyset(); // as pjcloud mentions, no need to collect these

	// This will add an error to all objects in the trigger context, which
	// will prevent DML operations downstream; leaving this in for your test
	for(Product2 allProdIds:trigger.old){
		allProdIds.addError('test');        
	}

	// don't really need to check if(!prodIds.isEmpty()), since if it
	// were empty the trigger wouldn't be firing

	//check accounts associated
	for(account a: [SELECT product__c FROM account WHERE Product__C IN :prodIds]){
		accPids.add(a.product__c);                                
	} 
              
	//Remove ProdIDs from delete list that has Account association
	prodIds.removeAll(accPids);                         
             
	for(product2 p:[select id, name FROM product2 where Id IN: accpids]){
		// get the product2 from the trigger context and add an error to it:
		Trigger.oldMap.get(p.Id).addError('Account associtated. Cannot delete this product');
	}
         
	if(!prodIds.isEmpty() || prodIds.size() != 0)  {
		for(Product_Rate__c delRateObj :[SELECT Id from Product_Rate__c where ProductID__c IN : prodIds]) {
			setRateID.add(delRateObj.id);
		}                 
                                    
		if(!setRateId.isEmpty())
			ProdTriggerHandler.OnBeforeDeleteProd(setRateID);
	}
}     

 

SFDummySFDummy
Trigger.oldMap.get(p.Id).addError('Account associtated. Cannot delete this product');

 the above line give the following error

ProductsTriggers: execution of BeforeDelete

caused by: System.NullPointerException: Attempt to de-reference a null object

Trigger.ProductsTriggers: line 39, column 1

 

Thanks much in taking time to re-write my code.

But, I am still getting errors when I try to delete product that has account associated.

 

Delete 1 product with not account association - works (prevent deletion)

Delete 1 prouduct with no account association -  works (delete product and product_rate__C associated)

 

when I try to delete 2 product with one each froma above scenarios I get errors

When I delete combination products, I do not want data loader to error out on all.  Is there any way to ignore  errors and continue with deletion .  I only test with 3 rows. 1 has accoutn association. I got same error on all prorducts.

 

 

ShadowSightShadowSight

A bit more careful of a rewrite.  Realized there was no point in re-querying products with the ids we gathered from the accounts that are linked to those products - we already have the id, so that should be all we need.

 

if(Trigger.isDelete && Trigger.isBefore){
	Set<Id> setRateID = new Set<id>();		// set of Product_Rate__c IDs to handle
	Set<Id> accPids = new Set<Id>();		// set of ids that are linked to by accounts
	Set<Id> prodIds = trigger.oldMap.keyset();	// set of all ids that are being deleted

	//check accounts associated
	for(account a: [SELECT Id, product__c FROM account WHERE Product__C IN :prodIds]){
		// add to our list of Ids associated with Accounts
		accPids.add(a.product__c);
		
		// get the object from the Trigger Context
		Product2 errorObj = Trigger.oldMap.get(a.product__c);
		
		if(errorObj != null){
			// add an error to the object which will prevent this object from being deleted
			errorObj.addError('Account associated. Cannot delete this product');
		} else {
			System.debug('Somehow, Trigger.oldMap does not contain a Product2 that is linked to an Account, even though that product2 ID is in the Trigger.oldMap keyset');
			System.debug('-- Trigger.oldMap: ' + Trigger.oldMap);
			System.debug('-- Account: ' + a);
		}
	}

	// Build a Set of IDs that are OK to delete: ids NOT in the accPids
	Set<Id> prodIdsOKToDelete = new Set<Id>();
	for(Id prodId : prodIds)){
		if(!accPids.contains(prodId))
			prodIdsOKToDelete.add(prodId);
	}

	if(!prodIdsOKToDelete.isEmpty())  {
		for(Product_Rate__c delRateObj :[SELECT Id from Product_Rate__c where ProductID__c IN : prodIdsOKToDelete]) {
			setRateID.add(delRateObj.id);
		}
		
		if(!setRateId.isEmpty())
			ProdTriggerHandler.OnBeforeDeleteProd(setRateID);
	}
}     

 

I'm not sure why, but using:

prodIds.removeAll(accPids);

 

just strikes me as a possible problem, but I can't put my finger on why... so I just skipped that and build a new set in the above.  See if that works any better.

SFDummySFDummy
if(errorObj != null){

 Adding the above line prevented Nullpointer exception

 

Thanks everyone for helping me figure out the problem.

here is the problem statement again

Our system has products (product2) with related objects Account (account) and ProductRate(Product_Rate__c)

       When I delete products

                      if product2account relation exists do not  delete

                       if product2ProductReates exists and no product2account relation delete productRate also

 

Here is the final code that works both via UI delete and Mass delete.

 

 

trigger ProductsTriggers on Product2 (after update, before delete) {
  
     Set<Id> prodIds = new Set<Id> (); 
    if(Trigger.isUpdate && Trigger.isAfter){
     //my update condition code is here
     }
 

else if(Trigger.isDelete && Trigger.isBefore){
           Set<Id> setRateID = new Set<id>();          
           List<Id> accPids = new List<Id>();
           prodIds = trigger.oldMap.keyset(); 
           
        //check accounts associated
       for(account a: [SELECT Id, product__c FROM account WHERE Product__C IN :prodIds]){        
        accPids.add(a.product__c);
        
        // get the object from the Trigger Context
        Product2 errorObj = Trigger.oldMap.get(a.product__c);
        
        if(errorObj != null){
            // add an error to the object which will prevent this object from being deleted
            errorObj.addError('Account associated. Cannot delete this product');
        } else {
            System.debug('Somehow, Trigger.oldMap does not contain a Product2 that is linked to an Account, even though that product2 ID is in the Trigger.oldMap keyset');
            System.debug('-- Trigger.oldMap: ' + Trigger.oldMap);
            System.debug('-- Account: ' + a);
        }
      }
       //Remove ProdIDs from delete list that has Account association
        prodIds.removeAll(accPids);             
            
        if(!prodIds.isEmpty() || prodIds.size() != 0)  {
             for(Product_Rate__c delRateObj :[SELECT Id from Product_Rate__c where ProductID__c IN :prodIds ]) {
                    setRateID.add(delRateObj.id);
              }                          
             if(!setRateId.isEmpty())
                 ProdTriggerHandler.OnBeforeDeleteProdAsync(setRateID);
            }   
    }        
       
}

 

works well now I am almost done with my test class. Thanks again to all for helping me find the solution