Что такое Jinja при использовании в Apache Superset?
Jinja2 — это шаблонизатор для Python, используемый для генерации динамического SQL за счет встраивания в код запроса переменных, условий и выражений.
Суть его работы заключается в том, что он заменяет часть SQL кода значениями переменных или функций с помощью определенного синтаксиса, который будет описан дальше в статье. После того, как код динамически сгенерировался — он отправляется на сервер базы данных.
Тем самым с помощью использования Jinja мы можем повысить интерактивность своих Dashboards, за счет кастомизации sql в метриках, динамических измерениях и виртуальных датасетах.
Если кратко, то выглядит это примерно так (пока без погружения в детали, картинка с хабра):
Еще один пример с хабра:
На высоком уровне шаблонизация — это возможность добавлять программные возможности к SQL, при этом Jinja действует как мост, позволяющий нам вводить контекст в запросы.
Для чего можно использовать Jinja
Включение Jinja повышает гибкость функциональности фильтра Superset. Вот несколько более конкретных примеров того, для чего можно использовать Jinja:
- Отображение только данных вошедшего в систему пользователя.
- Использование компонента фильтра для фильтрации запроса, когда имя столбца фильтра не соответствует имени в текущем запросе.
- Применение ограничений фильтра через URL-адрес дашборда Superset.
- Для таблиц, содержащих массивы, возможность поиска записей с определенным значением в массиве.
- Возможность применять операторы OR в фильтре для двух разных столбцов (например
‘name’ = “Matthew” OR ‘age’ = 12
) - Персонализированные дашборды.
- Управление доступом к данным с использованием доступа на уровне строк на основе User_ID или Email (использование информации о пользователе в Row Level Security RLS — построчное ограничение доступа на уровне датасета).
- Исследуйте доступ к метаданным Superset по User_ID.
Как работает контекст фильтров и Jinja в Superset
В контексте Jinja состояние фильтра можно менять за счет использования информации:
- Состояние пользователя
- user_id человека, вошедшего в систему
- имя пользователя
- email пользователя
- Состояние URL
- вы можете добавить параметры в URL-адрес Superset, который можно вставить в ваш запрос (то есть внедрить внутрь url параметры и их значения использовать через Jinja).
- Например, если у вас есть Dashboard по продажам, а конечный пользователь хочет видеть продажи только в Барселоне:
- Можно отправить панель мониторинга, где они смогут перейти к фильтру и нажать «Барселона».
- Отправьте информационную панель с предварительно отфильтрованным URL-адресом с городом в самом параметре URL-адреса, чтобы при загрузке дашборда они могли сразу увидеть то, что хотят.
Немного про синтаксис Jinja
- Выражение
{{ ... }}
: выражения используются, когда вы хотите вывести строку. Вы можете использовать выражения для ссылки на переменные и вызова макросов. - Оператор
{% ... %}
: этот оператор не выводит строку. Он используется для потока управления, например, для настройки for циклов и if (условных) операторов, для установки или изменения переменных или для определения макросов. - Комментарий
{# ... #}
: комментарии Jinja используются для предотвращения выполнения текста внутри комментария или вывода строки.
Как активировать работу Jinja в Apache Superset?
Для того, чтобы начать работу с Jinja в Apache Superset, необходимо в файле
superset/docker/pythonpath_dev/superset_config.py
активировать настройку (флаг)
"ENABLE_TEMPLATE_PROCESSING": True
Полный код в конфиг файле выглядит как:
FEATURE_FLAGS = { "ALERT_REPORTS": True, "DATAPANEL_CLOSED_BY_DEFAULT": True, "DASHBOARD_VIRTUALIZATION": True, "DASHBOARD_RBAC": True, "ENABLE_TEMPLATE_PROCESSING": True, "ESCAPE_MARKDOWN_HTML": True, "LISTVIEWS_DEFAULT_CARD_VIEW": True, "THUMBNAILS": True, "DRILL_BY": True, "DRILL_TO_DETAIL": True, "HORIZONTAL_FILTER_BAR": True, "ESTIMATE_QUERY_COST": True, "TAGGING_SYSTEM": True, "HTML_SANITIZATION": False, }
Jinja Templates (Шаблоны Джинджа) в Superset
Функциональность Superset «SQL Lab» и «Explore» поддерживает шаблоны Jinja в SQL запросах. Если включено использование шаблонов, код Python можно внедрить в виртуальные наборы данных и в пользовательский SQL в фильтрах и элементах управления метриками в Explore. По умолчанию в контексте Jinja доступны следующие переменные:
columns
: столбцы, по которым группируется запрос.filter
: фильтры, примененные в запросеfrom_dttm
: начальноеdatetime
значение из выбранного временного диапазона (None
если не определено)to_dttm
: конечноеdatetime
значение из выбранного временного диапазона (None
если не определено)groupby
: столбцы, по которым группируется запрос (устарело)metrics
: агрегированные выражения в запросеrow_limit
: ограничение строки запросаrow_offset
: смещение строки запросаtable_columns
: столбцы, доступные в наборе данныхtime_column
: временной столбец запроса (None
если не определен)time_grain
: выбранный временной интервал (None
если не определен)
Параметры Jinja вызываются в двойных скобках в запросе и без них в логических блоках.
SELECT * FROM tbl WHERE dttm_col > '{{ from_dttm }}' and dttm_col < '{{ to_dttm }}'
Вы также можете использовать логику Jinja, чтобы сделать ваш запрос устойчивым к очистке фильтра временного диапазона:
SELECT * FROM tbl WHERE ( {% if from_dttm is not none %} dttm_col > '{{ from_dttm }}' AND {% endif %} {% if to_dttm is not none %} dttm_col < '{{ to_dttm }}' AND {% endif %} true )
Добавление пользовательских функций/макросов в контекст Jinja
Чтобы добавить пользовательские макросы в контекст Jinja, вам необходимо перегрузить контекст Jinja по умолчанию в вашей среде, указав в JINJA_CONTEXT_ADDONS
конфигурации Apache Superset (файл superset_config.py
). Объекты, указанные в этом словаре, доступны для использования пользователями там, где доступен контекст Jinja.
Синтаксис:
JINJA_CONTEXT_ADDONS = { '<Template_name>': <function body>, }
Пример:
JINJA_CONTEXT_ADDONS = { 'my_new_custom_macro': lambda x: x*2, }
Как использовать JINJA_CONTEXT_ADDONS в SQL в Superset
Рассмотрим на примере простой функции:
JINJA_CONTEXT_ADDONS = { 'tst': lambda x=0, y=0: x * y, }
Добавляем в конфиг:
Добавляем в SQL запрос в SQL Lab:
Default Jinja value в SQL Lab
В SQL Lab справа рядом с кнопкой сохранить, нажмите на три точки и выберите Parameters:
Откроется меню, в котором можно внести значения по умолчанию:
Параметры становятся доступными в вашем SQL (пример: SELECT * FROM {{ my_table }}
) с помощью синтаксиса шаблонов Jinja.
Параметры шаблона SQL Lab хранятся вместе с набором данных в формате TEMPLATE PARAMETERS
.
Существует специальный _filters
параметр, который можно использовать для проверки фильтров, используемых в шаблоне jinja.
{ "_filters": [ { "col": "action_type", "op": "IN", "val": ["sell", "buy"] } ] }
SELECT action, count(*) as times FROM logs WHERE action in {{ filter_values('action_type'))|where_in }} GROUP BY action
Примечание _filters
не сохраняется вместе с набором данных. Он используется только в пользовательском интерфейсе SQL Lab.
CUSTOM_TEMPLATE_PROCESSORS
шаблон SQL
Помимо шаблонов Jinja по умолчанию, лаборатория SQL также поддерживает самоопределяемый процессор шаблонов, устанавливая его CUSTOM_TEMPLATE_PROCESSORS
в конфигурации расширенного набора.
Значения в этом словаре перезаписывают процессоры шаблонов Jinja по умолчанию для указанного ядра базы данных. В приведенном ниже примере настраивается пользовательский процессор шаблонов presto, который реализует собственную логику обработки шаблона макроса с анализом регулярных выражений. Он использует макрос $
стиля вместо {{ }}
стиля в шаблонах Jinja.
При настройке с помощью CUSTOM_TEMPLATE_PROCESSORS
шаблон SQL в базе данных presto обрабатывается пользовательским шаблоном, а не шаблоном по умолчанию.
def DATE( ts: datetime, day_offset: SupportsInt = 0, hour_offset: SupportsInt = 0 ) -> str: """Current day as a string.""" day_offset, hour_offset = int(day_offset), int(hour_offset) offset_day = (ts + timedelta(days=day_offset, hours=hour_offset)).date() return str(offset_day) class CustomPrestoTemplateProcessor(PrestoTemplateProcessor): """A custom presto template processor.""" engine = "presto" def process_template(self, sql: str, **kwargs) -> str: """Processes a sql template with $ style macro using regex.""" # Add custom macros functions. macros = { "DATE": partial(DATE, datetime.utcnow()) } # type: Dict[str, Any] # Update with macros defined in context and kwargs. macros.update(self.context) macros.update(kwargs) def replacer(match): """Expand $ style macros with corresponding function calls.""" macro_name, args_str = match.groups() args = [a.strip() for a in args_str.split(",")] if args == [""]: args = [] f = macros[macro_name[1:]] return f(*args) macro_names = ["$" + name for name in macros.keys()] pattern = r"(%s)\s*\(([^()]*)\)" % "|".join(map(re.escape, macro_names)) return re.sub(pattern, replacer, sql) CUSTOM_TEMPLATE_PROCESSORS = { CustomPrestoTemplateProcessor.engine: CustomPrestoTemplateProcessor }
SQL Lab также включает функцию проверки запросов в реальном времени с подключаемыми серверными модулями. Вы можете настроить, какая реализация проверки используется с каким ядром базы данных, добавив в файл конфигурации блок, подобный следующему:
FEATURE_FLAGS = { 'SQL_VALIDATORS_BY_ENGINE': { 'presto': 'PrestoDBSQLValidator', } }
Доступные валидаторы и имена можно найти в sql_validators.
Где находится исходный код Jinja Python в репозитории Superset в GitHub
Исходный код шаблонизации Jinja лежит в файле:
superset/superset/jinja_context.py
Я не пробовал, но теоретически сюда можно добавить нестандартный функционал для вызова через функционал Jinja. Но это уже будет нестандартной доработкой Superset, что усложнит обновление на новые версии Superset.
Доступные макросы Jinja для использования в Superset
В этом разделе будут описаны предопределенные макросы Jinja в Superset, которые доступны для использования.
Current Username: current_username()
Макрос {{ current_username() }}
возвращает имя username текущего пользователя, вошедшего в систему.
Если в конфигурации Superset включено кэширование, то по умолчанию значение username
будет использоваться Superset при вычислении ключа кэша.
Ключ кэша — это уникальный идентификатор, который определяет, произойдет ли попадание в кэш в будущем, и Superset сможет получить кэшированные данные.
Вы можете отключить включение значения username
в вычисление ключа кэша, добавив в свой код Jinja следующий параметр:
{{ current_username(add_to_cache_keys=False) }}
Current User ID: current_user_id()
Макрос {{ current_user_id() }}
возвращает идентификатор учетной записи вошедшего в систему пользователя.
Вы можете отключить включение значения учетной записи id в расчет ключа кэша, добавив в свой код Jinja следующий параметр:
{{ current_user_id(add_to_cache_keys=False) }}
Current User Email: current_user_email()
Макрос {{ current_user_email() }}
возвращает адрес электронной почты текущего пользователя, вошедшего в систему.
Вы можете отключить включение значения электронной почты в расчет ключа кэша, добавив в свой код Jinja следующий параметр:
{{ current_user_email(add_to_cache_keys=False) }}
Custom URL Parameters: url_param('custom_variable')
Макрос {{ url_param('custom_variable') }}
позволяет вам определять произвольные параметры URL-адреса и ссылаться на них в вашем коде SQL.
Примеры custom параметров URL-адреса
Предположим, Вы написали SQL запрос в SQLLab интерфейсе:
SELECT count(*) FROM ORDERS WHERE country_code = '{{ url_param('countrycode') }}'
Предположим ваш суперсет находится на домене your-superset.ru.
Вы решили отправить одному коллеге URL-адрес SQL LAB:
https://your-superset.ru/superset/sqllab?countrycode=ES
А другому коллеге URL-адрес SQL LAB:
https://your-superset.ru/superset/sqllab?countrycode=US
У первого коллеги SQL будет отображаться как:
SELECT count(*) FROM ORDERS WHERE country_code = 'ES'
А у второго коллеги, как:
SELECT count(*) FROM ORDERS WHERE country_code = 'US'
Explicitly Including Values in Cache Key: cache_key_wrapper()
Функция {{ cache_key_wrapper() }}
явно указывает Superset добавить значение в накопленный список значений, используемых при вычислении ключа кэша.
Эта функция необходима только в том случае, если вы хотите поместить возвращаемые значения собственной пользовательской функции в ключ кэша.
Обратите внимание, что эта функция обеспечивает кэширование значений user_id и username в вызовах функций current_user_id()
и current_username()
(если у вас включено кеширование).
Filter Values: filter_values()
Вы можете получить значение для определенного фильтра в виде списка, используя
{{ filter_values() }}
Это полезно, если:
- Вы хотите использовать компонент фильтра для фильтрации запроса, в котором имя столбца компонента фильтра не соответствует имени в операторе выбора.
- Вы хотите иметь возможность фильтровать внутри основного запроса в целях повышения производительности.
Пример с описанным кейсом:
SELECT action, count(*) as times FROM logs WHERE action in {{ filter_values('action_type')|where_in }} GROUP BY action
Здесь where_in
фильтр преобразует список значений filter_values('action_type')
в строку, подходящую для IN
выражения.
Filters for a Specific Column: get_filters()
Макрос {{ get_filters() }}
возвращает фильтры, примененные к данному столбцу.
Помимо возврата значений (аналогично тому, как это filter_values()
происходит), макрос get_filters()
возвращает оператор, указанный в пользовательском интерфейсе Explore.
Это полезно, если:
- Вы хотите обрабатывать больше, чем оператор IN в SQL clause.
- Вы хотите обрабатывать создание custom SQL conditions для фильтра.
- Вы хотите иметь возможность фильтровать внутри основного запроса для повышения скорости.
Пример кейса:
WITH RECURSIVE superiors(employee_id, manager_id, full_name, level, lineage) AS ( SELECT employee_id, manager_id, full_name, 1 as level, employee_id as lineage FROM employees WHERE 1=1 {# Render a blank line #} {%- for filter in get_filters('full_name', remove_filter=True) -%} {%- if filter.get('op') == 'IN' -%} AND full_name IN {{ filter.get('val')|where_in }} {%- endif -%} {%- if filter.get('op') == 'LIKE' -%} AND full_name LIKE {{ "'" + filter.get('val') + "'" }} {%- endif -%} {%- endfor -%} UNION ALL SELECT e.employee_id, e.manager_id, e.full_name, s.level + 1 as level, s.lineage FROM employees e, superiors s WHERE s.manager_id = e.employee_id ) SELECT employee_id, manager_id, full_name, level, lineage FROM superiors order by lineage, level
Специальные функции базы данных
Другой случай, когда шаблоны Jinja чрезвычайно полезны в контексте Superset, — это использование экзотических функций, специфичных для базы данных, которые не были реализованы в базовых спецификациях ядра базы данных.
- Например, TimescaleDB расширяет PostgreSQL, добавляя новые функциональные возможности, а также упаковывает и оптимизирует PostgreSQL для анализа данных временных рядов. TimescaleDB имеет собственную уникальную функцию time_bucket() , которая оптимизирует функцию PostgreSQL date_trunc() по умолчанию . Эта
time_bucket()
функция работает намного быстрее и производительнее, чемdate_trunc()
при попытке разрезать миллионы и миллиарды строк значений дат. Здесь шаблоны Jinja становятся предохранительным клапаном, который позволяет вам использовать особенности БДtime_bucket()
, избегая при этом использования гораздо более медленныхdate_trunc()
Использование Jinja в Datasets в Apache Superset
todo
Описание практических кейсов по использованию Jinja в Superset
Подставление той или иной таблички в SQL FROM Statement
select * from {% if my_table %} {{my_table}} {% else %} foo {% endif %}
6 Comments