Desencadenadores de Apex masivos
Objetivos de aprendizaje
Después de completar esta unidad, podrá hacer lo siguiente:
- Escribir desencadenadores que funcionen en colecciones de sObjects.
- Escribir desencadenadores que realicen operaciones SOQL y DML eficientes.
Patrones de diseño de desencadenadores masivos
Los desencadenadores de Apex se han optimizado para funcionar de forma masiva. Se recomienda el uso de patrones de diseño masivos para procesar registros en desencadenadores. Mediante el uso de patrones de diseño masivos, mejora el desempeño de los desencadenadores, se consumen menos recursos de servidor y es menos probable que se superen los límites de la plataforma.
La ventaja de procesar el código de forma masiva es que este puede procesar un elevado número de registros de forma eficiente y ejecutarse dentro de los límites regulados en la Plataforma Lightning. Estos límites regulados se incluyen para garantizar que el código no controlado no monopolice los recursos de la plataforma de múltiples arrendatarios.
En las siguientes secciones se describen los métodos principales para procesar de forma masiva el código Apex en desencadenadores (realización de operaciones en todos los registros del desencadenador y aplicación simultánea de SOQL y DML a colecciones de sObjects en lugar de a sObjects individuales). Las mejores prácticas de procesamiento masivo de SOQL y DML se aplican a todo el código Apex, lo que incluye SOQL y DML en clases. Los ejemplos incluidos se basan en desencadenadores y se usa la variable de contexto Trigger.New
.
Realización de operaciones en conjuntos de registros
En primer lugar, vamos a examinar el concepto de diseño masivo más básico de los desencadenadores. Los desencadenadores procesados de forma masiva realizan operaciones en todos los sObjects en el contexto de los desencadenadores. Normalmente, los desencadenadores realizan operaciones en un registro si la acción que ha activado el desencadenador se origina en la interfaz de usuario. Sin embargo, si el origen de la acción ha sido DML masivo o la API, el desencadenador realiza operaciones en un conjunto de registros en lugar de en un solo registro. Por ejemplo, si importa muchos registros con la API, los desencadenadores realizan operaciones en todo el conjunto de registros. Por consiguiente, una práctica de programación adecuada es dar por supuesto siempre que el desencadenador realiza operaciones en una colección de registros de modo que funcione en todos los casos.
En el caso del siguiente desencadenador (MyTriggerNotBulk
), se da por supuesto que tan solo un registro ha activado el desencadenador. Este desencadenador no funciona en un conjunto de registros completo si se insertan varios registros en la misma transacción. En el siguiente ejemplo se muestra una versión procesada de forma masiva.
trigger MyTriggerNotBulk on Account(before insert) { Account a = Trigger.new[0]; a.Description = 'New description'; }
Este ejemplo (MyTriggerBulk
) es una versión modificada de MyTriggerNotBulk
. Se usa un bucle for
para iterar en todos los sObjects disponibles. Este bucle funciona si Trigger.New
contiene un solo sObject o muchos sObjects.
trigger MyTriggerBulk on Account(before insert) { for(Account a : Trigger.new) { a.Description = 'New description'; } }
Aplicación de SOQL masivo
Las consultas SOQL pueden ser muy útiles. Puede recuperar registros relacionados y comprobar una combinación de múltiples condiciones en una sola consulta. Las funciones de SOQL permiten escribir menos código y realizar menos consultas a la base de datos. La reducción del número de consultas a la base de datos contribuye a evitar alcanzar los límites de consultas, los cuales se corresponden con 100 consultas SOQL para Apex sincrónico o 200 para Apex asíncrono.
En el siguiente desencadenador (SoqlTriggerNotBulk
) se muestra un patrón de consulta SOQL que se debe evitar. El ejemplo realiza una consulta SOQL dentro de un bucle for
para obtener las oportunidades relacionadas con cada cuenta, que se ejecuta una vez para cada cuenta sObject en Trigger.new
. Si tiene una larga lista de cuentas, una consulta SOQL en un bucle for
puede generar demasiadas consultas SOQL. En el siguiente ejemplo se muestra el método recomendado.
trigger SoqlTriggerNotBulk on Account(after update) { for(Account a : Trigger.new) { // Get child records for each account // Inefficient SOQL query as it runs once for each account! Opportunity[] opps = [SELECT Id,Name,CloseDate FROM Opportunity WHERE AccountId=:a.Id]; // Do some other processing } }
El ejemplo siguiente (SoqlTriggerBulk
) es una versión modificada de otra anterior y en él se muestra una mejor práctica para ejecutar consultas SOQL. La consulta SOQL hace la mayor parte del trabajo y se llama a ella una vez fuera del bucle principal.
- La consulta SOQL usa una consulta interna (
SELECT Id FROM Opportunities
) para obtener las oportunidades relacionadas de las cuentas. - La consulta SOQL se conecta a los registros del contexto del desencadenador mediante el uso de la cláusula
IN
y el enlace de la variableTrigger.new
en la cláusulaWHERE
(WHERE Id IN :Trigger.new
). Esta condiciónWHERE
filtra las cuentas para obtener solo los registros que activaron este desencadenador.
La combinación de las dos partes de los resultados de la consulta genera los registros correspondientes en una sola llamada (las cuentas de este desencadenador con las oportunidades relacionadas de cada cuenta).
Una vez obtenidos los registros y sus registros relacionados, el bucle for
itera en los registros de interés mediante el uso de la variable de recopilación (en este caso, acctsWithOpps
). La variable de colección contiene los resultados de la consulta SOQL. De este modo, el bucle for
itera solo en los registros en los que se van a realizar operaciones. Dado que ya se han obtenido los registros relacionados, no se requieren consultas adicionales en el bucle para obtener dichos registros.
trigger SoqlTriggerBulk on Account(after update) { // Perform SOQL query once. // Get the accounts and their related opportunities. List<Account> acctsWithOpps = [SELECT Id,(SELECT Id,Name,CloseDate FROM Opportunities) FROM Account WHERE Id IN :Trigger.new]; // Iterate over the returned accounts for(Account a : acctsWithOpps) { Opportunity[] relatedOpps = a.Opportunities; // Do some other processing } }
De forma alternativa, si no necesita los registros principales de cuenta, puede recuperar solo las oportunidades relacionadas con las cuentas en el contexto de este desencadenador. Esta lista se especifica en la cláusula WHERE
mediante la coincidencia con el campo AccountId
de la oportunidad con el ID
de las cuentas de Trigger.New
: WHERE AccountId IN :Trigger.new
. Las oportunidades devueltas son para todas las cuentas en el contexto de este desencadenador y no para una cuenta específica. En el siguiente ejemplo se muestra la consulta usada para obtener todas las oportunidades relacionadas.
trigger SoqlTriggerBulk on Account(after update) { // Perform SOQL query once. // Get the related opportunities for the accounts in this trigger. List<Opportunity> relatedOpps = [SELECT Id,Name,CloseDate FROM Opportunity WHERE AccountId IN :Trigger.new]; // Iterate over the related opportunities for(Opportunity opp : relatedOpps) { // Do some other processing } }
Puede reducir el tamaño del ejemplo anterior mediante la combinación de la consulta SOQL con el bucle for
en una sola declaración: bucle for
de SOQL. A continuación, se muestra otra versión de este desencadenador masivo mediante un bucle for
de SOQL.
trigger SoqlTriggerBulk on Account(after update) { // Perform SOQL query once. // Get the related opportunities for the accounts in this trigger, // and iterate over those records. for(Opportunity opp : [SELECT Id,Name,CloseDate FROM Opportunity WHERE AccountId IN :Trigger.new]) { // Do some other processing } }
Aplicación de DML masivo
Al hacer llamadas a DML en un desencadenador o una clase, realice estas llamadas a DML en una colección de sObjects siempre que sea posible. La aplicación de DML a cada sObject individualmente implica un uso ineficiente de los recursos. El tiempo de ejecución de Apex permite hasta 150 llamadas a DML en una sola transacción.
Este desencadenador (DmlTriggerNotBulk
) realiza una llamada de actualización en un bucle for
que itera en las oportunidades relacionadas. Si se cumplen determinadas condiciones, el desencadenador actualiza la descripción de las oportunidades. En este ejemplo, se llama una vez a la declaración update de forma ineficiente para cada oportunidad. Si una operación de actualización de cuentas masiva activa el desencadenador, puede haber muchas cuentas. Si cada cuenta tiene una o dos oportunidades, podemos terminar fácilmente con más de 150 oportunidades. El límite de la declaración DML es de 150 llamadas.
trigger DmlTriggerNotBulk on Account(after update) { // Get the related opportunities for the accounts in this trigger. List<Opportunity> relatedOpps = [SELECT Id,Name,Probability FROM Opportunity WHERE AccountId IN :Trigger.new]; // Iterate over the related opportunities for(Opportunity opp : relatedOpps) { // Update the description when probability is greater // than 50% but less than 100% if ((opp.Probability >= 50) && (opp.Probability < 100)) { opp.Description = 'New description for opportunity.'; // Update once for each opportunity -- not efficient! update opp; } } }
Este siguiente ejemplo (DmlTriggerBulk
) muestra cómo realizar declaraciones DML de forma masiva eficientes con una sola llamada a DML en una lista de oportunidades. En el ejemplo se agrega el sObject de oportunidad a una lista de oportunidades (oppsToUpdate
) en el bucle. A continuación, el desencadenador realiza la llamada a DML fuera del bucle en la lista una vez que se han agregado todas las oportunidades a la lista. Este patrón usa solo una llamada a DML con independencia del número de sObjects que se van a actualizar.
trigger DmlTriggerBulk on Account(after update) { // Get the related opportunities for the accounts in this trigger. List<Opportunity> relatedOpps = [SELECT Id,Name,Probability FROM Opportunity WHERE AccountId IN :Trigger.new]; List<Opportunity> oppsToUpdate = new List<Opportunity>(); // Iterate over the related opportunities for(Opportunity opp : relatedOpps) { // Update the description when probability is greater // than 50% but less than 100% if ((opp.Probability >= 50) && (opp.Probability < 100)) { opp.Description = 'New description for opportunity.'; oppsToUpdate.add(opp); } } // Perform DML on a collection update oppsToUpdate; }
Proceso de patrón de diseño masivo: Ejemplo de obtención de registros relacionados
Vamos a aplicar los patrones de diseño que ya conoce mediante la escritura de un desencadenador que acceda a las oportunidades relacionadas de las cuentas. Modifique el ejemplo de desencadenador de la unidad anterior para el desencadenador AddRelatedRecord
. El desencadenador AddRelatedRecord
realiza operaciones de forma masiva, pero el de la unidad anterior no es tan eficiente como debería serlo ya que itera en todos los registros de sObject de Trigger.New
. En el siguiente ejemplo, se modifica tanto el código del desencadenador como la consulta SOQL para obtener solo los registros de interés y luego iterar sobre esos registros. Si no ha creado este desencadenador, no se preocupe, ya que puede hacerlo en esta sección.
Empecemos por revisar los requisitos del desencadenador AddRelatedRecord
. El desencadenador se activa una vez que se insertan o actualizan las cuentas. El desencadenador agrega una oportunidad predeterminada para cada cuenta que aún no tenga ninguna oportunidad.
En primer lugar, las cuentas recién insertadas nunca tienen una oportunidad predeterminada, así que definitivamente tenemos que agregar una. En el caso de las cuentas actualizadas, tenemos que determinar si tienen una oportunidad relacionada o no. Entonces, debemos separar cómo procesamos las inserciones y las actualizaciones mediante una instrucción switch
en la variable de contexto Trigger.operationType
. Luego, debemos realizar un seguimiento de las cuentas que necesitamos procesar con una variable toProcess
. Por ejemplo:
List<Account> toProcess = null; switch on Trigger.operationType { when AFTER_INSERT { // do stuff } when AFTER_UPDATE { // do stuff } }
Para todas las inserciones de cuentas, simplemente asignamos las nuevas cuentas a la lista toProcess
:
when AFTER_INSERT { toProcess = Trigger.New; }
Para las actualizaciones, debemos averiguar qué cuentas existentes en este desencadenador aún no tienen una oportunidad relacionada. Dado que este es un desencadenador posterior, podemos consultar los registros afectados en la base de datos. Aquí está la declaración SOQL, cuyos resultados asignamos a la lista toProcess
.
when AFTER_UPDATE { toProcess = [SELECT Id,Name FROM Account WHERE Id IN :Trigger.New AND Id NOT IN (SELECT AccountId FROM Opportunity WHERE AccountId in :Trigger.New)]; }
Ahora utilizamos un bucle for
para crear una iteración mediante la lista de cuentas toProcess
y agregar una oportunidad predeterminada relacionada a oppList
. Cuando terminemos, agregaremos la lista de oportunidades mediante la declaración DML insert
. Así es cómo se crea o actualiza el desencadenador completo.
- Si ya creó el desencadenador
AddRelatedRecord
en la unidad anterior, modifíquelo mediante la sustitución de su contenido por el siguiente desencadenador. En caso contrario, agregue el siguiente desencadenador mediante Developer Console e ingreseAddRelatedRecord
como nombre del desencadenador.trigger AddRelatedRecord on Account(after insert, after update) { List<Opportunity> oppList = new List<Opportunity>(); // Add an opportunity for each account if it doesn't already have one. // Iterate over accounts that are in this trigger but that don't have opportunities. List<Account> toProcess = null; switch on Trigger.operationType { when AFTER_INSERT { // All inserted Accounts will need the Opportunity, so there is no need to perform the query toProcess = Trigger.New; } when AFTER_UPDATE { toProcess = [SELECT Id,Name FROM Account WHERE Id IN :Trigger.New AND Id NOT IN (SELECT AccountId FROM Opportunity WHERE AccountId in :Trigger.New)]; } } for (Account a : toProcess) { // Add a default opportunity for this account oppList.add(new Opportunity(Name=a.Name + ' Opportunity', StageName='Prospecting', CloseDate=System.today().addMonths(1), AccountId=a.Id)); } if (oppList.size() > 0) { insert oppList; } }
- Para probar el desencadenador, cree una cuenta en la interfaz de usuario de Salesforce y asígnele el nombre
Lions & Cats
(Leones y gatos). - En la lista relacionada de oportunidades de la página de la cuenta, busque la oportunidad nueva
Lions & Cats
. El desencadenador ha agregado la oportunidad automáticamente.
Recursos