Nazwa tabeli jako parametr funkcji PostgreSQL

85

Chcę przekazać nazwę tabeli jako parametr w funkcji Postgres. Wypróbowałem ten kod:

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

I mam to:

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 "."

A oto błąd, który otrzymałem po zmianie na to 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...

Prawdopodobnie quote_ident($1)działa, bo bez where quote_ident($1).id=1części, którą dostaję 1, czyli coś jest wybrane. Dlaczego pierwsza może quote_ident($1)działać, a druga nie w tym samym czasie? Jak można to rozwiązać?

nieznany z nazwiska
źródło
Wiem, że to trochę stare pytanie, ale znalazłem je, szukając odpowiedzi na inny problem. Czy Twoja funkcja nie mogłaby po prostu wysłać zapytania do schematu informacyjnego? Chodzi mi o to, że w pewnym sensie do tego służy - abyś mógł zapytać i zobaczyć, jakie obiekty istnieją w bazie danych. Tylko pomysł.
David S,
@DavidS Dzięki za komentarz, spróbuję.
John Doe

Odpowiedzi:

125

Można to jeszcze bardziej uprościć i ulepszyć:

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$;

Zadzwoń z nazwą kwalifikowaną w schemacie (patrz poniżej):

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

Lub:

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

Główne punkty

  • Użyj OUTparametru, aby uprościć funkcję. Możesz bezpośrednio wybrać wynik dynamicznego SQL do niego i gotowe. Nie ma potrzeby stosowania dodatkowych zmiennych i kodu.

  • EXISTSrobi dokładnie to, czego chcesz. Otrzymasz, truejeśli wiersz istnieje lub w falseinny sposób. Można to zrobić na różne sposoby, EXISTSzazwyczaj jest to najbardziej wydajne.

  • Wydaje się, że chcesz odzyskać liczbę całkowitą , więc rzutuję booleanwynik z EXISTSdo integer, co daje dokładnie to, co miałeś. Zamiast tego zwróciłbym wartość logiczną .

  • Używam typu identyfikatora obiektu regclassjako typu wejściowego dla _tbl. To robi wszystko quote_ident(_tbl)lub format('%I', _tbl)zrobiłoby, ale lepiej, ponieważ:

  • .. równie dobrze zapobiega iniekcji SQL .

  • .. zawodzi natychmiast i bardziej wdzięcznie, jeśli nazwa tabeli jest nieprawidłowa / nie istnieje / jest niewidoczna dla bieżącego użytkownika. ( regclassParametr ma zastosowanie tylko do istniejących tabel).

  • .. działa z nazwami tabel kwalifikowanymi według schematu, w przypadku których zwykły quote_ident(_tbl)lub format(%I)nie powiedzie się, ponieważ nie mogą rozwiązać niejednoznaczności. Musiałbyś osobno przekazywać i zmieniać nazwy schematów i tabel.

  • Nadal używam format(), ponieważ upraszcza składnię (i pokazuje, jak jest używany), ale z %szamiast %I. Zazwyczaj zapytania są bardziej złożone, więc format()pomaga bardziej. Dla prostego przykładu moglibyśmy równie dobrze połączyć:

      EXECUTE 'SELECT (EXISTS (SELECT FROM ' || _tbl || ' WHERE id = 1))::int'
    
  • Nie ma potrzeby kwalifikowania idkolumny do tabeli, jeśli na FROMliście jest tylko jedna tabela . W tym przykładzie nie jest możliwa żadna dwuznaczność. (Dynamiczne) polecenia SQL wewnątrz EXECUTEmają oddzielny zakres , zmienne funkcyjne lub parametry nie są tam widoczne - w przeciwieństwie do zwykłych poleceń SQL w treści funkcji.

Oto dlaczego zawsze poprawnie unikasz wprowadzania danych przez użytkownika dla dynamicznego SQL:

db <> fiddle tutaj demonstruje wstrzyknięcie SQL
Stary sqlfiddle

Erwin Brandstetter
źródło
2
@suhprano: Jasne. Spróbuj:DO $$BEGIN EXECUTE 'ANALYZE mytbl'; END$$;
Erwin Brandstetter
dlaczego% s a nie% L?
Lotus
3
@Lotus: Wyjaśnienie jest w odpowiedzi. regclasswartości są automatycznie zmieniane przy wyprowadzaniu jako tekst. %Lbyłby zły w tym przypadku.
Erwin Brandstetter
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; utwórz funkcję liczenia wierszy w tabeli,select table_rows('nf_part1');
l mingzhi
jak możemy zdobyć wszystkie kolumny?
Ashish
12

Jeśli to możliwe, nie rób tego.

Oto odpowiedź - to anty-wzór. Jeśli klient zna tabelę, z której chce uzyskać dane, to SELECT FROM ThatTable. Jeśli baza danych jest zaprojektowana w taki sposób, że jest to wymagane, wydaje się, że została zaprojektowana w sposób nieoptymalny. Jeśli warstwa dostępu do danych musi wiedzieć, czy wartość istnieje w tabeli, łatwo jest utworzyć kod SQL w tym kodzie, a umieszczenie tego kodu w bazie danych nie jest dobre.

Wydaje mi się, że jest to instalowanie urządzenia wewnątrz windy, w którym można wpisać numer pożądanego piętra. Po naciśnięciu przycisku Go przesuwa mechaniczną dłoń do odpowiedniego przycisku dla żądanego piętra i naciska go. To wprowadza wiele potencjalnych problemów.

Uwaga: tutaj nie ma zamiaru kpić. Mój głupi przykład z windą był * najlepszym urządzeniem, jakie mogłem sobie wyobrazić * do zwięzłego wskazywania problemów z tą techniką. Dodaje bezużyteczną warstwę pośredniej, przenosząc wybór nazwy tabeli z przestrzeni wywołującej (przy użyciu solidnego i dobrze zrozumiałego DSL, SQL) do hybrydy, używając niejasnego / dziwacznego kodu SQL po stronie serwera.

Taki podział odpowiedzialności poprzez przeniesienie logiki konstrukcji zapytań do dynamicznego SQL sprawia, że ​​kod jest trudniejszy do zrozumienia. Narusza standardową i niezawodną konwencję (jak zapytanie SQL wybiera, co wybrać) w nazwie niestandardowego kodu obarczonego potencjałem błędu.

Oto szczegółowe punkty dotyczące niektórych potencjalnych problemów związanych z tym podejściem:

  • Dynamiczny SQL oferuje możliwość iniekcji SQL, która jest trudna do rozpoznania w kodzie frontonu lub w samym kodzie zaplecza (aby to zobaczyć, trzeba je razem sprawdzić).

  • Procedury i funkcje składowane mogą uzyskiwać dostęp do zasobów, do których właściciel SP / funkcji ma prawa, ale wywołujący nie. O ile rozumiem, bez szczególnej ostrożności, to domyślnie, gdy używasz kodu, który tworzy dynamiczny SQL i uruchamia go, baza danych wykonuje dynamiczny SQL na prawach wywołującego. Oznacza to, że albo w ogóle nie będziesz mógł korzystać z uprzywilejowanych obiektów, albo będziesz musiał otworzyć je dla wszystkich klientów, zwiększając powierzchnię potencjalnego ataku na dane uprzywilejowane. Ustawienie SP / funkcji w czasie tworzenia, aby zawsze działała jako określony użytkownik (w SQL Server EXECUTE AS) może rozwiązać ten problem, ale komplikuje sprawę. Zwiększa to ryzyko wstrzyknięcia SQL, o którym mowa w poprzednim punkcie, czyniąc dynamiczny SQL bardzo kuszącym wektorem ataku.

  • Kiedy programista musi zrozumieć, co robi kod aplikacji, aby go zmodyfikować lub naprawić błąd, bardzo trudno mu będzie uzyskać dokładne zapytanie SQL, które jest wykonywane. Można użyć profilera SQL, ale wymaga to specjalnych uprawnień i może mieć negatywny wpływ na wydajność systemów produkcyjnych. Wykonane zapytanie może zostać zarejestrowane przez SP, ale zwiększa to złożoność i przynosi wątpliwe korzyści (wymaga dostosowania nowych tabel, usunięcia starych danych itp.) I jest dość nieoczywiste. W rzeczywistości niektóre aplikacje są zaprojektowane w taki sposób, że programista nie ma poświadczeń bazy danych, więc zobaczenie przesłanego zapytania jest dla niego prawie niemożliwe.

  • Gdy wystąpi błąd, na przykład podczas próby wybrania tabeli, która nie istnieje, otrzymasz z bazy danych komunikat zawierający informację o „nieprawidłowej nazwie obiektu”. Stanie się to dokładnie tak samo, niezależnie od tego, czy tworzysz kod SQL na zapleczu, czy w bazie danych, ale różnica polega na tym, że jakiś biedny programista, który próbuje rozwiązać problem z systemem, musi przejść o jeden poziom głębiej do kolejnej jaskini poniżej tej, w której istnieje problem, aby zagłębić się w cudowną procedurę, która robi wszystko, aby spróbować ustalić, na czym polega problem. Dzienniki nie pokażą „Błąd w GetWidget”, pokaże „Błąd w OneProcedureToRuleThemAllRunner”. Ta abstrakcja ogólnie pogorszy system .

Przykład w pseudo-C # przełączania nazw tabel na podstawie parametru:

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

Chociaż nie eliminuje to każdego możliwego problemu, jaki można sobie wyobrazić, wady, które nakreśliłem w innej technice, są nieobecne w tym przykładzie.

ErikE
źródło
4
Nie do końca się z tym zgadzam. Powiedzmy, że wciskasz ten przycisk „Go”, a następnie jakiś mechanizm sprawdza, czy podłoga istnieje. Funkcje mogą być używane w wyzwalaczach, które z kolei mogą sprawdzać niektóre warunki. Ta susza może nie jest najpiękniejsza, ale jeśli system jest już wystarczająco duży i trzeba wprowadzić pewne poprawki w jego logice, cóż, ten wybór nie jest chyba tak dramatyczny.
John Doe,
1
Ale weź pod uwagę, że próba naciśnięcia przycisku, który nie istnieje, po prostu wygeneruje wyjątek, bez względu na to, jak sobie z nim poradzisz. W rzeczywistości nie możesz wcisnąć nieistniejącego przycisku, więc nie ma korzyści z dodawania, oprócz naciskania przycisków, warstwy sprawdzającej nieistniejące liczby, ponieważ taki wpis numeru nie istniał przed utworzeniem wspomnianej warstwy! Abstrakcja to moim zdaniem najpotężniejsze narzędzie programowania. Jednak dodanie warstwy, która jedynie słabo powiela istniejącą abstrakcję, jest błędne . Sama baza danych jest już warstwą abstrakcji, która odwzorowuje nazwy na zestawy danych.
ErikE
3
Na miejscu. Celem SQL jest wyrażenie zbioru danych, które chcesz wyodrębnić. Jedyne, co robi ta funkcja, to hermetyzacja gotowej instrukcji SQL. Biorąc pod uwagę fakt, że identyfikator jest również zakodowany na stałe, całość ma nieprzyjemny zapach.
Nick Hristov
1
@three Dopóki ktoś nie znajdzie się w fazie opanowania umiejętności (patrz model nabywania umiejętności Dreyfusa ), powinien po prostu bezwzględnie przestrzegać reguł typu „NIE przekazywać nazw tabel do procedury, która ma być używana w dynamicznym SQL”. Nawet sugerowanie, że nie zawsze jest źle, jest samo w sobie złą radą . Wiedząc o tym, początkujący będzie miał ochotę go użyć! To źle. Tylko mistrzowie tematu powinni łamać zasady, ponieważ tylko oni mają doświadczenie, aby wiedzieć w konkretnym przypadku, czy takie łamanie zasad ma sens.
ErikE
1
@ three-cups Zaktualizowałem, podając dużo więcej szczegółów na temat tego, dlaczego to zły pomysł.
ErikE
10

W kodzie plpgsql instrukcja EXECUTE musi być używana do zapytań, w których nazwy tabel lub kolumny pochodzą ze zmiennych. Również IF EXISTS (<query>)konstrukcja jest niedozwolone, jeśli queryjest dynamicznie wygenerowany.

Oto twoja funkcja z rozwiązanymi obydwoma problemami:

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;
Daniel Vérité
źródło
Dziękuję, robiłem to samo kilka minut temu, czytając twoją odpowiedź. Jedyną różnicą było to, że musiałem to usunąć, quote_ident()ponieważ dodano dodatkowe cudzysłowy, co mnie trochę zaskoczyło, bo jest używane w większości przykładów.
John Doe
Te dodatkowe cudzysłowy będą potrzebne, jeśli / kiedy nazwa tabeli zawiera znaki spoza [az] lub jeśli / kiedy koliduje z zastrzeżonym identyfikatorem (przykład: „grupa” jako nazwa tabeli)
Daniel Vérité
A tak przy okazji, czy możesz podać link, który udowodniłby, że IF EXISTS <query>konstrukcja nie istnieje? Jestem prawie pewien, że widziałem coś takiego jako działający przykład kodu.
John Doe
1
@JohnDoe: IF EXISTS (<query>) THEN ...jest doskonale poprawną konstrukcją w plpgsql. Po prostu nie z dynamicznym SQL dla <query>. Często go używam. Funkcję tę można również nieco ulepszyć. Wysłałem odpowiedź.
Erwin Brandstetter
1
Przepraszamy, masz rację if exists(<query>), jest to ważne w ogólnym przypadku. Właśnie sprawdziłem i odpowiednio zmodyfikowałem odpowiedź.
Daniel Vérité
4

Pierwsza nie "działa" w takim sensie, jak masz na myśli, działa tylko o tyle, o ile nie generuje błędu.

Spróbuj SELECT * FROM quote_ident('table_that_does_not_exist');, a zobaczysz, dlaczego funkcja zwraca 1: zaznaczenie zwraca tabelę z jedną kolumną (nazwaną quote_ident) z jednym wierszem (zmienna $1lub w tym konkretnym przypadku table_that_does_not_exist).

To, co chcesz zrobić, będzie wymagało dynamicznego SQL, czyli miejsca, w którym quote_*funkcje mają być używane.

Matt
źródło
Wielkie dzięki, Matt, table_that_does_not_existdał ten sam wynik, masz rację.
John Doe
2

Jeśli chodziło o sprawdzenie, czy tabela jest pusta, czy nie (id = 1), oto uproszczona wersja przechowywanego proc Erwina:

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;
Julien Feniou
źródło
1

Wiem, że to stary wątek, ale natknąłem się na niego ostatnio, próbując rozwiązać ten sam problem - w moim przypadku dla niektórych dość złożonych skryptów.

Przekształcenie całego skryptu w dynamiczny SQL nie jest idealnym rozwiązaniem. Jest to żmudna i podatna na błędy praca, a ponadto tracisz możliwość parametryzacji: parametry muszą być interpolowane na stałe w języku SQL, co ma zły wpływ na wydajność i bezpieczeństwo.

Oto prosta sztuczka, która pozwala zachować SQL w stanie nienaruszonym, jeśli potrzebujesz tylko dokonać wyboru z tabeli - użyj dynamicznego SQL, aby utworzyć tymczasowy widok:

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;
Nathan Meyers
źródło
0

Jeśli chcesz, aby nazwa tabeli, nazwa kolumny i wartość były dynamicznie przekazywane do funkcji parametru

użyj tego kodu

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
Sandip Debnath
źródło
-2

Mam PostgreSQL w wersji 9.4 i zawsze używam tego kodu:

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

I wtedy:

SELECT add_new_table('my_table_name');

U mnie to działa.

Uwaga! Powyższy przykład jest jednym z tych, które pokazują "Jak tego nie zrobić, jeśli chcemy zachować bezpieczeństwo podczas odpytywania bazy danych": P

dm3
źródło
1
Tworzenie newtabeli różni się od operacji z nazwą istniejącej tabeli. Tak czy inaczej, powinieneś zmienić znaczenie parametrów tekstowych wykonywanych jako kod lub jesteś otwarty na iniekcję SQL.
Erwin Brandstetter
Och, tak, mój błąd. Temat mnie zmylił i na dodatek nie przeczytałem go do końca. Zwykle w moim przypadku. : P Dlaczego kod z parametrem tekstowym jest narażony na iniekcję?
dm3
Ups, to naprawdę niebezpieczne. Dziękuję za Twoją odpowiedź!
dm3