Назва таблиці як параметр функції PostgreSQL


85

Я хочу передати ім'я таблиці як параметр у функції 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)працювати одночасно, а другий - не одночасно? І як це можна було вирішити?


Я знаю, що це запитання старе, але я знайшов його під час пошуку відповіді на інше питання. Чи не могла ваша функція просто запитати інформаційну_схему? Я маю на увазі, що це свого роду для чогось - дозволити вам запитувати і бачити, які об’єкти існують у базі даних. Просто ідея.
David S

@DavidS Дякую за коментар, спробую це.
John Doe

Відповіді:


124

Це можна ще спростити та вдосконалити:

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:

db <> скрипка тут демонструє ін'єкцію SQL
Старий sqlfiddle


2
@suhprano: Звичайно. Спробуйте:DO $$BEGIN EXECUTE 'ANALYZE mytbl'; END$$;
Ервін Брандштеттер

чому% s, а не% L?
Лотос,

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');
l mingzhi

як ми можемо отримати всі стовпці?
Ашіш

12

Якщо це можливо, не робіть цього.

Це відповідь - це анти-шаблон. Якщо клієнт знає таблицю, з якої йому потрібні дані, тоді 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,

1
Але враховуйте, що дія спроби натиснути кнопку, яка не існує, просто генерує виняток, незалежно від того, як ви з цим справляєтесь. Ви насправді не можете натиснути неіснуючу кнопку, тому немає ніякої вигоди додавати поверх натискання кнопки шар для перевірки неіснуючих чисел, оскільки такий запис номера не існував до того, як ви створили згаданий шар! На мій погляд, абстракція є найпотужнішим інструментом програмування. Однак додавання шару, який просто погано дублює існуючу абстракцію, є неправильним . Сама база даних уже є абстракційним шаром, який відображає імена в наборах даних.
ErikE

3
Пляма на. Вся суть SQL полягає у вираженні набору даних, які ви хочете витягти. Єдине, що робить ця функція, це інкапсуляція "консервованого" оператора SQL. Враховуючи той факт, що ідентифікатор також жорстко закодований, все це має неприємний запах.
Нік Христов

1
@three Поки хтось не переходить до фази засвоєння (див . модель набуття навичок Дрейфуса ) навички, він повинен просто абсолютно дотримуватися правил типу "НЕ передавати імена таблиць у процедуру, яка буде використана в динамічному SQL". Навіть натяк на те, що це не завжди погано - це саме по собі погана порада . Знаючи це, у новачка з'явиться спокуса ним скористатися! Це погано. Тільки майстри теми повинні порушувати правила, оскільки вони єдині, хто має досвід, знають у кожному конкретному випадку, чи насправді таке порушення правил має сенс.
ErikE

1
@ 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()оскільки це додало зайві лапки, що мене трохи здивувало, ну, тому що це використовується в більшості прикладів.
John Doe,

Ці додаткові лапки будуть потрібні, якщо / коли назва таблиці містить символи поза [az], або якщо / коли вона зіткнеться із зарезервованим ідентифікатором (приклад: "група" як назва таблиці)
Даніель Веріте

І, до речі, не могли б ви надати посилання, яке підтверджувало б, що IF EXISTS <query>конструкція не існує? Я майже впевнений, що бачив щось подібне як робочий зразок коду.
John Doe,

1
@JohnDoe: IF EXISTS (<query>) THEN ...є цілком допустимою конструкцією в plpgsql. Тільки не з динамічним SQL для <query>. Я його багато використовую. Крім того, цю функцію можна трохи вдосконалити. Я розмістив відповідь.
Ервін Брандштеттер

1
Вибачте, ви маєте рацію if exists(<query>), це дійсно в загальному випадку. Просто перевірив та змінив відповідь відповідно.
Даніель Веріте,

4

Перший насправді не "працює" у тому сенсі, який ви маєте на увазі, він працює лише в тій мірі, в якій він не генерує помилки.

Спробуйте SELECT * FROM quote_ident('table_that_does_not_exist');, і ви побачите, чому ваша функція повертає 1: select повертає таблицю з одним стовпцем (з іменем quote_ident) з одним рядком (змінною $1або в цьому конкретному випадку table_that_does_not_exist).

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


Велике спасибі, Метт, table_that_does_not_existдав той самий результат, ти маєш рацію.
Джон Доу,

2

Якщо питання полягало в тому, щоб перевірити, чи порожня таблиця (id = 1), ось спрощена версія збереженого процесу Erwin:

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


1
Створення newтаблиці відрізняється від роботи з іменем існуючої таблиці. У будь-якому випадку, вам слід уникнути текстових параметрів, що виконуються як код, або ви відкриті для введення SQL.
Ервін Брандштеттер

О, так, моя помилка. Тема ввів мене в оману, і до того ж я не прочитав її до кінця. Зазвичай у моєму випадку. : P Чому код із текстовим параметром піддається ін’єкції?
dm3

На жаль, це справді небезпечно. Дякую за відповідь!
dm3,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.