14 Занятие 14: PL/pgSQL / Динамические команды
Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_14_plpgsql_dynamic.html Archive: DEV1-handouts-16.zip Страниц PDF: 16
Page 1: PL/pgSQL / Динамические команды
Авторские права
© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
Использование материалов курса
Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.
Page 2: Темы
- Причины использования
- Выполнение динамического запроса
- Способы формирования динамического запроса
Page 3: Динамический SQL
Текст SQL-команды формируется в момент выполнения
- Причины использования
- дополнительная гибкость в приложении
- формирование нескольких конкретных запросов
- вместо одного универсального для оптимизации
- Цена
- операторы не подготавливаются
- возрастает риск внедрения SQL-кода
- возрастает сложность сопровождения
Комментарии преподавателя
Под динамическими командами понимаются команды SQL, текст которых формируется и затем выполняется внутри PL/pgSQL-блока в подпрограммах или в анонимных блоках.
В большинстве случаев можно обойтись без динамических команд, но иногда они могут предоставить дополнительную гибкость. Например, можно встроить в определенные места приложения возможность выполнять команды, считанные из настроек системы. Управлять такими настройками могут специалисты поддержки во время эксплуатации приложения, а не программисты в момент разработки.
Иногда, при формировании отчета с большим количеством необязательных параметров, бывает проще формировать текст запроса прямо во время выполнения только для указанных параметров, чем заранее, при разработке, писать сложный запрос, учитывающий все возможные комбинации параметров.
Платой за использование динамических команд будет отказ от подготовленных операторов, которые по умолчанию используются в PL/pgSQL. Также нужно следить за безопасностью динамических команд с точки зрения возможности внедрения SQL-кода.
Следует отметить и существенное возрастание сложности сопровождения. В частности, в исходном коде приложения невозможно будет текстовым поиском обнаружить все выполняемые команды.
Page 4: Выполнение запроса
Конструкция EXECUTE
- выполняет строковое представление SQL-запроса
- позволяет использовать параметры
- переменные PL/pgSQL не становятся неявными параметрами
- Может использоваться вместо SQL-запроса
- сама по себе
- при открытии курсора
- в цикле по запросу
- в предложении RETURN QUERY
Комментарии преподавателя
Для выполнения динамических команд в PL/pgSQL используется конструкция EXECUTE, которая выполняет SQL-оператор, заданный в виде текстовой строки.
Динамический запрос может содержать явно заданные параметры. В тексте команды параметры обозначаются как $1, $2 и т. д., а значения параметров указываются в предложении USING. Параметры работают точно так же, как в подготовленных операторах (что рассматривалось в теме «Архитектура. Общее устройство PostgreSQL»). Однако переменные PL/pgSQL не становятся неявными параметрами, как это происходит при обычном, не динамическом, использовании SQL в PL/pgSQL.
Конструкция EXECUTE может использоваться как самостоятельный оператор (в этом случае просто выполняется динамическая команда). Она также может использоваться в циклах по запросу, при открытии курсора, в операторе RETURN QUERY — во всех этих случаях EXECUTE заменяет собой команду SQL.
https://postgrespro.ru/docs/postgresql/16/plpgsql-statements#PLPGSQL-ST ATEMENTS-EXECUTING-DYN
Тонкий момент: процедура не может управлять транзакциями, если она вызывается оператором EXECUTE.
Page 5: Выполнение динамического запроса
Оператор EXECUTE позволяет выполнить SQL-команду, заданную в виде строки.
Предложение INTO оператора EXECUTE позволяет вернуть одну (первую) строку результата в переменную составного типа или несколько скалярных переменных. Для проверки результата выполнения динамической команды можно использовать команду GET DIAGNOSTICS, как и в случае статических команд (но не переменную FOUND).
При необходимости с помощью STRICT можно гарантировать, что команда обработает ровно одну строку. Результат динамического запроса можно обработать в цикле FOR.
Этот же пример с использованием курсора.
Page 6: Page 6
Оператор RETURN QUERY для возврата строк из функции также может использовать динамические запросы. Напишем функцию, возвращающую все здания, возведенные архитектором до определенной даты. Для этого нам понадобятся параметры:
Page 7: Формирование команды
Подстановка значений параметров
- предложение USING
- гарантируется невозможность внедрения SQL-кода
- Экранирование значений
- идентификаторы: format(’%I’), quote_ident
- литералы: format(’%L’), quote_literal, quote_nullable
- внедрение SQL-кода невозможно при правильном использовании
- Обычные строковые функции
- конкатенация и др.
- возможно внедрение SQL-кода!
Комментарии преподавателя
Использование оператора EXECUTE имеет смысл, если команда формируется динамически. Приведенные ранее примеры можно было бы записать и без оператора EXECUTE.
Поскольку команда представляется текстовой строкой, ее можно сформировать обычными строковыми функциями, такими как конкатенация и т. п. В этом случае надо проявлять крайнюю осторожность, поскольку возможно внедрение SQL-кода.
Внедрение SQL-кода невозможно в принципе, если значения передаются как параметры с помощью предложения USING.
Однако параметры применимы не всегда: может понадобиться конкатенировать отдельные части запроса или подставлять в запрос имя таблицы. В этом случае для защиты от внедрений следует экранировать значения, полученные из ненадежного источника.
Для идентификаторов используется функция format со спецификатором %I или функция quote_ident. Эти функции формируют правильные имена идентификаторов, при необходимости заключая их в двойные кавычки и экранируя специальные символы.
Для подстановки литералов внутрь текста команды можно использовать функции quote_literal, quote_nullable или функцию format со спецификатором %L.
https://postgrespro.ru/docs/postgresql/16/functions-string
Page 8: Возможность внедрения SQL-кода
Перепишем функцию, возвращающую здания, добавив параметр — код города. По задумке такая функция должна позволять обращаться только к таблицам, начинающимся на city_.
Функция правильно работает при «нормальных» значениях параметров:
Однако злоумышленник может подобрать такое значение, которое изменит синтаксическую конструкцию запроса и позволит ему получить несанкционированный доступ к данным:
При использовании подготовленных операторов или динамических команд с параметрами это невозможно в принципе, так как структура SQL-запроса фиксируется при синтаксическом разборе. Выражение всегда останется выражением и не сможет превратиться, например, в имя таблицы.
Формирование динамической команды
Параметры в предложении USING нельзя использовать для имен объектов (названия таблиц, столбцов и пр.) в динамической команде. Такие идентификаторы необходимо экранировать, чтобы структура запроса не могла измениться:
Page 9: Page 9
То же самое выполняет и другая функция:
Вот как может выглядеть пример с созданием таблицы:
Вместо использования параметров, можно вставлять в строку литералы. В этом случае также требуется экранирование, но другое:
Это же выполняет и функция quote_nullable:
Похожая функция quote_literal отличается тем, что не превращает неопределенное значение в литерал:
В качестве примера перепишем функцию, возвращающую здания в определенном городе, без использования параметров, но так, чтобы она осталась безопасной.
Page 10: Page 10
Обратите внимание, что в этом случае получается два лишних приведения типов: сначала параметр типа integer приводится к строке, а затем, на этапе выполнения, строка обратно приводится к integer (в случае использования параметров USING такого не происходит):
Попытка передачи некорректного значения не приведет к успеху:
Page 11: Итоги
Динамические команды дают дополнительную гибкость
- Позволяют формировать отдельные запросы для разных значений параметров с целью оптимизации
- Не подходят для коротких, частых запросов
- Увеличивается сложность поддержки
Page 12: Практика
- Измените функцию get_catalog так, чтобы запрос к представлению catalog_v формировался динамически и содержал условия только на те поля, которые заполнены на форме поиска в «Магазине».
- Убедитесь, что реализация не допускает возможности
- внедрения SQL-кода.
- Проверьте работу функции в приложении.
Комментарии преподавателя
- Скажем, если на форме поиска не заполнены поля «Название книги» и «Имя автора», но установлен флажок «Есть на складе», должен быть сформирован такой, например, запрос:
Учтите, что поиск при такой реализации вовсе не обязательно будет работать эффективнее, но поддерживать его совершенно точно будет сложнее. Поэтому в реальной работе не стоит прибегать к такому приему, если для того нет веских оснований. Тема оптимизации запросов рассматривается в курсе QPT.
Page 13: 1. Функция get_catalog
Page 14: Практика+
- Создайте функцию, которая возвращает строки матричного отчета по функциям в базе данных.
- Столбцы должны содержать имена владельцев функций,
- строки — названия схем, а ячейки — количество функций
- данного владельца в данной схеме.
- Как можно вызвать такую функцию?
Комментарии преподавателя
- Примерный вид результата:
Количество столбцов в запросе заранее не известно. Поэтому необходимо сконструировать запрос и затем динамически его выполнить. Текст запроса может быть таким:
Выделенные строки — динамическая часть — необходимо сформировать дополнительным запросом. Начало и конец запроса — статические.
Столбец proowner имеет тип oid, для получения имени владельца можно воспользоваться конструкцией proowner::regrole::text.
Page 15: Получение матричного отчета
Вспомогательная функция для формирования текста динамического запроса:
Итоговый текст запроса:
Теперь создаем функцию для матричного отчета:
Простое выполнение запроса приведет к ошибке, так как не указана структура возвращаемых записей:
Page 16: Page 16
В этом состоит важное ограничение на использование функций, возвращающих произвольную выборку. В момент вызова необходимо знать и указать структуру возвращаемой записи. В общем случае структура возвращаемой записи может быть неизвестна, но, применительно к нашему матричному отчету, можно выполнить еще один запрос, который покажет, как правильно вызвать функцию matrix. Подготовим текст запроса:
Теперь мы можем вызовом martix_call получить запрос, отражающий структуру матричного отчета, а затем выполнить этот запрос и получить отчет (psql позволяет все это сделать одной командой \gexec):
Матричный отчет корректно формируется. Уровень изоляции Repeatable Read гарантирует, что отчет сформируется, даже если между двумя запросами появится функция у нового владельца. Можно было бы и напрямую выполнить запрос, возвращаемый функцией form_query. Но задача получить в клиентском приложении список возвращаемых столбцов все равно останется. Функция matrix_call показывает, как ее можно решить дополнительным запросом. Еще один вариант решения заключается в том, чтобы вместо набора записей произвольной структуры возвращать набор строк слабоструктурированного типа (такого, как JSON или XML). Эти типы рассматриваются в курсе DEV2.