Перейти к основному содержимому

Связанные таблицы через 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>

Что получилось

./run each-demo.tdc
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, а VipOrders11, 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>
./run lifecycle.tdc
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
./run broken.tdc (each на не-списке)
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), где одна карточка должна стать несколькими физическими строками.

Смотрите также