Apex Academy
Módulo 3·05·30 min

Variables de enlace y SOQL dinámico

Meter valores de Apex dentro de la consulta con dos puntos, filtrar por una colección entera con IN y construir consultas en tiempo de ejecución sin abrirle la puerta a nadie.

En Admin esEl filtro de un Get Records que usa {!recordId}

✓ En esta sub-lección

  1. 01Usar una variable de Apex dentro de una consulta con :variable.
  2. 02Filtrar por muchos Ids a la vez con IN :conjunto.
  3. 03Saber cuándo hace falta SOQL dinámico y cómo evitar la inyección de SOQL.

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

La subconsulta (SELECT Id FROM Opportunities WHERE IsClosed = false) en el SELECT de cuentas, ¿quita las cuentas sin oportunidades abiertas?

Una consulta con 'Acme' escrito dentro solo sirve para Acme. En código real, el valor del filtro llega de fuera: el registro que se acaba de guardar, lo que un usuario escribió en un buscador, la lista de cuentas que procesas. Esta lección es sobre cómo meter esos valores en la consulta, de la forma segura y de la forma peligrosa.

La nutria: El paralelo de Admin

En un Flow, yo filtraba el Get Records con «AccountId Equals {!$Record.AccountId}»: no escribía un Id, apuntaba a una variable del Flow y el valor se ponía en el momento. En SOQL eso se llama y se escribe con dos puntos: WHERE AccountId = :accId.

Los dos puntos

Apex
Id accId = '0015g00000Xyz12AAB';
String stage = 'Prospecting';
Date since = Date.today().addDays(-30);

List<Opportunity> opps = [
    SELECT Id, Name
    FROM Opportunity
    WHERE AccountId = :accId
      AND StageName = :stage
      AND CreatedDate >= :since
];
:stage no lleva comillas aunque sea un texto: Apex sabe que es un String y lo pasa como valor. Nunca como trozo de código.
  • Detrás de los dos puntos puede ir una variable (:accId), el campo de un objeto (:opp.AccountId) o una expresión (:Date.today().addDays(-30)).
  • El tipo tiene que encajar con el campo: no puedes comparar Amount con un String. Es el tipado estático del Módulo 1 funcionando también dentro de la consulta.

IN :colección: muchos valores de una vez

Aquí está la pieza que más usarás a partir del Módulo 6. Si tienes 200 oportunidades y quieres las cuentas de todas, no haces 200 consultas: juntas los AccountId en un Set<Id> y haces una sola consulta con IN :accountIds. Es el operador «In» del Get Records de Flow, apuntando a una variable de colección.

Apex
// 1. Juntar los Ids (el Set descarta repetidos)
Set<Id> accountIds = new Set<Id>();
for (Opportunity o : opps) {
    accountIds.add(o.AccountId);
}

// 2. Una sola consulta para todas
Map<Id, Account> accountsById = new Map<Id, Account>(
    [SELECT Id, Name, Industry FROM Account WHERE Id IN :accountIds]
);

// 3. Buscar sin recorrer
for (Opportunity o : opps) {
    Account parent = accountsById.get(o.AccountId);
}
new Map<Id, Account>(consulta) construye el mapa Id → registro directamente: es el VLOOKUP del Módulo 2, montado en una línea.

SOQL dinámico: cuando la consulta se decide al ejecutar

La consulta entre corchetes es estática: su forma (qué objeto, qué campos, qué condiciones) está fija al guardar la clase, y por eso el compilador la revisa. A veces la forma no se conoce hasta que el código corre: un buscador donde el usuario elige por qué campo ordenar, o una utilidad que sirve para cualquier objeto. Para eso existe Database.query, que recibe la consulta como texto.

Apex
String sortField = 'Name';   // viene de una opción del usuario
String soql = 'SELECT Id, Name FROM Account ORDER BY ' + sortField + ' LIMIT 50';
List<Account> accs = Database.query(soql);

La nutria: El paralelo de Admin

Es la diferencia entre un informe que yo dejaba configurado y una list view donde cada usuario elige sus propias columnas y filtros. Más flexible, pero ahora lo que llega del usuario forma parte de la definición. Y ahí empieza el riesgo.

La inyección de SOQL

Si pegas con + lo que escribió el usuario dentro del texto de la consulta, el usuario ya no escribe un valor: escribe código. Imagina un buscador de casos por asunto que hace '... WHERE IsClosed = false AND Subject LIKE \'%' + keyword + '%\''. Alguien escribe en el buscador «%' OR Subject LIKE '%» y la consulta resultante ya no filtra casos cerrados: ve todos. Eso es la .

EL AGENTE ESCRIBE

CONCATENANDO CON +

WHERE IsClosed = false AND Subject LIKE '%Impresora%'
  • · Impresora no imprime

CON :pattern

WHERE IsClosed = false AND Subject LIKE :pattern
  • · Impresora no imprime

Con un texto normal las dos versiones devuelven lo mismo. Por eso el problema pasa desapercibido en las pruebas.

Con : el valor viaja en un sobre cerrado. Con + se mezcla con el código de la consulta.
  1. Primera opción: no uses SOQL dinámico. Si solo cambia el valor del filtro, la consulta estática con :variable basta y es inmune a la inyección.
  2. Si necesitas dinámico, pasa los valores como enlaces: Database.queryWithBinds(soql, new Map<String, Object>{ 'kw' => pattern }, AccessLevel.USER_MODE), con :kw dentro del texto.
  3. Lo que no puede ir como enlace (un nombre de campo para ORDER BY) compruébalo contra una lista de valores permitidos. String.escapeSingleQuotes() existe, pero es la última línea de defensa, no la primera.