Tablas relacionales con each
Se usa cuando un registro representa en realidad varias filas — un cliente con un puñado de pedidos, una factura con sus partidas, una publicación con sus etiquetas — y usted las quiere como filas separadas en una tabla hija, cada una cargando una llave foránea de vuelta al padre, todo desde una sola configuración.
El atributo each repite una
<line> una vez por cada elemento de una
lista. Una tarjeta se convierte en N filas de salida — una tabla normalizada, lista
para una base de datos. Apuntado a un <pool>, imprime el pool
registro por registro, así que una tabla de consulta sale de la misma configuración que las
filas que la referencian.
Las salidas de ejemplo que siguen son ilustrativas. Los valores exactos que emite un generador pueden cambiar entre versiones del core y entre semillas; lo que la característica garantiza son los conteos y las reglas estructurales (qué filas aparecen, cuáles quedan vacías, que las llaves sean únicas).
- Alas filas padre, cada una con su propia llave
- Blas filas hijas — todas y cada una cargan la llave de su padre, así que nada queda huérfano
De un vistazo
| Dónde | Qué |
|---|---|
| Se aplica en | <line> |
| Valor | El nombre de una secuencia cuyo generador tiene repeat, o de un <pool> |
| Efecto | La línea se emite una vez por cada elemento de esa lista, o una vez por cada registro de ese pool |
Una secuencia destino debe llevar repeat: eso es lo
que la convierte en una lista recorrible. Apunte each a cualquier otra cosa y TDC le
dice por qué (vea la última sección de abajo).
El problema: una lista embutida en una sola tarjeta
Un cliente hizo tres pedidos. En la tarjeta quedan como una lista:
1;VIP;8648,7170,7063
Eso no sirve para una base de datos. Usted necesita una tabla orders donde cada
pedido sea su propia fila, enlazada de vuelta al cliente. Una tarjeta tiene que
producir tres filas, no una fila con una lista adentro.
La herramienta
Déle a los pedidos su propia lista con repeat y luego
ponga each en la línea del pedido para que se dispare una vez por pedido:
<env count="4" seed="each-demo" inject="${{%}}" local="es">
<sequence name="Id"><gen type="increment" value="1"/></sequence>
<sequence name="Name"><gen type="template" value="person.male.firstName"/></sequence>
<sequence name="Tier"><gen type="text" value="VIP,normal" percent="50,50"/></sequence>
<sequence name="VipOrders" parent="Tier.VIP">
<gen type="number" value="1000..9999" repeat="2..3"/>
</sequence>
<sequence name="StdOrders" parent="Tier.normal">
<gen type="number" value="100..999" repeat="0..2"/>
</sequence>
</env>
<block>
<line><data>INSERT INTO customers VALUES (${{Id}}, '${{Name}}', '${{Tier}}');</data></line>
<line each="VipOrders"><data>INSERT INTO orders VALUES (${{_item_id}}, ${{Id}}, ${{VipOrders}});</data></line>
<line each="StdOrders"><data>INSERT INTO orders VALUES (${{_item_id}}, ${{Id}}, ${{StdOrders}});</data></line>
</block>
Lo que obtiene
INSERT INTO customers VALUES (1, 'Carlos', 'normal'); INSERT INTO orders VALUES (4, 1, 433); INSERT INTO orders VALUES (5, 1, 474); INSERT INTO customers VALUES (2, 'Delfín', 'normal'); INSERT INTO customers VALUES (3, 'Salvador', 'VIP'); INSERT INTO orders VALUES (11, 3, 2460); INSERT INTO orders VALUES (12, 3, 5137); INSERT INTO orders VALUES (13, 3, 7717); INSERT INTO customers VALUES (4, 'Ulises', 'VIP'); INSERT INTO orders VALUES (16, 4, 5249); INSERT INTO orders VALUES (17, 4, 2324);
La línea del pedido está escrita una sola vez en la configuración, y sin embargo se
imprime tantas veces como pedidos tenga el cliente. El cliente 2 (Rafael) sacó cero
pedidos, así que no hay ninguna fila de pedido para él, y tampoco queda un hueco vacío de
relleno. Dos clientes VIP sacan de VipOrders y los clientes normal de StdOrders,
y en cada fila se recorre la lista correcta porque
parent ya decidió cuál lista está activa.
Por qué/cuándo: esta es la manera de emitir, desde una sola configuración, una tabla padre y su tabla hija juntas y correctamente enlazadas — sin post-procesamiento, sin una segunda corrida, sin scripts para desplegar la lista.
Qué se ve dentro de una línea con each
Dentro de una línea bajo each, el nombre de la secuencia recorrida significa el
elemento actual, más dos integrados extra:
| Token | Qué significa |
|---|---|
${{VipOrders}} | el elemento actual. Fuera de la línea con each, el mismo nombre es la lista completa unida |
${{_item}} | la posición dentro de la tarjeta: 1, 2, 3 |
${{_item_id}} | un número global, único en toda la corrida — su llave primaria |
| todo lo demás | como siempre: ${{Id}}, ${{_count}}, cualquier otra secuencia |
Justamente por esto funciona la llave foránea: ${{Id}} sigue significando el
cliente en cada fila de pedido, no el elemento. Si al recorrer se reasignara Id al
elemento, cada pedido apuntaría al lugar equivocado. Las columnas del padre se quedan
fijas; solo avanzan el nombre recorrido y _item / _item_id.
El nombre recorrido también lleva las posiciones de la línea:
| Token | Qué significa |
|---|---|
${{VipOrders._item}} | el número de esta línea entre las que imprime el each: 1, 2, 3 |
${{VipOrders._first}} | true en la primera línea impresa, false en las demás |
${{VipOrders._last}} | true en la última línea impresa, false en las demás |
Llevan el nombre de la lista recorrida, así que nunca se mezclan con los
_first y _last propios del run, que marcan el primer y el
último registro. _last en una lista recorrida es lo que pone una coma entre los
elementos y no después del último: <data if="!VipOrders._last">,</data>.
Un if= en una línea con each, y en cada
<data> dentro de ella, se evalúa para cada elemento, con el elemento a la vista:
<line each="VipOrders" if="VipOrders > 5000"> imprime solo los pedidos grandes. Las
posiciones cuentan las líneas que pasan el filtro, así que VipOrders._last es la última
línea que de verdad se imprime.
Sobre los números de pedido
Mire las llaves primarias: 4, 5, luego 11, 12, 13, luego 16, 17. Suben, pero
con brincos.
Es a propósito. _item_id se calcula a partir del número de la tarjeta, de modo que una
fila puede producirse sin conocer a sus vecinas — que es lo que mantiene el
paralelismo de --jobs idéntico byte por byte a una corrida de un
solo hilo. El precio son los brincos donde una tarjeta tiene menos pedidos que el máximo.
Para una llave primaria eso está perfectamente bien: las bases de datos reales tampoco
suelen tener ids sin brincos.
La unicidad sigue siendo a prueba de balas, incluso entre varias listas: StdOrders
usa 4, 5 mientras VipOrders usa 11, 12, 13, y los dos espacios nunca chocan. En una
verificación más grande, 2000 tarjetas produjeron 3501 pedidos con 3501 llaves distintas,
y cero pedidos apuntando a un cliente inexistente.
Por qué/cuándo: confíe en _item_id como llave sustituta estable, única y segura en
paralelo. Use _item en cambio cuando quiera un número de orden por tarjeta (1, 2, 3
que reinicia en cada padre).
Un pool entero, registro a registro
Un <pool> es una tabla pequeña que se construye una vez, antes de
las filas, y las filas sortean sus miembros de ella. each con el nombre del pool imprime esa
tabla: una línea por registro, en el orden del pool, y cada campo se lee como
${{Pool.field}}. Esa es la tabla de consulta a la que apuntan las llaves foráneas de las
filas:
<env count="4" seed="visits" local="en">
<pool name="Doctors" count="3">
<sequence name="id"><gen type="increment" value="1"/></sequence>
<sequence name="last"><gen type="template" value="person.lastName"/></sequence>
</pool>
<sequence name="Id"><gen type="increment" value="1"/></sequence>
<sequence name="Doc"><gen type="pool" value="Doctors"/></sequence>
<before>
<line each="Doctors"><data>INSERT INTO doctors VALUES (${{Doctors.id}}, '${{Doctors.last | sql}}');</data></line>
</before>
</env>
<block>
<line><data>INSERT INTO visits VALUES (${{Id}}, ${{Doc.id}});</data></line>
</block>
INSERT INTO doctors VALUES (1, 'Smith'); INSERT INTO doctors VALUES (2, 'Johnson'); INSERT INTO doctors VALUES (3, 'Williams'); INSERT INTO visits VALUES (1, 1); INSERT INTO visits VALUES (2, 3); INSERT INTO visits VALUES (3, 2); INSERT INTO visits VALUES (4, 2);
Cada médico que nombra una visita existe en doctors, porque ambos salen del mismo pool. El
pool es el mismo en todas las filas, así que una línea que lo recorre imprime la tabla entera
cada vez que corre: para imprimirla una sola vez, póngala en
<before> o <after>, o en un run con
count="1".
Un campo que sortea de otro pool es un registro un nivel más abajo, y se lee igual:
${{Doctors.dept.name}}. Las posiciones funcionan como en una lista — Doctors._last es el
último registro impreso — y un filtro también:
<line><data>[</data></line>
<line each="Doctors" if="Doctors.shift == day"><data> ${{Doctors.last | json}}</data><data if="!Doctors._last">,</data></line>
<line><data>]</data></line>
[ "Johnson", "Smith" ]
El pool tiene cinco médicos, y el último trabaja de noche, así que el filtro lo descarta.
Doctors._last cae en Smith, el último registro impreso, y el arreglo se cierra sin una
coma colgando. Formatos de salida → JSON arma así un
documento entero a partir de dos pools.
Varias líneas por elemento: <group>
each en una <line> repite una línea. Cuando un elemento necesita varias — un
departamento, luego sus médicos, luego un corchete de cierre —, envuelva las líneas en un
<group>. <group each="…"> imprime todo lo que contiene una vez por elemento, en orden, con
ese elemento a la vista de cada línea:
<block>
<line><data>[</data></line>
<group each="Depts">
<line><data> {"name": ${{Depts.name | json}}, "doctors": [</data></line>
<group each="Doctors" if="Doctors.dept.id == Depts.id">
<line><data> {"name": ${{Doctors.last | json}},</data></line>
<line><data> "languages": ${{Doctors.languages | json:;}}}</data><data if="!Doctors._last">,</data></line>
</group>
<line><data> ]}</data><data if="!Depts._last">,</data></line>
</group>
<line><data>]</data></line>
</block>
Los pools Depts y Doctors son los de
Formatos de salida → Dos tablas en un solo documento.
[
{"name": "Cardiology", "doctors": [
{"name": "Williams",
"languages": ["English", "Polish"]}
]},
{"name": "Neurology", "doctors": [
{"name": "Smith",
"languages": ["English"]},
{"name": "Johnson",
"languages": ["English"]}
]}
]El grupo exterior imprime tres líneas por cada departamento. Dentro de él, ${{Depts.name}} es el
departamento que se está imprimiendo, así que el grupo interior, que recorre todos los médicos,
se queda con los que tienen el mismo dept.id. Cada nivel cuenta sus propias posiciones:
Doctors._last es el último médico impreso en este departamento y vuelve a empezar en el
siguiente, y Depts._last es el último departamento. Por eso cada coma cae donde JSON la
necesita. Un grupo puede contener líneas y otros grupos, tan hondo como llegue el documento.
Sin each, el if= de un grupo conserva o descarta todas sus líneas a la vez — una condición en
lugar de la misma copiada en cada línea:
<block>
<line><data>#${{Id}} ${{Kind}}</data></line>
<group if="Kind == visit">
<line><data> room ${{Room}}</data></line>
<line><data> check-in at the front desk</data></line>
</group>
</block>
#1 call #2 visit room 122 check-in at the front desk #3 visit room 116 check-in at the front desk #4 call
Una línea dentro de un grupo conserva su propio if= y puede recorrer una lista propia con
each. El if= de un grupo puede leer el elemento que recorre, pero no sus posiciones, que
cuentan los elementos que ese if= deja pasar (TDC215). Una columna tipada — <data name="…"> —
no puede estar en ningún lugar dentro de un grupo (TDC209): el grupo imprime sus líneas varias
veces o ninguna, y una columna tiene una celda por registro.
Un ciclo de vida, no solo una lista
Las filas de un registro no tienen que ser un saco de valores sin relación. Pueden ser una
historia: un pedido que va created → paid → shipped → delivered, una fila por paso, y
nunca un paso fuera de orden.
Lo hacen dos piezas. Un <mix> elige qué camino toma este pedido, e
if en cada <line> decide si ese paso
pertenece al camino — así los pasos se escriben en el orden en que ocurren y solo se
emiten las líneas del camino elegido:
<env count="20" seed="lifecycle" local="en">
<sequence name="OrderId"><gen type="increment" value="1000"/></sequence>
<mix name="Outcome" percent="60,25,15">
<case><gen type="text" value="delivered"/></case>
<case><gen type="text" value="refunded"/></case>
<case><gen type="text" value="cancelled"/></case>
</mix>
</env>
<block>
<line><data>${{OrderId}},1,created</data></line>
<line if="Outcome.delivered"><data>${{OrderId}},2,paid</data></line>
<line if="Outcome.delivered"><data>${{OrderId}},3,shipped</data></line>
<line if="Outcome.delivered"><data>${{OrderId}},4,delivered</data></line>
<line if="Outcome.refunded"><data>${{OrderId}},2,paid</data></line>
<line if="Outcome.refunded"><data>${{OrderId}},3,refunded</data></line>
<line if="Outcome.cancelled"><data>${{OrderId}},2,cancelled</data></line>
</block>
1000,1,created 1000,2,paid 1000,3,shipped 1000,4,delivered 1001,1,created 1001,2,paid 1001,3,refunded 1002,1,created 1002,2,paid 1002,3,shipped 1002,4,delivered 1003,1,created 1003,2,paid 1003,3,shipped 1003,4,delivered 1004,1,created 1004,2,paid 1004,3,refunded
Cada pedido toma un camino legal — uno enviado se pagó primero, uno cancelado nunca se
envió — y los desenlaces caen en las proporciones exactas que declaró <mix>. El número de
paso se escribe en la línea en vez de contarse, porque las líneas de un camino ya se
conocen cuando se escribe el config.
Una sola secuencia puede llevar el camino entero:
<gen type="text" value="created,paid,shipped,delivered" repeat="4" order="sequential"/>
más each para desplegarla. Un repeat fijo sobre una
lista recorrida toma N valores del recorrido por fila, así que una lista de cuatro leída de
cuatro en cuatro es el camino entero en cada registro:
<sequence name="Order"><gen type="increment" value="1000"/></sequence>
<sequence name="Step"><gen type="text" value="created,paid,shipped,delivered" repeat="4" order="sequential"/></sequence>
...
<line each="Step"><data>${{Order}},${{Step}}</data></line>
1000,created 1000,paid 1000,shipped 1000,delivered 1001,created 1001,paid
Úsela cuando cada registro sigue el MISMO camino. El <mix> de arriba es para cuando
no lo hacen: allí un pedido se cancela y otro se entrega, en proporciones declaradas, y
ninguna lista recorrida puede decir eso. Las reglas del recorrido y las dos formas que
rechaza están en order= / cycle=.
legal — uno enviado se pagó antes, uno cancelado nunca se
envió — y los desenlaces caen en las proporciones exactas que declaró <mix>. De las tres
líneas solo se dispara una por registro, porque parent deja vacías las otras dos.
Por qué está hecho así. Una columna de estado que cambiara «mirando la fila anterior» obligaría a calcular la ejecución en orden, desde la primera fila. Elegir un camino entero por adelantado y desplegarlo mantiene cada registro independiente — así que esto funciona en los motores de streaming y en paralelo, sin cambios.
La misma forma cubre cualquier cosa con un vocabulario fijo de pasos: un ticket de soporte
(open → assigned → resolved), un envío, una cola de moderación, un onboarding.
Dónde no va a funcionar
each es estricto sobre lo que puede recorrer, y falla de forma ruidosa en vez de
adivinar:
| Qué | Por qué | Error |
|---|---|---|
each sobre una secuencia sin repeat | no hay nada que recorrer | TDC207 |
each sobre un nombre que no es ni una secuencia ni un pool | no existe nada con ese nombre, y la línea no imprimiría nada en absoluto | TDC206 |
<data name="…"> dentro de una línea con each o de un <group> | un <data> con nombre es una columna para Parquet, y Parquet junta columnas por tarjeta, no por fila recorrida | TDC209 |
Orders._last en el if= de la misma línea con each o del mismo grupo | las posiciones cuentan las líneas que deja pasar ese if=, así que no pueden decidir cuáles son | TDC215 |
error[TDC207]: each="Tier" — that sequence holds one value, not a list note: Add repeat= to its <gen>, e.g. <gen … repeat="1..5"/>, or drop each=.
Para la salida en Parquet no necesita each para nada: una lista con
repeat se queda como una lista real dentro de la
columna, que ya es la forma correcta para un archivo columnar. each es la herramienta
para los formatos de texto — SQL, CSV, JSON lines — donde una tarjeta debe volverse
varias filas físicas.
Vea también
- Dependencias jerárquicas —
parent, que decide cuál lista está activa en cada fila. - Datos coherentes y relacionales — la otra manera de relacionar tablas, por búsqueda contra un padre compartido.
repeat/separator— cómo una secuencia se convierte en lista, para empezar.- Integrados —
_item,_item_id,_county compañía. - Pools — las tablas que puede imprimir una línea con
each, y cómo las filas sortean de ellas.