- Создатель Васильев Антон, отредактировано 04.07.2024
Любой sql-запрос системы, который идет через платформенный API проходит через несколько этапов модификации. Цель этих этапов:
- Обогащение запроса проверкой прав доступа;
- Вставка фильтров (слой sochi);
- Добавление в запрос системных колонок для id и даты;
- Определение типов возвращаемых значений для корректного преобразования табличных данных в ДО платформы.
Таким образом, запрос, который пишет пользователь будет отличаться от запроса, выполненный в БД. Для уменьшения количества ошибок, необходимо учитывать, каким образом платформа производит модификации и понимать ее ограничения.
Статус | ГОТОВО |
---|---|
Раздел |
Обогащение запроса проверками прав доступа
В данном случае, платформа обходит запрос и выявляет таблицы, которые описаны в доменной модели. При работе, платформа разбивает запрос на группы, на которые будут наложены права. На каждую группу права накладываются только один раз. Необходимо учитывать, что права будут внедряться только к первой таблице из группы в порядке добавления.
Для успешного наложения прав, платформе необходимо знать конечный объект наложения прав. Таким образом, в запросе обязательно должен присутствовать конечный объект. Если его не будет, то платформа может наложить права на любой из подходящих объектов.
Например, если в запросе фигурирует только верхнеуровневая таблица, а дочерние наследуют права от разных объектов (а не наследуют от общей), то у платформы не будет данных о том, какую таблицу использовать для наложения прав. Стоит отметить, что в настоящий момент, такая иерархия в платформе запрещена.
Добавление системных колонок
Для добавления системных колонок платформенный механизм делает обход запроса, для определения типов данных. Для этого он использует модель данных системы. После определения данных подзапроса производится вставка колонок _type и _dt.
Важно, что до версии 1.2 при сборе информации могла происходить перезапись и, как следствие, типы данных могли определяться неверно. В версии 1.2 механизм был доработан, перезапись типов не происходит.
Замечание. Есть баг, в котором обнаружено место, где перезапись происходит и в версии 1.2, но основные места поправлены.
Основной задачей добавления системных колонок в запрос является подготовка результатов запроса к преобразованию в доменную модель, а также добавление параметров в запрос. Таким образом, при добавлении системных колонок платформа модифицирует лишь возвращаемые колонки, GroupBy и места вставки параметров. Отсюда можно сделать вывод, что платформа не будет добавлять колонки в:
- WHERE подзапросы или условия фильтрации (если в них не применяются параметры), т.к. это не влияет на возвращаемые значения, а также может привести к ошибкам в логике
- в ON часть JOIN, т.к. это не влияет на результат и может привести к ошибкам в логике
- в подзапросы, используемые в качестве колонок для возврата значений:
Т.е. в SELECT (SELECT ....) as column FROM ...
Порядок обхода CTE
Важно понимать, что платформа не строит дерева зависимостей между подзапросами и обрабатывает основной запрос в определенном порядке. Таким образом, при работе с CTE необходимо учитывать порядок обхода. Обход идет сверху вниз по порядку. Как следствие, если 1 CTE зависит от другого, то она должна располагаться ниже.
Ручное добавление колонок
Добавление колонок Reference и DateTimeWithTimeZone
У разработчика всегда есть возможность ручного добавления колонок с типом идентификатора и тайм зоной. Но при добавлении необходимо учитывать следующее:
- В версии af5-1.1 и ранее, при добавлении колонки нет проверки, добавлена ли уже такая колонка, возможно дублирование
- В версии af5-1.2 появилась такая проверка, но в ограниченном виде. На настоящий момент определение идет по имени колонки (не по алиасу), что приводит к тому, что платформа не добавит колонку самостоятельно в случае, если для 1 таблицы разработчик руками прописал системную колонку, а для другой таблицы не прописал. Пока механизм не доработан, необходимо соблюдать правило - добавлять колонку нужно либо для всех таблиц, либо не добавлять вообще
Замечание. На самом деле, механизм работы добавления колонок выглядит так:
- Платформа обходит колонки SELECT и собирает имена колонок (не alias, а именно имя, т.е. rkk.id as rkkid будет взята id )
- Когда платформа принимает решение на добавлении колонки, то она сверяет текущее имя колонки с полученным списком и если находит там такую колонку, то новую колонку с типом не добавляет.
Отсюда мы получаем следующий эффект:
В случае
... rkk.id as id, rkk.id_type as id_type, rkk.id as rkkid ...
Платформе НЕ добавит колонку rkk.id_type as rkkid_type и ее надо будет добавить руками или удалить колонку rkk.id_type as id_type
Как побочный эффект (и это можно будет поправить в будущем) колонка не будет добавлена и в случае:
... rkk.id as id, rkk.id_type as id_type, ord.id as ordid ...
Вероятно, сразу появится вопрос, по какой причине определение колонки идет по имени, а не по alias, ответ прост. Часто разработчики не соблюдают по той или иной причине нейминг платформы и добавляют код вида:
... rkk.id as id, rkk.id_type as typeId, ...
Если бы платформа определяла по alias, то она добавила бы новую колонку rkk.id_type as id_type, а этого делать нельзя.
При этом, в случае, если колонка отличается, т.е:
... rkk.id as id, rkk.id_type as typeId, t.module, ...
Платформа успешно добавит колонку t.module_type.
Данный механизм в платформе был добавлен для обратной совместимости, т.к. в версии 1.2 платформа научилась определять типы более точно и часто вставляла лишнюю колонку
Добавление констант и NULL
Стандартное наименование колонки с идентификатором в платформе является <произвольное имя>_id, для колонки с типом <произвольное имя>_id_type. В случае, если в запросе необходимо прописать константу или NULL в качестве идентификатора, то можно это сделать следующим образом:
null as my_id, 'TTTT00000000000I' as string_id
где TTTT это тип, а I идентификатор (всего 16 символов). В этом случае, платформа самостоятельно добавит колонки, в результате получится
null as my_id, null as my_id_type, I as string_id, TTTT as string_id_type
Добавление колонок в WHERE и работа с фильтрами
При добавлении фильтров с Reference типами, платформа умеет добавлять колонки _type в WHERE. В случае работы с точными сравнениями, добавление тривиальное. Интереснее рассмотреть случай работы с IN. В этом случае, платформа определяет типы параметров-идентификаторов и добавляет через конъюнкцию или дизъюнкцию необходимые колонки, например:
WHERE id in (param)
Будет преобразован в
WHERE id in (param_id) AND id_type = param_id_type
в случае, если в коллекции параметров все идентификаторы будут одного типа. Если же типы идентификаторов будут разные, то запросе преобразуется следующим образом (на примере 2 разных типов):
WHERE ((id in (param_id_0) AND id_type = param_id_type_0) OR (id in (param_id_1) AND id_type = param_id_type_1)
Т.е. сами параметры физически будут разнесены по типам данных и проверены отдельно.
Учитывая вышесказанное, стоит обратить внимание на потенциальное кол-во разных типов с целью именьшения разрастания SQL запроса. В случае, если типов может быть неограничено много, а проверка их нецелесообразна, то стоит выделить только id-часть нашего идентификатора и добавлять в параметры их как числа.
Определение типов
При обходе запроса, платформа определяет типы данных колонок, по которым в дальнейшем будет строиться ДО или коллекция. Начиная с версии 1.2 платформа собирает одинаковые колонки в список. В случае, если тип колонки в БД мапится на один и тот же тип данных платформы, то проблем не возникает. Сложности могут возникнуть, если в списке оказались разные типы данных.
Инструменты
На данный момент, в платформе существует механизм проверки модификаций платформы. См. Проверка корректности запроса на работающей системе
К сожалению, там не полная поддержка. Не хватает вывода типов данных, модификаци WHERE и наложения фильтров. Также, инструмент пока не очень удобный, т.к. не имеет своего GUI
Использование подсказок
Подсказки (hints) пишутся в синтаксисе, максимально приближенном к Oracle Hints (только многострочные). В SELECT можно указать неограниченное кол-во подсказок, разделитель между подсказками - любое кол-во whitespaces.
Правила использования в платформе:
После слова SELECT ставим hint в виде /*+ <HINT_1> <HINT_2> ... <HINT_N> */
Hint ставится в том SELECT, внутри которого он должен примениться. Т.е. если hint должен отработать во вложенном SELECT, то и указывать его нужно во вложенном. Допускается добавления множества hint к разным подзапросам одного запроса.
IGNORE ACL
Работает с версий: af5-1.2.63, af5-1.1.218
В платформе существует механизм, который позволяет убрать ошибочную проверку прав. Т.к. анализ запроса и вставка SQL с проверкой прав доступа может быть достаточно сложным, то в платформенный механизм время от времени допускает ошибки вставляя лишние проверки (по той или иной причине он не может верно определить, проверял ли он права (прямо или косвенно) на таблицу).
Использовать данный механизм, чтобы совсем отключить проверку прав без согласования архитектурного комитета - строго запрещается! В случае согласования, необходимо фиксировать договоренности в запросе, ссылка на который помещена в commit message. Без согласования и явно зафиксированного решения код может быть исправлен для соблюдения информационной безопасности.
Общий вид:
/*+ IGNORE ACL (<table_1><delimeter><table_2>...<table_N>) */
где <table> - имя или алиас таблицы; <delimeter> - пробел или запятая
Например:
/*+ IGNORE ACL (doc) */
Данный hint отключает проверку прав для таблицы с alias "doc" в FROM или JOIN данного SELECT.
IGNORE ACL умеет работать по alias и именам таблиц (если в запросе используется alias, то указываем его). При необходимости отключить права на несколько таблиц, то необходимо перечислить их в скобках через запятую или пробел. Не допускает использование нескольких IGNORE ACL в рамках одного SELECT, т.е. вот так нельзя:
/*+ IGNORE ACL (<table_1>) IGNORE ACL (<table_2>) */
Но, как и говорилось выше, допускает использование других hints.
Для включения возможности использования, необходимо выставить настройку.
CTE MATERIALIZATION HINT
Работает с версий: af5-1.2.88, af5-1.3.9, af5-1.1.227, af5-1.0.1-85
С Postgres 12 появилась возможность использовать материализованные и не материализованные CTE, а также добавилось автоматическая материализация в случае использования в запросе CTE более одного раза. В связи с этим, в платформе была добавлена возможность регулировать принудительное выставление материализации для системных CTE, которые добавляются платформой.
Общий вид:
/*+ CTE ([ <Type>, ] [ NOT ] MATERIALIZED) */
где <Type> может принимать значения ACL и STAMP.
Замечание. В версии af5-1.0.1 нет штампов. Для этой версии параметр <Type> использовать нельзя. Для обратной совместимости, в 1.1+ версиях отсутствие <Type> трактуется как ACL.
В случае, если необходимо явно прописать значения обеих типов, то используется второй hint:
/*+ CTE (ACL, NOT MATERIALIZED) CTE (STAMP, MATERIALIZED) */
Замечание. В случае ошибочного добавления hint одного и того же типа, второе включение будет проигнорировано, а в лог выведена ошибка, т.е. для
/*+ CTE (ACL, NOT MATERIALIZED) CTE (MATERIALIZED) */
будет выставлен NOT MATERIALIZED.
Замечание. Если в верхнеуровневом запросе используется "combining queries", т.е. запросы объединенные UNION, INTERSECT или EXCEPT, то hint нужно ставить только в 1 запросе, остальные будут проигнорированы
Для включения возможности использования, необходимо выставить настройку.
Известные проблемы
Использование alias для таблиц
В платформе есть баг, при котором она не во всех случаях может определить тип колонки в случае если не используются алиасы таблиц в столбцах SELECT. Например:
-- Если возникают проблемы в запросе SELECT id FROM my_table t -- Попробовать добавить алиас SELECT t.id FROM my_table t
Проблемы с экранированием
Могут возникнуть проблемы, если в части запроса используется экранирование, а в части - нет. Например:
SELECT temp."id" FROM (SELECT t.id FROM my_table t) temp
В этом случае уберите лишнее экранирование или добавьте в других местах. Также стоит завести запрос, приведя запрос в котором происходит проблема.
Причина. Часто сравнение столбцов происходит по простому "equals" без учета экранирования или регистра. Где я это нахожу, стараюсь исправить, но таких мест еще много.
Общие рекомендации
Использование alias таблиц
Платформенный механизм определения типов данных и выставления системных колонок не иерархичен. Это значит, что при определении типов колонок она ориентируется по алиасам таблиц (см. раздел с известными проблемами). В большинстве запросов проблемы не будет, но для избежания ошибок желательно везде использовать алиасы
Не использовать функции или явное приведение типов для системных таблиц
Платформа не сможет корректно определить тип колонки, если она обернута в функцию или используется явное приведение типов. колонка с id_type в таком случае добавлена не будет
Использование служебных слов
Желательно избегать использования служебных слов. Для своей работы платформа использует стороннюю библиотеку, которая не сможет распарсить запрос. Например, частая проблема возникает со словом value.
Список слов (будет обновляться):
- value
- sel
Перекрытие алиасами имен колонок
Старайтесь не использовать алиасы, аналогичные именам колонок. Это может приводить к ошибкам. Например (как не следует делать):
... hierProfile.name as hierprofile, ... left join lateral( select string_agg(name, '; ') as name from( select string_agg(PT_ProfileTop.name, '; ') as name from PT_ProfileTop where PT_ProfileTop.id = SO_Post.hierProfile ... ) hierProfile on 1=1 ...
В этом случае колонка hierProfile имеет строковый тип, но платформа, вероятно, определит его как Reference, т.к. одноименная колонка есть в so_post. Во многих случаях платформа корректно справляется с задачей определения (в том числе используя алиасы), но лучше избегать подобного использования
Другой очень частый пример:
... post."name" "post", ... JOIN "so_post" post ON post."id" = head."id" LEFT JOIN "so_appointmenthead" apphead ON head."id" = apphead."post" ...
Аналогичным образом стоит избегать перекрытия имен таблиц временными таблицами (CTE)
Создание багов
В случае, если найдена проблема при модификации запроса или платформа работает не так, как ожидается, необходимо завести баг. Не стоит пытаться перехитрить платформу, т.к. подобные хитрости в последствии приводят к большим работам по их поиску и устранению. К хитростям и обходам проблем стоит переходить после анализа ошибки разработчиком платформы.