Имя таблицы как параметр функции PostgreSQL

87

Я хочу передать имя таблицы в качестве параметра функции Postgres. Я пробовал этот код:

CREATE OR REPLACE FUNCTION some_f(param character varying) RETURNS integer 
AS $$
    BEGIN
    IF EXISTS (select * from quote_ident($1) where quote_ident($1).id=1) THEN
     return 1;
    END IF;
    return 0;
    END;
$$ LANGUAGE plpgsql;

select some_f('table_name');

И я получил вот что:

ERROR:  syntax error at or near "."
LINE 4: ...elect * from quote_ident($1) where quote_ident($1).id=1)...
                                                             ^

********** Error **********

ERROR: syntax error at or near "."

И вот ошибка, которую я получил при изменении на это select * from quote_ident($1) tab where tab.id=1:

ERROR:  column tab.id does not exist
LINE 1: ...T EXISTS (select * from quote_ident($1) tab where tab.id...

Наверное, quote_ident($1)работает, потому что без той where quote_ident($1).id=1детали я получаю 1, а значит, что-то выделено. Почему первый может quote_ident($1)работать, а второй нет одновременно? И как это можно было решить?

Джон Доу
источник
Я знаю, что это старый вопрос, но я нашел его, когда искал ответ на другой вопрос. Не могла ли ваша функция просто запросить информационную_схему? Я имею в виду, что это своего рода то, для чего это нужно - чтобы вы могли запрашивать и видеть, какие объекты существуют в базе данных. Просто идея.
Дэвид С.
@DavidS Спасибо за комментарий, я попробую.
John Doe

Ответы:

126

Это можно еще больше упростить и улучшить:

CREATE OR REPLACE FUNCTION some_f(_tbl regclass, OUT result integer)
    LANGUAGE plpgsql AS
$func$
BEGIN
   EXECUTE format('SELECT (EXISTS (SELECT FROM %s WHERE id = 1))::int', _tbl)
   INTO result;
END
$func$;

Вызов с именем, указанным в схеме (см. Ниже):

SELECT some_f('myschema.mytable');  -- would fail with quote_ident()

Или же:

SELECT some_f('"my very uncommon table name"');

Основные моменты

  • Используйте OUTпараметр, чтобы упростить функцию. Вы можете напрямую выбрать результат динамического SQL в нем и готово. Нет необходимости в дополнительных переменных и коде.

  • EXISTSделает именно то, что вы хотите. Вы получаете, существует trueли строка или falseнет. Есть разные способы сделать это, EXISTSкак правило, наиболее эффективно.

  • Кажется, вам нужно вернуть целое число , поэтому я привел booleanрезультат от EXISTSк integer, что дает именно то, что было у вас. Вместо этого я бы вернул логическое значение .

  • Я использую тип идентификатора объекта в regclassкачестве типа ввода для _tbl. Это делает все quote_ident(_tbl)или format('%I', _tbl)подойдет, но лучше, потому что:

  • .. он также предотвращает внедрение SQL .

  • .. он терпит неудачу немедленно и более изящно, если имя таблицы недействительно / не существует / невидимо для текущего пользователя. (Аregclass Параметр применим только к существующим таблицам.)

  • .. он работает с именами таблиц с указанием схемы, где простой quote_ident(_tbl) или не format(%I)будет работать, потому что они не могут разрешить двусмысленность. Вам нужно будет передавать и экранировать имена схемы и таблицы отдельно.

  • Я до сих пор использую format(), потому что он упрощает синтаксис (и демонстрирует, как он используется), но %sвместо %I. Обычно запросы более сложные, поэтомуformat() помогает больше. Для простого примера мы могли бы просто объединить:

      EXECUTE 'SELECT (EXISTS (SELECT FROM ' || _tbl || ' WHERE id = 1))::int'
    
  • Нет необходимости уточнять idстолбец, пока в FROMсписке всего одна таблица . В этом примере нет двусмысленности. (Динамические) команды SQL внутри EXECUTEимеют отдельную область видимости , переменные функции или параметры там не видны - в отличие от простых команд SQL в теле функции.

Вот почему ты всегда правильно избегаете пользовательского ввода для динамического SQL:

дб <> скрипку здесь демонстрирует инъекции SQL
Старый sqlfiddle

Эрвин Брандштеттер
источник
2
@suhprano: Конечно. Попробуйте:DO $$BEGIN EXECUTE 'ANALYZE mytbl'; END$$;
Эрвин Брандштеттер,
почему% s, а не% L?
Lotus
3
@Lotus: Объяснение в ответе. regclassзначения экранируются автоматически при выводе в виде текста. %Lбыло бы неправильно в этом случае.
Эрвин Брандштеттер
CREATE OR REPLACE FUNCTION table_rows(_tbl regclass, OUT result integer) AS $func$ BEGIN EXECUTE 'SELECT (SELECT count(1) FROM ' || _tbl || ' )::int' INTO result; END $func$ LANGUAGE plpgsql; создать функцию подсчета строк таблицы,select table_rows('nf_part1');
Феррис
как мы можем получить все столбцы?
Ашиш
13

По возможности не делайте этого.

Это ответ - это антипаттерн. Если клиент знает таблицу, из которой ему нужны данные, тогдаSELECT FROM ThatTable . Если база данных спроектирована таким образом, чтобы это требовалось, кажется, что она спроектирована неоптимально. Если слою доступа к данным необходимо знать, существует ли значение в таблице, легко составить SQL в этом коде, и помещать этот код в базу данных нецелесообразно.

Для меня это похоже на установку устройства внутри лифта, где можно ввести номер желаемого этажа. После нажатия кнопки Go он перемещает механическую руку к нужной кнопке для нужного этажа и нажимает ее. Это создает множество потенциальных проблем.

Обратите внимание: здесь нет намерения насмехаться. Мой глупый пример с лифтом был * самым лучшим устройством, которое я мог представить * для лаконичного указания на проблемы с этой техникой. Он добавляет бесполезный уровень косвенного обращения, перемещая выбор имени таблицы из пространства вызывающей стороны (с использованием надежного и хорошо понятного DSL, SQL) в гибрид с использованием непонятного / причудливого серверного кода SQL.

Такое разделение ответственности за счет перемещения логики построения запроса в динамический SQL затрудняет понимание кода. Это нарушает стандартное и надежное соглашение (как SQL-запрос выбирает, что выбрать) в названии настраиваемого кода чревато ошибкой.

Вот подробные сведения о некоторых потенциальных проблемах этого подхода:

  • Динамический SQL предлагает возможность SQL-инъекции, которую трудно распознать в коде внешнего интерфейса или только в коде серверной части (чтобы увидеть это, нужно изучить их вместе).

  • Хранимые процедуры и функции могут получать доступ к ресурсам, на которые владелец SP / функции имеет права, а вызывающий - нет. Насколько я понимаю, без особой осторожности, то по умолчанию, когда вы используете код, который производит динамический SQL и запускает его, база данных выполняет динамический SQL под правами вызывающего. Это означает, что вы либо вообще не сможете использовать привилегированные объекты, либо вам придется открыть их для всех клиентов, увеличивая поверхность потенциальной атаки на привилегированные данные. Установка SP / функции во время создания на постоянный запуск от имени конкретного пользователя (в SQL Server EXECUTE AS) может решить эту проблему, но усложняет задачу. Это усугубляет риск SQL-инъекции, упомянутый в предыдущем пункте, делая динамический SQL очень привлекательным вектором атаки.

  • Когда разработчик должен понять, что делает код приложения, чтобы изменить его или исправить ошибку, ему будет очень трудно получить точный выполняемый SQL-запрос. Можно использовать профилировщик SQL, но это требует особых привилегий и может отрицательно сказаться на производительности производственных систем. Выполненный запрос может регистрироваться SP, но это увеличивает сложность с сомнительной выгодой (требующей размещения новых таблиц, очистки старых данных и т. Д.) И совершенно неочевидно. Фактически, некоторые приложения спроектированы таким образом, что разработчик не имеет учетных данных базы данных, поэтому для него становится практически невозможным фактически увидеть отправляемый запрос.

  • При возникновении ошибки, например, при попытке выбрать несуществующую таблицу, вы получите сообщение в строке «недопустимое имя объекта» из базы данных. Это будет происходить точно так же, независимо от того, составляете ли вы SQL в серверной части или в базе данных, но разница в том, что какой-то плохой разработчик, пытающийся устранить неполадки в системе, должен углубиться на один уровень глубже в еще одну пещеру ниже той, где находится Проблема существует, чтобы покопаться в чудо-процедуре, которая делает все, чтобы попытаться выяснить, в чем проблема. В журналах не будет отображаться «Ошибка в GetWidget», будет отображаться «Ошибка в OneProcedureToRuleThemAllRunner». Эта абстракция, как правило, ухудшает систему .

Пример на псевдо-C # переключения имен таблиц на основе параметра:

string sql = $"SELECT * FROM {EscapeSqlIdentifier(tableName)};"
results = connection.Execute(sql);

Хотя это не устраняет все возможные проблемы, которые можно вообразить, недостатки, которые я обозначил с помощью другой техники, отсутствуют в этом примере.

Эрике
источник
4
Я не совсем согласен с этим. Допустим, вы нажимаете кнопку «Пуск», а затем какой-то механизм проверяет, существует ли пол. Функции могут использоваться в триггерах, которые, в свою очередь, могут проверять некоторые условия. Это решение может быть не самым красивым, но если система уже достаточно велика и вам нужно внести некоторые коррективы в ее логику, что ж, я полагаю, что этот выбор не так драматичен.
John Doe
2
Но учтите, что попытка нажать кнопку, которой не существует, просто вызовет исключение, независимо от того, как вы с этим справитесь. На самом деле вы не можете нажать несуществующую кнопку, поэтому нет никакой пользы от добавления поверх нажатия кнопки слоя для проверки несуществующих чисел, поскольку такая запись числа не существовала до того, как вы создали указанный слой! На мой взгляд, абстракция - самый мощный инструмент программирования. Однако добавлять слой, который просто плохо дублирует существующую абстракцию, неправильно . Сама база данных уже является слоем абстракции, который отображает имена в наборы данных.
ErikE
3
На месте. Вся суть SQL состоит в том, чтобы выразить набор данных, которые вы хотите извлечь. Единственное, что делает эта функция, - это инкапсулирует «стандартный» оператор SQL. Учитывая тот факт, что идентификатор также жестко запрограммирован, все это имеет неприятный запах.
Ник Христов
2
@three Пока кто-нибудь не достигнет фазы мастерства (см. навыком модель приобретения навыка Дрейфуса ), он должен просто полностью подчиняться правилам типа «НЕ передавать имена таблиц в процедуру, которая будет использоваться в динамическом SQL». Даже намек на то, что это не всегда плохо, - плохой совет . Зная это, у новичка возникнет соблазн воспользоваться этим! Это плохо. Только мастера темы должны нарушать правила, поскольку они единственные, у кого есть опыт, чтобы знать в каждом конкретном случае, действительно ли такое нарушение правил имеет смысл.
ErikE
2
@ three-cups Я обновил намного больше деталей о том, почему это плохая идея.
ErikE
10

Внутри кода plpgsql оператор EXECUTE должен использоваться для запросов, в которых имена таблиц или столбцы берутся из переменных. Также IF EXISTS (<query>)конструкция не допускается, когдаquery она создается динамически.

Вот ваша функция с исправленными обеими проблемами:

CREATE OR REPLACE FUNCTION some_f(param character varying) RETURNS integer 
AS $$
DECLARE
 v int;
BEGIN
      EXECUTE 'select 1 FROM ' || quote_ident(param) || ' WHERE '
            || quote_ident(param) || '.id = 1' INTO v;
      IF v THEN return 1; ELSE return 0; END IF;
END;
$$ LANGUAGE plpgsql;
Даниэль Верите
источник
Спасибо, я делал то же самое пару минут назад, когда читал ваш ответ. Единственное отличие в том, что мне пришлось удалить, quote_ident()потому что он добавил лишние кавычки, что меня немного удивило, потому что оно используется в большинстве примеров.
Джон Доу
Эти дополнительные кавычки потребуются, если / когда имя таблицы содержит символы вне [az], или если / когда оно конфликтует с зарезервированным идентификатором (пример: «группа» в качестве имени таблицы)
Даниэль Верите
И, кстати, не могли бы вы предоставить ссылку, которая доказывает, что IF EXISTS <query>конструкции не существует? Я почти уверен, что видел что-то подобное как рабочий образец кода.
Джон Доу
1
@JohnDoe: IF EXISTS (<query>) THEN ...вполне допустимая конструкция в plpgsql. Только не с динамическим SQL для <query>. Я часто им пользуюсь. Кроме того, эту функцию можно немного улучшить. Я отправил ответ.
Эрвин Брандштеттер,
1
Извините, вы правы if exists(<query>), в общем случае это действительно так. Просто проверил и соответствующим образом изменил ответ.
Даниэль Верите
4

Первый на самом деле не «работает» в том смысле, в котором вы имеете в виду, он работает только постольку, поскольку он не вызывает ошибки.

Попробуйте SELECT * FROM quote_ident('table_that_does_not_exist');, и вы увидите, почему ваша функция возвращает 1: выборка возвращает таблицу с одним столбцом (с именем quote_ident) с одной строкой (переменная $1или в этом конкретном случае table_that_does_not_exist).

То, что вы хотите сделать, потребует динамического SQL, который на самом деле является местом, где quote_*должны использоваться функции.

Мэтт
источник
Большое спасибо, Мэтт, table_that_does_not_existдал такой же результат, ты прав.
Джон Доу,
2

Если вопрос заключался в том, чтобы проверить, пуста ли таблица или нет (id = 1), вот упрощенная версия хранимой процедуры Эрвина:

CREATE OR REPLACE FUNCTION isEmpty(tableName text, OUT zeroIfEmpty integer) AS
$func$
BEGIN
EXECUTE format('SELECT COALESCE ((SELECT 1 FROM %s LIMIT 1),0)', tableName)
INTO zeroIfEmpty;
END
$func$ LANGUAGE plpgsql;
Жюльен Фениу
источник
1

Я знаю, что это старый поток, но недавно я столкнулся с ним, пытаясь решить ту же проблему - в моем случае для некоторых довольно сложных сценариев.

Превращение всего сценария в динамический SQL - не лучший вариант. Это утомительная и подверженная ошибкам работа, и вы теряете возможность параметризации: параметры должны быть интерполированы в константы в SQL, что плохо сказывается на производительности и безопасности.

Вот простой трюк, который позволяет сохранить SQL в неизменном виде, если вам нужно только выбрать из таблицы - используйте динамический SQL для создания временного представления:

CREATE OR REPLACE FUNCTION some_f(_tbl varchar) returns integer
AS $$
BEGIN
    drop view if exists myview;
    execute format('create temporary view myview as select * from %s', _tbl);
    -- now you can reference myview in the SQL
    IF EXISTS (select * from myview where myview.id=1) THEN
     return 1;
    END IF;
    return 0;
END;
$$ language plpgsql;
Натан Мейерс
источник
0

Если вы хотите, чтобы имя таблицы, имя столбца и значение динамически передавались для работы в качестве параметра

используйте этот код

create or replace function total_rows(tbl_name text, column_name text, value int)
returns integer as $total$
declare
total integer;
begin
    EXECUTE format('select count(*) from %s WHERE %s = %s', tbl_name, column_name, value) INTO total;
    return total;
end;
$total$ language plpgsql;


postgres=# select total_rows('tbl_name','column_name',2); --2 is the value
Сэндип Дебнат
источник
-2

У меня 9.4 версия PostgreSQL, и я всегда использую этот код:

CREATE FUNCTION add_new_table(text) RETURNS void AS
$BODY$
begin
    execute
        'CREATE TABLE ' || $1 || '(
        item_1      type,
        item_2      type
        )';
end;
$BODY$
LANGUAGE plpgsql

А потом:

SELECT add_new_table('my_table_name');

У меня это работает хорошо.

Внимание! Приведенный выше пример является одним из тех, которые показывают «Как этого не сделать, если мы хотим сохранить безопасность при запросе к базе данных»: P

дм3
источник
1
Создание newтаблицы отличается от работы с именем существующей таблицы. В любом случае вы должны избегать текстовых параметров, выполняемых как код, или вы открыты для SQL-инъекции.
Эрвин Брандштеттер
Ах да, моя ошибка. Тема ввела меня в заблуждение и к тому же я не дочитал ее до конца. Обычно в моем случае. : P Почему код с текстовым параметром подвергается инъекции?
dm3
Ой, это действительно опасно. Спасибо за ответ!
dm3