Одна з ключових можливостей dbt — керування потоком виконання за допомогою Jinja, а також написання макросів і хуків.
Список статей
- Спробуймо dbt. Частина 1 — пролог
- Спробуймо dbt. Частина 2 — перший запуск
- Спробуймо dbt. Частина 3 — seed і source
- Спробуймо dbt. Частина 4 — матеріалізація моделей
- Спробуймо dbt. Частина 5 — від Jinja до макросів і хуків
- Спробуймо dbt. Частина 6 — знімки та аналізи
- Спробуймо dbt. Частина 7 — тести
- Спробуймо dbt. Частина 8 — DAG і документація
- Спробуймо dbt. Частина 9 — змінні
- Спробуймо dbt. Частина 10 — пакети
- Спробуймо dbt. Частина 11 — SQLFluff і Pre-Commit
У dbt можна виконувати не лише SQL: однією з ключових можливостей є Jinja. У цьому дописі ми керуватимемо потоком виконання за допомогою макросів і налаштуємо хуки.
Jinja
Автор має окремий допис Let’s try: Jinja2 про Jinja та Python. Нагадаю основне ще раз тут.
Коментарі
Коментар потрібно писати у {# ... #}.
{# це коментар #}Інструкції
Інструкція (statement) керує потоком виконання програми й записується в {% ... %}.
Змінні
Щоб оголосити й ініціалізувати змінну, використовуємо set.
{% set var_a = 1 %}
{% set str = "abcd" %}
{% set is_empty = false %}
{% set arr = ['a', 'b', 'c'] %}
{% set tuple = ('a', 'b') %}
{% set dictionary = dict(a=1, b=2, c=3, d=[4, 5]) %}
{# або {'a': 1, 'b': 2, 'c': 3, 'd': [4, 5]} #}
{% set dict_arr = [dict(a=1), dict(b=2), dict(c=3), dict(d=[4, 5])] %}
{# або [{'a' : 1}, {'b': 2}, {'c': 3}, {'d': [4, 5]}] #}
{% set multiline_text %}
Lorem ipsum dolor sit amet consectetur adipiscing elit.
Quisque faucibus ex sapien vitae pellentesque sem placerat.
In id cursus mi pretium tellus duis convallis.
Tempus leo eu aenean sed diam urna tempor.
{% endset %}Конструкція if-else
Пишемо if–elif–else і завершуємо endif.
{% if <statement> %}
...
{% elif <statement> %}
...
{% else %}
...
{% endif %}Цикл for
Так само просто: for–endfor.
{% for element in <iterator> %}
...
{% endfor %}Під час ітерації по словнику можна використати .items(), щоб отримати пари «ключ-значення».
{% for dict in <dict_iterator> %}
{% for key, value in dict.items() %}
...
{% endfor %}
{% endfor %}Вирази
Вираз (expression) — це вивід. Ми звертаємося до нього через {{ ... }}.
{{ var_a }}Макроси
Макроси працюють як функції: ми викликаємо їх, щоб виконати певні завдання, наприклад отримати з довідкових таблиць дату останнього запуску або вставити кілька записів у таблицю логів.
Один макрос, generate_schema_name(), ми вже писали раніше в Let’s try: dbt Частина 3 — seed і source, щоб схема створювалася зі звичайною вхідною назвою, а не через конкатенацію, як показано нижче.
{% macro generate_schema_name(custom_schema_name, node) -%}
{%- set default_schema = target.schema -%}
{%- if custom_schema_name is none -%}
{{ default_schema }}
{%- else -%}
{{ custom_schema_name | trim }}
{%- endif -%}
{%- endmacro %}Наведений макрос — це перевизначення: ми просто створюємо макрос із тією самою назвою й додаємо потрібну нам поведінку. Вбудовані макроси dbt можна знайти тут ↗, на що я натрапив на форумі Stackoverflow ↗.
Розробка макросу
Щоб розробити макрос, починаємо з такої структури Jinja.
{% macro <macro_name>(<argument_1>, <argument_2>, ... ) -%}
{# заготовка коду #}
...
{{ return(<variable>) }} {# `return`, якщо потрібно #}
{%- endmacro %}Також можна описати файл властивостей[1], наприклад так.
macros:
- name: <macro_name>
description: <macro_description>
arguments:
- name: <argument_name>
type: <argument_type>
description: <argument_description>Використання макросу
Макрос можна просто викликати у виразі ({{ ... }}) або записати значення, яке він повертає, у нову змінну через інструкцію ({% ... %}), як у прикладах нижче.
{# виклик макросу у виразі #}
{{ my_macro(param01, param02) }}
{# виклик макросу в інструкції з отриманням значення, що повертається #}
{% set some_var = my_compute_macro(param01, param02, param03) %}Приклад макросу
Припустімо, у нас є така таблиця логів.

І нам потрібен макрос, який знаходить дані в довідковій таблиці за останньою датою з таблиці логів.
Спершу пишемо файл властивостей макросу.
macros:
- name: get_last_date_from_log
description: get last date from log table
arguments:
- name: ref_table
type: string
description: reference table to be searched in this log tableА тепер сам скрипт макросу.
{% macro get_last_date_from_log(ref_table) %}
{% set log_table = "raw.logs" %}
{% set query %}
SELECT MAX(process_date) as last_process_date
FROM `{{ log_table }}`
WHERE true
AND process_name = "{{ ref_table }}"
AND is_successful IS TRUE
{% endset %}
{% if execute %}
{{ print("query = " + query) }}
{% set query_result = run_query(query) %}
{{ log(query_result.columns["last_process_date"].values()) }}
{{ return(query_result.columns[0].values()[0]) }}
{% endif %}
{% endmacro %}- Рядок 1: починаємо з
{% macro <macro_name>(<params>) %}, де params — лишеref_table. - Рядок 2: потрібна змінна
log_tableз назвою таблиці логів. - Рядок 3: задаємо
queryяк інструкцію для запиту до таблиці логів через{% set query %}. - Рядки 5 і 7: підставляємо в запит
{{ log_table }}і{{ ref_table }}. - Рядок 9: завершуємо визначення
queryчерез{% endset %}. - Рядок 11: використовуємо
{% if execute %}, щоб код виконувався лише в режимі виконання (execute)[2], тобто коли ми будуємо, компілюємо чи запускаємо модель, а не на етапі побудови DAG. - Рядок 12:
{{ print() }}виводить текст у консоль[3]. - Рядок 13: виконуємо запит через
run_query[4] і зберігаємо результат уquery_result. - Рядок 14:
query_resultмає тип Table[5], тому ми можемо записати значення стовпцяlast_process_dateу лог-файл черезlog()[6], щоб побачити його. - Рядок 15: нарешті повертаємо дату через
{{ return() }}. - Рядок 17: завершуємо макрос через
{% endmacro %}.
Тепер можна використати цей макрос так:
{%- set last_date = get_last_date_from_log(this.table) -%}
select *
from {{ source("delhi_climate", "daily_delhi_climate") }}
where date = "{{ last_date }}"Ми просто викликаємо макрос і отримуємо значення, яке він повертає, через інструкцію {% set var = macro(params) %}. А this.table — це назва таблиці поточної моделі, у нашому випадку ім’я файлу climate-stat-from-log.
this[7] — також одна з функцій dbt Jinja.
Спробуймо скомпілювати й подивитися на результат.
Результат компіляції
Коли ми виконуємо dbt compile, ми бачимо запит, який вивели через print("query = " ...), і скомпільовану модель, як показано нижче.
22:42:51 Running with dbt=1.10.13
22:42:53 Registered adapter: bigquery=1.10.2
22:42:53 Found 6 models, 7 seeds, 4 data tests, 2 sources, 510 macros
22:42:53
22:42:53 Concurrency: 1 threads (target='dev')
22:42:53
query =
SELECT MAX(process_date) as last_process_date
FROM `raw.logs`
WHERE true
AND process_name = "climate-stat-from-log"
AND is_successful IS TRUE
Compiled node 'climate-stat-from-log' is:
select *
from `bluebirz-playground`.`raw`.`daily_delhi_climate`
where date = "2016-11-10"Лог dbt
Якщо відкрити лог logs/dbt.log, там також видно те, що ми виводили через print(), і дату, яку записали через log() (передостанній рядок).
23:42:55.769840 [debug] [Thread-1 (]: Began compiling node model.dbt_proj01.climate-stat-from-log
23:42:55.780263 [info ] [Thread-1 (]: query =
SELECT MAX(process_date) as last_process_date
FROM `raw.logs`
WHERE true
AND process_name = "climate-stat-from-log"
AND is_successful IS TRUE
23:42:55.789551 [debug] [Thread-1 (]: On model.dbt_proj01.climate-stat-from-log: /* {"app": "dbt", "dbt_version": "1.10.13", "profile_name": "dbt_proj01", "target_name": "dev", "node_id": "model.dbt_proj01.climate-stat-from-log"} */
SELECT MAX(process_date) as last_process_date
FROM `raw.logs`
WHERE true
AND process_name = "climate-stat-from-log"
AND is_successful IS TRUE
23:42:55.790241 [debug] [Thread-1 (]: Opening a new connection, currently in state closed
23:42:56.422744 [debug] [Thread-1 (]: BigQuery adapter: https://console.cloud.google.com/bigquery?project=<project>&j=<job_id>&page=queryresults
23:42:57.036284 [debug] [Thread-1 (]: (datetime.date(2016, 11, 10),)
23:42:57.038868 [debug] [Thread-1 (]: Writing injected SQL for node "model.dbt_proj01.climate-stat-from-log"Усі ці функції входять до функцій dbt Jinja[8].
Хуки
Хуки[9] — це команди, які автоматично виконуються до або після запуску моделей. Їх часто використовують для ланцюгового виконання, наприклад щоб зафіксувати запис і час того, яка модель ось-ось запуститься або успішно завершилась.
pre-hook: виконується перед побудовою моделей, seed або snapshot.post-hook: виконується після побудови моделей, seed або snapshot.on-run-start: виконується на початку виконання команд dbt.on-run-end: виконується наприкінці виконання команд dbt.
Визначення хуків
pre-hook і post-hook можна визначати в dbt_project.yml, у файлах моделей або в properties.yml. А on-run-start і on-run-end потрібно налаштовувати лише в dbt_project.yml.
Структура файлів
dbt_directory
├── dbt_project.yml
└── models
└─── <model_name>
├── properties.yml
└── model_file.sqldbt_project.yml
models:
<project_name>:
+pre-hook:
- "<pre-hook statement for models>"
+post-hook:
- "<post-hook statement for models>"
seeds:
<project_name>:
+pre-hook:
- "<pre-hook statement for seeds>"
+post-hook:
- "<post-hook statement for seeds>"
snapshots:
<project_name>:
+pre-hook:
- "<pre-hook statement for snapshots>"
+post-hook:
- "<post-hook statement for snapshots>"
on-run-start:
- "<on-run-start statement>"
on-run-end:
- "<on-run-end statement>"SQL-файли моделей
{{-
config(
pre_hook=["<statement>"],
post_hook=["<statement>"],
)
-}}
SELECT ...property.yml
models:
- name: [<model_name>]
config:
pre_hook:
- "<pre-hook statement for models"
post_hook:
- "<post-hook statement for models"
seeds:
- name: [<seed_name>]
config:
pre_hook:
- "<pre-hook statement for seeds"
post_hook:
- "<post-hook statement for seeds"
snapshots:
- name: [<snapshot_name>]
config:
pre_hook:
- "<pre-hook statement for snapshots"
post_hook:
- "<post-hook statement for snapshots"Приклад хуків
Ми просто виведемо повідомлення про виконання кожного хука й подивимося на результат.
dbt_project.yml
Просто виводимо, який хук виконується.
on-run-start:
- "{{ print('on-run-start executed') }}"
on-run-end:
- "{{ print('on-run-end executed') }}"SQL-файли моделей
Я використав модель із розділу про макроси й додав хуки, які виводять, котрий саме хук виконується.
{{-
config(
pre_hook=["{{ print('pre-hook executed') }}"],
post_hook=["{{ print('post-hook executed') }}"],
)
-}}
{%- set last_date = get_last_date_from_log(this.table) -%}
select *
from {{ source("delhi_climate", "daily_delhi_climate") }}
where date = "{{ last_date }}"Результат запуску
Під час dbt run ми побачимо ці тексти в такому порядку:
- on-run-start executed
- pre-hook executed
- post-hook executed
- on-run-end executed
22:19:40 Running with dbt=1.10.13
22:19:42 Registered adapter: bigquery=1.10.2
22:19:42 Found 6 models, 7 seeds, 2 operations, 4 data tests, 2 sources, 510 macros
22:19:42
22:19:42 Concurrency: 1 threads (target='dev')
22:19:42
on-run-start executed
22:19:45 1 of 1 START hook: dbt_proj01.on-run-start.0 ................................... [RUN]
22:19:45 1 of 1 OK hook: dbt_proj01.on-run-start.0 ...................................... [OK in 0.02s]
22:19:45
22:19:45 1 of 1 START sql table model dbt_test_dataset.climate-stat-from-log ............ [RUN]
query =
SELECT MAX(process_date) as last_process_date
FROM `raw.logs`
WHERE true
AND process_name = "climate-stat-from-log"
AND is_successful IS TRUE
pre-hook executed
post-hook executed
22:19:49 1 of 1 OK created sql table model dbt_test_dataset.climate-stat-from-log ....... [CREATE TABLE (1.0 rows, 2.4 KiB processed) in 4.87s]
22:19:49
on-run-end executed
22:19:49 1 of 1 START hook: dbt_proj01.on-run-end.0 ..................................... [RUN]
22:19:49 1 of 1 OK hook: dbt_proj01.on-run-end.0 ........................................ [OK in 0.00s]
22:19:49
22:19:49 Finished running 2 project hooks, 1 table model in 0 hours 0 minutes and 7.31 seconds (7.31s).
22:19:50
22:19:50 Completed successfully
22:19:50
22:19:50 Done. PASS=3 WARN=0 ERROR=0 SKIP=0 NO-OP=0 TOTAL=3Підсумок
- Jinja
- коментар:
{# ... #} - змінна:
{% set var = ... %}або{% set var %} ... {% endset %} - if-else:
{% if bool %} ... {% elif bool %} ... {% else %} ... {% endif %} - цикл for:
{% for element in iterator %} ... {% endfor %} - цикл for по словнику:
{% for key, value in dict.items() %} ... {% endfor %} - вираз:
{{ ... }}
- коментар:
- Макроси
- визначення макросу:
{% macro <name>(params) %} ... {% endmacro %} - виклик макросу:
{{ macro(params) }}або{% set var = macro(params) %} executeмає значення true, коли dbt працює в режимі виконання, тобто будує, компілює чи запускає моделі.{{ print() }}виводить текст у консоль.{{ run_query() }}виконує запит через адаптер проєкту.- Після
run_query()повертається об’єкт Table, значення з якого можна дістати. {{ log() }}записує дані в лог-файлlogs/dbt.log.{{ return() }}повертає значення.thisпосилається на поточну модель, аthis.table— це назва таблиці поточної моделі.
- визначення макросу:
- Хуки
on-run-startабоon-run-endуdbt_project.ymlспрацьовують на початку/в кінці виконання команд dbt.pre_hookабоpost_hookспрацьовують перед/після побудови конкретної моделі, seed чи snapshot.
Джерела
- Властивості макросів | dbt Developer Hub
- Про змінну execute | dbt Developer Hub
- Про функцію print | dbt Developer Hub
- Про макрос run_query | dbt Developer Hub
- Table — документація agate
- log | dbt Developer Hub
- Про this | dbt Developer Hub
- Функції dbt Jinja | dbt Developer Hub
- Хуки та операції | dbt Developer Hub
ОРИГІНАЛ СТАТТІ: Let’s try: dbt part 5 – from Jinja to macros and hooks
АВТОР СТАТТІ: bluebirz
Оригінал опубліковано під ліцензією CC BY 4.0. Цей матеріал є перекладом українською мовою.
Доєднуйтесь до наших спільнот 👇
