Nome della tabella come parametro della funzione PostgreSQL


87

Voglio passare il nome di una tabella come parametro in una funzione Postgres. Ho provato questo codice:

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

E ho capito:

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

Ed ecco l'errore che ho ricevuto quando sono cambiato in questo 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...

Probabilmente, quote_ident($1)funziona, perché senza la where quote_ident($1).id=1parte che ottengo 1, il che significa che qualcosa è selezionato. Perché il primo può quote_ident($1)funzionare e il secondo non allo stesso tempo? E come potrebbe essere risolto?


So che questa domanda è un po 'vecchia, ma l'ho trovata mentre cercavo la risposta a un altro problema. La tua funzione non potrebbe semplicemente interrogare lo schema_informazionale? Voglio dire, questo è un po 'quello che serve in un certo senso - per farti interrogare e vedere quali oggetti esistono nel database. Solo un'idea.
David S,

@DavidS Grazie per un commento, ci proverò.
John Doe,

Risposte:


126

Questo può essere ulteriormente semplificato e migliorato:

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

Chiama con nome qualificato dallo schema (vedi sotto):

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

O:

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

Punti importanti

  • Usa un OUTparametro per semplificare la funzione. Puoi selezionare direttamente il risultato dell'SQL dinamico in esso ed essere fatto. Non sono necessarie variabili e codice aggiuntivi.

  • EXISTSfa esattamente quello che vuoi. Ottieni truese la riga esiste o in falsealtro modo. Ci sono vari modi per farlo, in EXISTSgenere è più efficiente.

  • Sembra che tu voglia indietro un intero , quindi lancio il booleanrisultato da EXISTSa integer, che restituisce esattamente quello che avevi. Ritornerei invece booleano .

  • Uso il tipo di identificatore oggetto regclasscome tipo di input per _tbl. Fa tutto quote_ident(_tbl)o format('%I', _tbl)lo farebbe, ma meglio, perché:

  • .. impedisce altrettanto bene l' iniezione SQL .

  • .. fallisce immediatamente e con maggiore grazia se il nome della tabella non è valido / non esiste / è invisibile all'utente corrente. (Un regclassparametro è applicabile solo per le tabelle esistenti .)

  • .. funziona con nomi di tabella qualificati dallo schema, dove un normale quote_ident(_tbl)o format(%I)fallirebbe perché non possono risolvere l'ambiguità. Dovresti passare ed uscire da schemi e nomi di tabella separatamente.

  • Uso ancora format(), perché semplifica la sintassi (e per dimostrare come viene utilizzata), ma con %sinvece di %I. In genere, le query sono più complesse, quindi format()aiutano di più. Per il semplice esempio potremmo anche concatenare:

      EXECUTE 'SELECT (EXISTS (SELECT FROM ' || _tbl || ' WHERE id = 1))::int'
    
  • Non è necessario qualificare la idcolonna in base a una tabella mentre FROMnell'elenco è presente una sola tabella . Nessuna ambiguità possibile in questo esempio. I comandi SQL (dinamici) all'interno EXECUTEhanno un ambito separato , le variabili di funzione oi parametri non sono visibili lì, a differenza dei semplici comandi SQL nel corpo della funzione.

Ecco perché esci sempre correttamente dall'input dell'utente per SQL dinamico:

db <> fiddle qui dimostrando SQL injection
Vecchio sqlfiddle


2
@suhprano: certo. Provalo:DO $$BEGIN EXECUTE 'ANALYZE mytbl'; END$$;
Erwin Brandstetter

perché% se non% L?
Lotus

3
@Lotus: la spiegazione è nella risposta. regclassi valori vengono sottoposti a escape automaticamente quando vengono visualizzati come testo. %Lsarebbe sbagliato in questo caso.
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; creare una funzione di conteggio delle righe della tabella,select table_rows('nf_part1');
Ferris

come possiamo ottenere tutte le colonne?
Ashish

13

Se possibile, non farlo.

Questa è la risposta: è un anti-pattern. Se il client conosce la tabella da cui desidera i dati, allora SELECT FROM ThatTable. Se un database è progettato in modo tale da renderlo necessario, sembra essere progettato in modo non ottimale. Se un livello di accesso ai dati ha bisogno di sapere se esiste un valore in una tabella, è facile comporre SQL in quel codice e inserire questo codice nel database non va bene.

A me sembra come installare un dispositivo all'interno di un ascensore dove si può digitare il numero del piano desiderato. Dopo aver premuto il pulsante Vai, sposta una lancetta meccanica sul pulsante corretto per il piano desiderato e lo preme. Questo introduce molti potenziali problemi.

Nota: non c'è intenzione di deridere, qui. Il mio sciocco esempio di ascensore è stato * il miglior dispositivo che potessi immaginare * per evidenziare sinteticamente i problemi con questa tecnica. Aggiunge un livello inutile di indirezione, spostando la scelta del nome della tabella da uno spazio del chiamante (utilizzando un DSL, SQL robusto e ben compreso) in un ibrido che utilizza codice SQL lato server oscuro / bizzarro.

Tale suddivisione delle responsabilità attraverso lo spostamento della logica di costruzione delle query in SQL dinamico rende il codice più difficile da comprendere. Viola una convenzione standard e affidabile (come una query SQL sceglie cosa selezionare) in nome del codice personalizzato irto di potenziali errori.

Ecco i punti dettagliati su alcuni dei potenziali problemi con questo approccio:

  • Dynamic SQL offre la possibilità di SQL injection che è difficile da riconoscere nel codice front-end o nel solo codice back-end (è necessario ispezionarli insieme per vederlo).

  • Le procedure e le funzioni archiviate possono accedere alle risorse per le quali il proprietario dell'SP / funzione ha diritti, ma il chiamante no. Per quanto ho capito, senza particolari cure, quindi per impostazione predefinita quando si utilizza codice che produce SQL dinamico e lo esegue, il database esegue l'SQL dinamico sotto i diritti del chiamante. Ciò significa che non sarai in grado di utilizzare affatto gli oggetti privilegiati, oppure dovrai aprirli a tutti i client, aumentando la superficie di potenziale attacco ai dati privilegiati. L'impostazione della funzione SP / al momento della creazione in modo che venga sempre eseguita come un determinato utente (in SQL Server EXECUTE AS) può risolvere il problema, ma rende le cose più complicate. Ciò aggrava il rischio di SQL injection menzionato nel punto precedente, rendendo l'SQL dinamico un vettore di attacco molto allettante.

  • Quando uno sviluppatore deve capire cosa sta facendo il codice dell'applicazione per modificarlo o correggere un bug, troverà molto difficile ottenere l'esatta query SQL in esecuzione. È possibile utilizzare il profiler SQL, ma ciò richiede privilegi speciali e può avere effetti negativi sulle prestazioni sui sistemi di produzione. La query eseguita può essere registrata dall'SP, ma ciò aumenta la complessità per un beneficio discutibile (che richiede di accogliere nuove tabelle, eliminare i vecchi dati, ecc.) Ed è abbastanza non ovvio. In effetti, alcune applicazioni sono progettate in modo tale che lo sviluppatore non dispone delle credenziali del database, quindi diventa quasi impossibile per lui vedere effettivamente la query inviata.

  • Quando si verifica un errore, ad esempio quando si tenta di selezionare una tabella che non esiste, verrà visualizzato un messaggio del tipo "nome oggetto non valido" dal database. Ciò accadrà esattamente allo stesso modo sia che tu stia componendo l'SQL nel back-end o nel database, ma la differenza è che un povero sviluppatore che sta cercando di risolvere i problemi del sistema deve scrutare un livello più in profondità in un'altra caverna sotto quella in cui il esiste il problema, scavare nella procedura miracolosa che fa tutto per cercare di capire qual è il problema. I log non mostreranno "Errore in GetWidget", mostreranno "Errore in OneProcedureToRuleThemAllRunner". Questa astrazione generalmente peggiorerà un sistema .

Un esempio in pseudo-C # di cambio di nomi di tabelle in base a un parametro:

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

Anche se questo non elimina ogni possibile problema immaginabile, i difetti che ho delineato con l'altra tecnica sono assenti da questo esempio.


4
Non sono completamente d'accordo con questo. Diciamo, premi questo pulsante "Vai" e poi qualche meccanismo controlla, se il pavimento esiste. Le funzioni possono essere utilizzate nei trigger, che a loro volta possono verificare alcune condizioni. Questa decisione potrebbe non essere la più bella, ma se il sistema è già abbastanza grande e devi apportare alcune correzioni alla sua logica, beh, questa scelta non è così drammatica, suppongo.
John Doe

2
Ma considera che l'azione di provare a premere un pulsante che non esiste genererà semplicemente un'eccezione, indipendentemente da come la gestisci. Non puoi effettivamente premere un pulsante inesistente, quindi non c'è alcun vantaggio nell'aggiungere, oltre alla pressione del pulsante, un livello per verificare la presenza di numeri inesistenti, poiché tale immissione del numero non esisteva prima di creare detto livello! L'astrazione è secondo me lo strumento più potente nella programmazione. Tuttavia, l'aggiunta di un livello che duplica semplicemente male un'astrazione esistente è sbagliata . Il database stesso è già un livello di astrazione che mappa i nomi su set di dati.
ErikE

3
Azzeccato. Il punto centrale di SQL è esprimere il set di dati che si desidera estrarre. L'unica cosa che fa questa funzione è incapsulare un'istruzione SQL "preconfezionata". Dato che l'identificatore è anche hardcoded, l'intera cosa ha un cattivo odore.
Nick Hristov

2
@three Finché qualcuno non è nella fase di padronanza (vedi il modello di acquisizione di abilità Dreyfus ) di un'abilità, dovrebbe semplicemente obbedire assolutamente a regole come "NON passare i nomi delle tabelle in una procedura da utilizzare in SQL dinamico". Anche accennare che non è sempre un male è di per sé un cattivo consiglio . Sapendo questo, il principiante sarà tentato di usarlo! Questo è male. Solo i padroni di un argomento dovrebbero infrangere le regole, poiché sono gli unici con l'esperienza per sapere in ogni caso particolare se tale violazione delle regole ha davvero senso.
ErikE

2
@ tre tazze ho aggiornato con molti più dettagli sul motivo per cui è una cattiva idea.
ErikE

10

All'interno del codice plpgsql, l' istruzione EXECUTE deve essere utilizzata per le query in cui i nomi di tabella o le colonne provengono da variabili. Inoltre il IF EXISTS (<query>)costrutto non è consentito quando queryè generato dinamicamente.

Ecco la tua funzione con entrambi i problemi risolti:

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;

Grazie, stavo facendo lo stesso un paio di minuti fa quando ho letto la tua risposta. L'unica differenza è che ho dovuto rimuovere quote_ident()perché aggiungeva virgolette extra, il che mi ha sorpreso un po ', beh, perché è usato nella maggior parte degli esempi.
John Doe

Quelle virgolette extra saranno necessarie se / quando il nome della tabella contiene caratteri al di fuori di [az], o se / quando si scontra con un identificatore riservato (esempio: "group" come nome di tabella)
Daniel Vérité

E, a proposito, potresti fornire un collegamento che provi che il IF EXISTS <query>costrutto non esiste? Sono abbastanza sicuro di aver visto qualcosa di simile come un esempio di codice funzionante.
John Doe

1
@ JohnDoe: IF EXISTS (<query>) THEN ...è un costrutto perfettamente valido in plpgsql. Solo non con SQL dinamico per <query>. Lo uso molto. Inoltre, questa funzione può essere migliorata un po '. Ho pubblicato una risposta.
Erwin Brandstetter

1
Scusa, hai ragione if exists(<query>), è valido nel caso generale. Ho appena controllato e modificato la risposta di conseguenza.
Daniel Vérité

4

Il primo in realtà non "funziona" nel senso che intendi, funziona solo nella misura in cui non genera un errore.

Prova SELECT * FROM quote_ident('table_that_does_not_exist');e vedrai perché la tua funzione restituisce 1: la selezione restituisce una tabella con una colonna (denominata quote_ident) con una riga (la variabile $1o in questo caso particolare table_that_does_not_exist).

Quello che vuoi fare richiederà SQL dinamico, che in realtà è il luogo in cui le quote_*funzioni devono essere utilizzate.


Grazie mille, Matt, ha table_that_does_not_existdato lo stesso risultato, hai ragione.
John Doe,

2

Se la domanda era verificare se la tabella è vuota o meno (id = 1), ecco una versione semplificata della procedura memorizzata di 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

So che questo è un vecchio thread, ma mi sono imbattuto di recente durante il tentativo di risolvere lo stesso problema, nel mio caso per alcuni script abbastanza complessi.

Trasformare l'intero script in SQL dinamico non è l'ideale. È un lavoro noioso e soggetto a errori e si perde la capacità di parametrizzare: i parametri devono essere interpolati in costanti nell'SQL, con conseguenze negative per le prestazioni e la sicurezza.

Ecco un semplice trucco che ti consente di mantenere intatto l'SQL se devi solo selezionare dalla tua tabella: usa l'SQL dinamico per creare una vista temporanea:

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

Se si desidera che il nome della tabella, il nome della colonna e il valore vengano passati dinamicamente alla funzione come parametro

usa questo codice

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

Ho la versione 9.4 di PostgreSQL e utilizzo sempre questo codice:

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

E poi:

SELECT add_new_table('my_table_name');

Funziona bene per me.

Attenzione! L'esempio sopra è uno di quelli che mostra "Come non farlo se vogliamo mantenere la sicurezza durante l'interrogazione del database": P


1
Creare una newtabella è diverso dall'operare con il nome di una tabella esistente. In ogni caso, dovresti sfuggire ai parametri di testo eseguiti come codice o sei aperto a SQL injection.
Erwin Brandstetter

Oh, sì, errore mio. L'argomento mi ha fuorviato e inoltre non l'ho letto fino alla fine. Normalmente nel mio caso. : P Perché il codice con un parametro di testo è esposto all'iniezione?
dm3

Ops, è davvero pericoloso. Grazie per la risposta!
dm3
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.