Apex Academy
Módulo 4·03·35 min

Bulkificación

Escribir código que hace el mismo número de consultas y de DML con un registro que con doscientos. La receta es siempre la misma: juntar, consultar una vez, trabajar en memoria, guardar una vez.

En Admin esEn Flow: asignar a una colección dentro del Loop y un solo Update Records después

✓ En esta sub-lección

  1. 01Reconocer consultas y DML dentro de bucles y explicar por qué fallan con volumen.
  2. 02Aplicar la receta juntar → consultar una vez → trabajar en memoria → guardar una vez.
  3. 03Evitar Ids duplicados en una lista de update usando Set o Map.

La nutria: ¿Te acuerdas? · Repaso de la lección 2

Con Database.insert(leads, false), si uno de 200 falla, ¿qué pasa?

Casi todo el código de este curso funciona con un registro. La pregunta que separa el código de práctica del código de producción es otra: ¿funciona con doscientos a la vez? En Salesforce, esa pregunta no es teórica, porque doscientos a la vez es lo normal.

La nutria: El paralelo de Admin

Yo lo vi por primera vez en Data Loader, en Settings: un «Batch size» que por defecto es 200. Cuando cargas 10.000 oportunidades, Salesforce las guarda en de 200, y cada lote es una transacción: tus triggers y flows reciben 200 registros de golpe, no uno. Lo mismo pasa con una actualización masiva desde una list view o con una integración. Tu código tiene que estar escrito pensando en ese lote.

El código que funciona… hasta que no

Apex
// Marcar como Hot las cuentas de las oportunidades ganadas hoy
List<Opportunity> wonToday = [SELECT Id, AccountId FROM Opportunity
                              WHERE IsWon = true AND CloseDate = TODAY];

for (Opportunity o : wonToday) {
    Account acc = [SELECT Id, Rating FROM Account WHERE Id = :o.AccountId];  // ❌ consulta en el bucle
    acc.Rating = 'Hot';
    update acc;                                                              // ❌ DML en el bucle
}
Con 3 oportunidades: 4 consultas y 3 DML. Con 150: 151 consultas y la transacción muere en la 101.

Este código pasa cualquier prueba hecha a mano, porque a mano ganas una oportunidad cada vez. El día que Ventas cierra el trimestre con una carga masiva, falla. Y falla entero: la no se puede capturar, así que se deshace todo lo que había hecho la transacción, incluido lo que guardaron los flows.

La receta en cuatro pasos

for (Opportunity o : wonToday) {
    Account a = [SELECT … WHERE Id = :o.AccountId];
    a.Rating = 'Hot';
    update a;
}
Consultas SOQL4 / 100
Instrucciones DML3 / 150

Con 3: 4 consultas y 3 DML. Funciona… hasta el cierre de trimestre.

Mueve el número de oportunidades y cambia de versión: una hace un viaje a la base de datos por registro; la otra, dos viajes para todos.
Apex
List<Opportunity> wonToday = [SELECT Id, AccountId FROM Opportunity
                              WHERE IsWon = true AND CloseDate = TODAY];

// 1. Juntar: los Ids que necesitas, sin repetir
Set<Id> accountIds = new Set<Id>();
for (Opportunity o : wonToday) {
    accountIds.add(o.AccountId);
}

// 2. Consultar una vez
List<Account> accounts = [SELECT Id, Rating FROM Account WHERE Id IN :accountIds];

// 3. Trabajar en memoria
for (Account a : accounts) {
    a.Rating = 'Hot';
}

// 4. Guardar una vez
update accounts;
Con 3 oportunidades: 2 consultas y 1 DML. Con 200: 2 consultas y 1 DML.

La nutria: Flow te enseñó esto primero

Yo aprendí esto en Flow antes que en Apex: dentro del Loop no pones Update Records; pones un Assignment que añade el registro a una variable de colección, y después del Loop, un único Update Records con esa colección. Si alguna vez lo hiciste así, ya sabes . La diferencia es que Flow Builder te avisa de algunos de estos errores, mientras que en Apex la responsabilidad es tuya.

La nutria: ¿Y por qué no un Flow? Aquí Flow habría acertado solo

Te debo una confesión: un flow desencadenado por registro que actualiza la cuenta de cada oportunidad ganada habría aguantado el cierre de trimestre, porque Salesforce junta en un solo guardado los Update Records de las interviews del mismo lote. Apex no hace eso por ti: si tu código guarda dentro del bucle, guarda 180 veces. Bulkificar en Apex es responsabilidad tuya, y por eso existe esta lección.

La trampa del Id duplicado

Si dos oportunidades ganadas son de la misma cuenta y vas añadiendo cuentas a una List dentro del bucle, la lista acaba con la misma cuenta dos veces. update de una lista con Ids repetidos falla con «Duplicate id in list». Por eso el paso 1 usa un Set: guarda cada Id una sola vez. Si además necesitas ir modificando los registros mientras recorres, usa un Map<Id, Account>: put con la misma clave sustituye en lugar de duplicar, y al final haces update mapa.values().

Apex
Map<Id, Account> toUpdate = new Map<Id, Account>();
for (Opportunity o : wonToday) {
    toUpdate.put(o.AccountId, new Account(Id = o.AccountId, Rating = 'Hot'));
}
update toUpdate.values();   // cada cuenta una sola vez, sin haberla consultado
Aquí ni siquiera hace falta consultar: el update sin consulta de la lección 1, combinado con el Map del Módulo 1.

Cómo detectar código sin bulkificar

  • Un corchete de consulta [SELECT … ] dentro de las llaves de un for o de un while.
  • insert, update, delete o upsert dentro de un bucle, o Database.insert/update llamados dentro.
  • Un método que recibe un solo registro (Account acc) y hace una consulta o un DML, llamado desde un bucle. Es el mismo error escondido un nivel más abajo: los métodos bien diseñados reciben listas.

La nutria: Bajar el Batch size a 1 no arregla nada

Confieso que alguna vez lo hice: cuando una carga fallaba con «Too many SOQL queries», bajaba el Batch size de Data Loader a 1 para que pasara. Pasa, pero el problema sigue ahí: la próxima integración, la próxima actualización masiva desde una list view o el siguiente Admin que use el tamaño por defecto mandará 200 registros de golpe. Incluso con la Bulk API, que acepta lotes mucho más grandes, los triggers reciben los registros en bloques de 200. La solución no está en la carga, está en el código.