Связанные таблицы через each
Когда пригодится — когда одна запись на самом деле означает несколько строк: клиент с горсткой заказов, счёт с позициями, пост с тегами — и вам нужно, чтобы они легли отдельными строками в дочернюю таблицу, каждая со ссылкой (внешним ключом) на родителя, и всё это из одного конфига.
Атрибут each повторяет
<line> по одному разу на каждый элемент
списка. Одна карточка превращается в N строк вывода — нормализованную таблицу,
готовую для базы данных.
Примеры вывода ниже — иллюстративные. Конкретные значения, которые выдаёт генератор, могут меняться от версии ядра и от сида; а вот количество строк и структурные правила (какие строки появляются, какие остаются пустыми, что ключи уникальны) — именно это функция и гарантирует.
- Aстроки-родители, у каждой свой ключ
- Bстроки-потомки — каждая несёт ключ своего родителя, поэтому сирот не бывает
Коротко
| Где | Что |
|---|---|
| Применяется на | <line> |
| Значение | Имя последовательности, у генератора которой стоит repeat |
| Эффект | Строка выводится по одному разу на каждый элемент этого списка |
У целевой последовательности обязан стоять repeat — именно
он делает её списком, который можно обойти. Наведите each на что-то другое — и TDC
объяснит, в чём дело (см. последний раздел ниже).
Задача: список, втиснутый в одну карточку
Клиент сделал три заказа. В карточке они лежат списком:
1;VIP;8648,7170,7063
Для базы так не годится. Нужна таблица orders, где каждый заказ — своя строка
со ссылкой на клиента. Одна карточка должна дать три строки, а не одну строку со
списком внутри.
Инструмент
Дайте заказам собственный список через repeat,
а на строку с заказом поставьте each — и она сработает по разу на каждый заказ:
<env count="4" seed="each-demo" inject="${{%}}" local="ru">
<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,обычный" 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.обычный">
<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>
Что получилось
INSERT INTO customers VALUES (1, 'Дмитрий', 'обычный'); INSERT INTO orders VALUES (4, 1, 433); INSERT INTO orders VALUES (5, 1, 474); INSERT INTO customers VALUES (2, 'Сергей', 'обычный'); INSERT INTO customers VALUES (3, 'Николай', '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, 'Андрей', 'VIP'); INSERT INTO orders VALUES (16, 4, 5249); INSERT INTO orders VALUES (17, 4, 2324);
Строка с заказом написана в конфиге один раз, а печатается столько раз, сколько
заказов у клиента. У клиента №2 (Сергей) выпало ноль заказов — поэтому строк
с его заказами нет вовсе, и никакой пустой заглушки не осталось. Два VIP-клиента
берут заказы из VipOrders, обычные — из StdOrders, и на каждой строке обходится
именно нужный список, потому что parent уже отобрал,
какой список активен.
Зачем/когда: это способ одним конфигом вывести родительскую таблицу и её дочернюю таблицу вместе, правильно связанными — без постобработки, без второго прогона, без скриптов, разворачивающих список.
Что видно внутри строки с each
Внутри строки под each имя обходимой последовательности означает текущий элемент, плюс
есть два дополнительных встроенных значения:
| Запись | Что означает |
|---|---|
${{VipOrders}} | текущий элемент. Вне строки с each то же имя — это весь склеенный список |
${{_item}} | номер внутри карточки: 1, 2, 3 |
${{_item_id}} | сквозной уникальный номер по всему прогону — ваш первичный ключ |
| остальное | как обычно: ${{Id}}, ${{_count}}, любая другая последовательность |
Именно поэтому внешний ключ работает: ${{Id}} на каждой строке заказа по-прежнему
означает клиента, а не элемент. Если бы обход перепривязывал Id к элементу,
каждый заказ ссылался бы не туда. Колонки родителя остаются неизменными; двигаются
только имя обходимого списка и _item / _item_id.
Про номера заказов
Посмотрите на первичные ключи: 4, 5, потом 11, 12, 13, потом 16, 17. Они
растут, но с пропусками.
Так сделано осознанно. _item_id вычисляется из номера карточки, поэтому строку можно
получить, не зная соседних — а именно это и позволяет многопоточности
--jobs давать байт в байт тот же результат, что и однопоточный
прогон. Плата — пропуски там, где у карточки заказов меньше максимума. Для первичного
ключа это совершенно нормально: в настоящих базах номера тоже редко идут подряд.
Уникальность при этом железная, в том числе между несколькими списками: StdOrders
занял 4, 5, а VipOrders — 11, 12, 13, и эти два пространства ключей никогда не
пересекаются. На большей проверке 2000 карточек дали 3501 заказ с 3501 различным ключом
и ноль заказов, указывающих на несуществующего клиента.
Зачем/когда: полагайтесь на _item_id как на стабильный, уникальный, безопасный
для многопоточности суррогатный ключ. Берите _item, когда нужен порядковый номер
внутри карточки (1, 2, 3, заново стартующий на каждом родителе).
Не просто список, а жизненный цикл
Список, по которому идёт each, не обязан быть мешком несвязанных значений. Если он
записан по порядку, строки и выйдут по порядку — так делается история: заказ идёт
created → paid → shipped → delivered, по строке на шаг, и ни один шаг не встанет не на
своё место.
Работают две вещи. order="sequential" заставляет список
repeat идти по значениям в том порядке, в каком они написаны, а не разыгрывать их, а
parent выбирает, по какому пути пойдёт эта
запись:
<env count="5" 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>
<sequence name="Happy" parent="Outcome.delivered">
<gen type="text" value="created,paid,shipped,delivered" repeat="4" order="sequential" cycle="true"/>
</sequence>
<sequence name="Refund" parent="Outcome.refunded">
<gen type="text" value="created,paid,refunded" repeat="3" order="sequential" cycle="true"/>
</sequence>
<sequence name="Cancel" parent="Outcome.cancelled">
<gen type="text" value="created,cancelled" repeat="2" order="sequential" cycle="true"/>
</sequence>
</env>
<block>
<line each="Happy"><data>${{OrderId}},${{_item}},${{Happy}}</data></line>
<line each="Refund"><data>${{OrderId}},${{_item}},${{Refund}}</data></line>
<line each="Cancel"><data>${{OrderId}},${{_item}},${{Cancel}}</data></line>
</block>
1000,1,created 1000,2,paid 1000,3,shipped 1000,4,delivered 1001,1,created 1001,2,paid 1001,3,shipped 1001,4,delivered 1002,1,created 1002,2,paid 1002,3,shipped 1002,4,delivered 1003,1,created 1003,2,cancelled 1004,1,created 1004,2,paid 1004,3,refunded
Каждый заказ идёт легальным путём — отгруженный сначала оплачен, отменённый никогда
не отгружался, — а исходы ложатся ровно в те доли, которые объявил <mix>. Из трёх строк
на запись срабатывает только одна: parent оставляет две другие пустыми.
Почему сделано именно так. Столбец статуса, который менялся бы «глядя на предыдущую строку», заставил бы считать прогон по порядку, с первой строки. Выбор целого пути заранее и его разворачивание оставляют каждую запись независимой — поэтому это работает и на потоковых движках, и в параллель, без изменений.
Та же форма годится на всё, где шаги берутся из фиксированного словаря: тикет поддержки
(open → assigned → resolved), доставка, очередь модерации, онбординг.
Где не сработает
each строг к тому, что он может обойти, и падает громко, а не гадает:
| Что | Почему | Ошибка |
|---|---|---|
each на последовательности без repeat | обходить нечего | TDC207 |
each на несуществующем имени | последовательность не объявлена | TDC206 |
<data name="…"> внутри строки с each | именованный <data> — это колонка для Parquet, а Parquet собирает колонки по карточке, а не по обходимой строке | TDC209 |
error[TDC207]: sequence "Tier" has no repeat, so each has nothing to iterate note: add repeat="…" to its generator, or point each at a list sequence
Для вывода в Parquet each вообще не нужен: список из
repeat остаётся настоящим списком внутри колонки —
а это уже правильная форма для колоночного файла. each — инструмент для текстовых
форматов (SQL, CSV, JSON lines), где одна карточка должна стать несколькими физическими
строками.
Смотрите также
- Иерархические зависимости —
parent, который решает, какой список активен на каждой строке. - Согласованные и связанные данные — другой способ связать таблицы, через общий родительский справочник.
repeat/separator— как последовательность вообще становится списком.- Встроенные значения —
_item,_item_id,_countи их родня.