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
Kenji775Kenji775 

Batch Apex job failing hard

Hey all.

I've tried to write and deploy my first batch apex trigger, btu it's causing some issues. It worked in sandbox, but of course when deployed to production everything went to crap.

 

This is the error I'm getting

 

Apex script unhandled exception by user/organization: 005400000012Tpc/00D400000009zFE

 

Failed to process batch for class 'deleteCampaignMembers' for job id '70740000003FdnJ'

 

caused by: System.DmlException: Delete failed. First exception on row 0 with id 00v4000000MqwniAAB; first error: CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY, deleteCampaignMembers: execution of AfterUpdate

 

caused by: System.AsyncException: Database.executeBatch cannot be called from a batch or future method.

 

Trigger.deleteCampaignMembers: line 17, column 31: []

 

Class.deleteCampaignMembers.execute: line 22, column 3 External entry point

 

Here is my trigger

 

trigger deleteCampaignMembers on Campaign (after update)
{
    Campaign[] campaigns = Trigger.new;
    List<Id> completedCampaigns = new List<Id>();
               
    for (Campaign currentCampaign : campaigns)
    {
        if(currentCampaign.isActive == false && (currentCampaign.status == 'Project Complete' || currentCampaign.status == 'Canceled' ))
        {                   
             completedCampaigns.add(currentCampaign.id);    
        }
    }
    if(completedCampaigns.size() > 0)
    {
         for (Id cid : completedCampaigns)
         {    
            id batchinstanceid = database.executeBatch(new deleteCampaignMembers('select Id from CampaignMember where campaignId = \''+cid+'\''));
        }
    }  
}

 

 

And here is the associated class.

 

global class deleteCampaignMembers implements Database.Batchable<sObject>
{
    global final String Query;
    global deleteCampaignMembers(String q)
    {
        Query=q;
    }

    global Database.QueryLocator start(Database.BatchableContext BC)
    {
        return Database.getQueryLocator(query);
    }

    global void execute(Database.BatchableContext BC,List<CampaignMember> scope)
    {
        List <CampaignMember> lstAccount = new list<CampaignMember>();
        for(Sobject s : scope)
        {
            CampaignMember a = (CampaignMember)s;
            lstAccount.add(a);
        }
        Delete lstAccount;
    }

    global void finish(Database.BatchableContext BC)
    {
        //Send an email to the User after your batch completes
        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
        String[] toAddresses = new String[] {'xxxxxxxxxxx@xxxxxxxxxxx.com'};
        mail.setToAddresses(toAddresses);
        mail.setSubject('Apex Batch Job is done');
        mail.setPlainTextBody('The batch Apex job processed ');
        Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
    }
}

 

 

I'm really not sure what's its talking about. It's really a fairly simple trigger. If a campaign has been deactivated and the status is project complete, delete all the campaign members, then send me an email. That's it. I'm not sure what I'm doing wrong. Any tips would be much appreciated. Thank you.

*werewolf**werewolf*

What happens if you just do the deletes in an @future instead?

BritishBoyinDCBritishBoyinDC

So I wonder if the campaign member update is somehow triggering an update on the parent campaign via another trigger/workflow - so the batch is in effect forcing an update on the parent campaign which would = the batch trying to call a batch? That seems to be what the error message is implying?

 

But more generally, looking at the way it is written, it is creating a batch per campaign record - I believe you can only have 5 batches running at any one time - so I think that is an issue for operating in bulk. You can look at this post for an example of passing in a set to process in one batch, but if for some reason you could have many campaigns updated, it would still fail.

 

But unless there is a compelling reason, I wonder if you would be better off scheduling the batch to run every 30 mins/hour, delete the members, and then mark the status of the updated campaigns as 'Member Removed' or something so it is not processed again in the next batch?

 

Kenji775Kenji775

@werewolf

I didn't know if an @future would support deleting that many rows. I just went with batch because it sounded like it would be better suited for what I needed, also I am already butting up against the limits for how many @future calls I can make because of other triggers.

 

@BritishBoy

I'll check and see if it is launching any other triggers, and see about implimenting a flag and schedule process like you suggested instead.

JA-DevJA-Dev

Do you know if you have read access to the Campaign, Lead and Contact objects in your production instance? I'd check that first.

 

Then in your batch code, instead of creating a new batch instance per each campaign, I'd just store the deactive campaign ids into a set and then just pass that set to the query string:

 

 

trigger deleteCampaignMembers on Campaign (after update) 
{
    Campaign[] campaigns = Trigger.new;
    Set<Id> completedCampaigns = new Set<Id>();
               
    for (Campaign currentCampaign : campaigns)
    {
        if(currentCampaign.isActive == false && (currentCampaign.status == 'Project Complete' || currentCampaign.status == 'Canceled' ))
        {                   
             completedCampaigns.add(currentCampaign.id);    
        }
    } 
    if(completedCampaigns.size() > 0)
    {
         id batchinstanceid = database.executeBatch(new deleteCampaignMembers('select Id from CampaignMember where campaignId IN :' + completedCampaigns)); 
    }  
}

 This way, you would only run one instance of the batch job. However, since you are running the batch job within a trigger, you need to consider the governor limitations that are associated with triggers as well.

 

Hope that helps.

 

Kenji775Kenji775

Thanks, that should help with some of the limits I've been hitting. So then I just need to update each campaign where the campaign members get deleted so they don't get pulled in again every run. Maybe I should just remove the trigger version and make it scheduleable like the BritishBoy suggested. That would make more sense probably.

Kenji775Kenji775

K, so I decided to go with a hybrid batch/scheduable solution. Does this make sense? A scheduable job is called once a day to find all campaigns that are not active, do not have the camapign members deleted flag set, and are of the correct record type. The trigger itself updates the statuses (there should never be more than a handful of campaigns in a day) and the actual query is sent off the batchable interface to do the deleting of the members.

 

 

Scheduable Class

 

global class deleteCampaignMembersScheduable implements Schedulable{
    global void execute(SchedulableContext ctx)
    {
        deleteMembers();
    }
        
    global boolean deleteMembers()
    {
        try
        {
            RecordType parentCampaignRecordType = [select id from RecordType where name = 'FPI Parent Campaign' and SobjectType='campaign' LIMIT 1];
            Campaign[] campaigns = [select Id from campaign where isActive = false and Members_Deleted__c = false and recordTypeId = :parentCampaignRecordType.id];
            Set<Id> completedCampaigns = new Set<Id>();
                       
            for (Campaign campaign : campaigns)
            {   
                completedCampaigns.add(campaign.id);   
                campaign.Members_Deleted__c = true;
            }

            if(completedCampaigns.size() > 0)
            {
                 update campaigns;
                 id batchinstanceid = database.executeBatch(new deleteCampaignMembers('select Id from CampaignMember where campaignId IN :' + completedCampaigns));
            }
            return true;     
        }    
        catch(Exception e)
        {
            return false;  
        }
    }    
}

 

Batch Apex Class

 

global class deleteCampaignMembers implements Database.Batchable<sObject>
{
    global final String Query;
    global deleteCampaignMembers(String q)
    {
        Query=q;
    }

    global Database.QueryLocator start(Database.BatchableContext BC)
    {
        return Database.getQueryLocator(query);
    }

    global void execute(Database.BatchableContext BC,List<CampaignMember> scope)
    {
        List <CampaignMember> lstAccount = new list<CampaignMember>();
        for(Sobject s : scope)
        {
            CampaignMember a = (CampaignMember)s;
            lstAccount.add(a);
        }
        Delete lstAccount;
    }

    global void finish(Database.BatchableContext BC)
    {
        //Send an email to the User after your batch completes
        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
        String[] toAddresses = new String[] {'XXXXXXXX@XXXXXXXXXXX.com'};
        mail.setToAddresses(toAddresses);
        mail.setSubject('Campaign Memberds Deleted');
        mail.setPlainTextBody('The batch Apex job to delete campaign members has processed ');
        Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
    }
}

BritishBoyinDCBritishBoyinDC

I think that is the better approach.

 

Couple of thoughts. I believe looking at another post that you can replace this :

 

 

global void execute(Database.BatchableContext BC,List<CampaignMember> scope)
    {
        List <CampaignMember> lstAccount = new list<CampaignMember>();
        for(Sobject s : scope)
        {
            CampaignMember a = (CampaignMember)s;
            lstAccount.add(a);
        }
        Delete lstAccount;
    }

 

 

with just this

 

 

global void execute(Database.BatchableContext BC,List<CampaignMember> scope)
    {
        
        Delete scope;
    }

 

 

Also, updating the Campaigns before the Batch has run makes me a bit nervous. I would be inclined to pass that Set of Ids into the Batch as part of the constructor, and then update the campaigns as part of the Batch Finish - that way, if the Batch for some reason fails, the Campaigns it was supposed to update would be included in the next run...

Kenji775Kenji775

Sweet, thanks for that tip.

 

I was also nervous about doing the campaign update pre campaign member delete, but I wasn't sure how to pass the Id's in. For some reason that whole scheduleableContext thing weird my out and I'm not sure if I can just pass variables to the method like I normally would or if I need to do anything crazy.

BritishBoyinDCBritishBoyinDC

Take a look at that post I linked to above - looks like you can just declare a Set as a variable in the batch, set the variable as part of the constructor, and then reference that as normal in the batch when you need it...

Kenji775Kenji775

cool. Right now I'm trying to figure out why this class isn't showing up in my scheduled jobs part of the administrator. I implimented the scheduable class, but I cannot figure out to actually schedule it. I know I've done this before and as I recall it should just show up in the scheduled jobs under monitoring, but it's not there. Weird....

BritishBoyinDCBritishBoyinDC

 So I've found in the past that it needs to be 'valid' which usually means you need to have a simple test class in the schedulable class, and run that test to make it valid - then it appears...