Crea PostgreSQL ROLE (utente) se non esiste


123

Come scrivo uno script SQL per creare un RUOLO in PostgreSQL 9.1, ma senza generare un errore se esiste già?

Lo script corrente ha semplicemente:

CREATE ROLE my_user LOGIN PASSWORD 'my_password';

Questo fallisce se l'utente esiste già. Vorrei qualcosa del tipo:

IF NOT EXISTS (SELECT * FROM pg_user WHERE username = 'my_user')
BEGIN
    CREATE ROLE my_user LOGIN PASSWORD 'my_password';
END;

... ma non funziona - IFnon sembra essere supportato in SQL semplice.

Ho un file batch che crea un database PostgreSQL 9.1, un ruolo e poche altre cose. Chiama psql.exe, passando il nome di uno script SQL da eseguire. Finora tutti questi script sono semplici SQL e vorrei evitare PL / pgSQL e simili, se possibile.

Risposte:


157

Semplifica in modo simile a quello che avevi in ​​mente:

DO
$do$
BEGIN
   IF NOT EXISTS (
      SELECT FROM pg_catalog.pg_roles  -- SELECT list can be empty for this
      WHERE  rolname = 'my_user') THEN

      CREATE ROLE my_user LOGIN PASSWORD 'my_password';
   END IF;
END
$do$;

(Basandosi sulla risposta di @a_horse_with_no_name e migliorato con il commento di @ Gregory .)

A differenza, ad esempio, con CREATE TABLEnon esiste una IF NOT EXISTSclausola per CREATE ROLE(fino ad almeno pg 12). E non è possibile eseguire istruzioni DDL dinamiche in semplice SQL.

La tua richiesta di "evitare PL / pgSQL" è impossibile se non utilizzando un altro PL. L' DOistruzione utilizza plpgsql come linguaggio procedurale predefinito. La sintassi consente di omettere la dichiarazione esplicita:

DO [ LANGUAGE lang_name ] code
... Il nome della lingua procedurale in cui è scritto il codice. Se omesso, il valore predefinito è .
lang_name
plpgsql


1
@Alberto: pg_user e pg_roles sono entrambi corretti. È ancora il caso dell'attuale versione 9.3 e non cambierà presto.
Erwin Brandstetter

2
@Ken: Se $ha un significato speciale nel tuo client, devi evitarlo in base alle regole di sintassi del tuo client. Prova a eseguire l'escape $con \$nella shell di Linux. Oppure inizia una nuova domanda: i commenti non sono il posto giusto. Puoi sempre collegarti a questo per il contesto.
Erwin Brandstetter

1
Sto usando 9.6 e se un utente è stato creato con NOLOGIN, non viene visualizzato nella tabella pg_user, ma nella tabella pg_roles. Pg_roles sarebbe una soluzione migliore qui?
Jess,

2
@ErwinBrandstetter Questo non funziona per i ruoli che hanno NOLOGIN. Vengono visualizzati in pg_roles ma non in pg_user.
Gregory Arenius

2
Questa soluzione soffre di una condizione di gara. Una variante più sicura è documentata in questa risposta .
blubb

60

La risposta accettata soffre di una condizione di competizione se due di tali script vengono eseguiti contemporaneamente sullo stesso cluster Postgres (server DB), come è comune negli ambienti di integrazione continua .

In genere è più sicuro provare a creare il ruolo e affrontare con garbo i problemi durante la creazione:

DO $$
BEGIN
  CREATE ROLE my_role WITH NOLOGIN;
  EXCEPTION WHEN DUPLICATE_OBJECT THEN
  RAISE NOTICE 'not creating role my_role -- it already exists';
END
$$;

2
Mi piace in questo modo perché notifica che esiste.
Matias Barone

2
DUPLICATE_OBJECTè la condizione precisa in questo caso, se non vuoi catturare quasi tutte le condizioni con OTHERS.
Danek Duvall

43

O se il ruolo non è il proprietario di alcun oggetto db, è possibile utilizzare:

DROP ROLE IF EXISTS my_user;
CREATE ROLE my_user LOGIN PASSWORD 'my_password';

Ma solo se l'eliminazione di questo utente non causerà alcun danno.


10

Alternativa Bash (per script Bash ):

psql -h localhost -U postgres -tc \
"SELECT 1 FROM pg_user WHERE usename = 'my_user'" \
| grep -q 1 \
|| psql -h localhost -U postgres \
-c "CREATE ROLE my_user LOGIN PASSWORD 'my_password';"

(non è la risposta per la domanda! è solo per coloro che possono essere utili)


3
Dovrebbe leggere FROM pg_roles WHERE rolnameinvece diFROM pg_user WHERE usename
Barth

8

Ecco una soluzione generica che utilizza plpgsql:

CREATE OR REPLACE FUNCTION create_role_if_not_exists(rolename NAME) RETURNS TEXT AS
$$
BEGIN
    IF NOT EXISTS (SELECT * FROM pg_roles WHERE rolname = rolename) THEN
        EXECUTE format('CREATE ROLE %I', rolename);
        RETURN 'CREATE ROLE';
    ELSE
        RETURN format('ROLE ''%I'' ALREADY EXISTS', rolename);
    END IF;
END;
$$
LANGUAGE plpgsql;

Uso:

posgres=# SELECT create_role_if_not_exists('ri');
 create_role_if_not_exists 
---------------------------
 CREATE ROLE
(1 row)
posgres=# SELECT create_role_if_not_exists('ri');
 create_role_if_not_exists 
---------------------------
 ROLE 'ri' ALREADY EXISTS
(1 row)

8

Alcune risposte suggeriscono di utilizzare il pattern: controlla se il ruolo non esiste e in caso contrario emetti il CREATE ROLEcomando. Questo ha uno svantaggio: le condizioni di gara. Se qualcun altro crea un nuovo ruolo tra il controllo e l'emissione del CREATE ROLEcomando, CREATE ROLEovviamente fallisce con un errore fatale.

Per risolvere il problema di cui sopra, più altre risposte già menzionate nell'uso PL/pgSQL, emettendo CREATE ROLEincondizionatamente e quindi intercettando le eccezioni da quella chiamata. C'è solo un problema con queste soluzioni. Eliminano silenziosamente qualsiasi errore, inclusi quelli che non sono generati dal fatto che il ruolo esiste già. CREATE ROLEpuò generare anche altri errori e la simulazione IF NOT EXISTSdovrebbe tacitare solo l'errore quando il ruolo esiste già.

CREATE ROLEgenera un duplicate_objecterrore quando il ruolo esiste già. E il gestore delle eccezioni dovrebbe rilevare solo questo errore. Come menzionato in altre risposte, è una buona idea convertire l'errore fatale in semplice avviso. Altri IF NOT EXISTScomandi PostgreSQL si aggiungono , skippingal loro messaggio, quindi per coerenza lo aggiungo anche qui.

Ecco il codice SQL completo per la simulazione CREATE ROLE IF NOT EXISTScon l'eccezione corretta e la propagazione sqlstate:

DO $$
BEGIN
CREATE ROLE test;
EXCEPTION WHEN duplicate_object THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
END
$$;

Uscita di prova (chiamata due volte tramite DO e poi direttamente):

$ sudo -u postgres psql
psql (9.6.12)
Type "help" for help.

postgres=# \set ON_ERROR_STOP on
postgres=# \set VERBOSITY verbose
postgres=# 
postgres=# DO $$
postgres$# BEGIN
postgres$# CREATE ROLE test;
postgres$# EXCEPTION WHEN duplicate_object THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
postgres$# END
postgres$# $$;
DO
postgres=# 
postgres=# DO $$
postgres$# BEGIN
postgres$# CREATE ROLE test;
postgres$# EXCEPTION WHEN duplicate_object THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
postgres$# END
postgres$# $$;
NOTICE:  42710: role "test" already exists, skipping
LOCATION:  exec_stmt_raise, pl_exec.c:3165
DO
postgres=# 
postgres=# CREATE ROLE test;
ERROR:  42710: role "test" already exists
LOCATION:  CreateRole, user.c:337

2
Grazie. Nessuna condizione di gara, cattiva eccezione, avvolgere il messaggio di Postgres invece di riscrivere il tuo.
Stefano Taschini

1
Infatti! Questa è attualmente l'unica risposta corretta qui, che non soffre di condizioni di gara e utilizza la necessaria gestione selettiva degli errori. È un vero peccato che questa risposta sia apparsa dopo che la risposta principale (non completamente corretta) ha raccolto più di 100 punti.
vog

1
Prego! La mia soluzione propaga anche SQLSTATE, quindi se stai chiamando l'istruzione da un altro script PL / SQL o da un altro linguaggio con connettore SQL, riceverai SQLSTATE corretto.
Pali

6

Dato che sei su 9.x, puoi racchiuderlo in un'istruzione DO:

do 
$body$
declare 
  num_users integer;
begin
   SELECT count(*) 
     into num_users
   FROM pg_user
   WHERE usename = 'my_user';

   IF num_users = 0 THEN
      CREATE ROLE my_user LOGIN PASSWORD 'my_password';
   END IF;
end
$body$
;

Selezionare dovrebbe essere `SELECT count (*) into num_users FROM pg_roles WHERE rolname = 'data_rw';` Altrimenti non funzionerà
Miro

6

Il mio team stava riscontrando una situazione con più database su un server, a seconda del database a cui ti connetti, il RUOLO in questione non è stato restituito da SELECT * FROM pg_catalog.pg_user, come proposto da @ erwin-brandstetter e @a_horse_with_no_name. Il blocco condizionale viene eseguito e noi premiamo role "my_user" already exists.

Sfortunatamente non siamo sicuri delle condizioni esatte, ma questa soluzione aggira il problema:

        DO  
        $body$
        BEGIN
            CREATE ROLE my_user LOGIN PASSWORD 'my_password';
        EXCEPTION WHEN others THEN
            RAISE NOTICE 'my_user role exists, not re-creating';
        END
        $body$

Probabilmente potrebbe essere reso più specifico per escludere altre eccezioni.


3
La tabella pg_user sembra includere solo i ruoli che hanno LOGIN. Se un ruolo ha NOLOGIN non viene visualizzato in pg_user, almeno in PostgreSQL 10.
Gregory Arenius

2

Puoi farlo nel tuo file batch analizzando l'output di:

SELECT * FROM pg_user WHERE usename = 'my_user'

e poi di psql.exenuovo in esecuzione se il ruolo non esiste.


2
La colonna "nome utente" non esiste. Dovrebbe essere "nome utente".
Mouhammed Soueidane

3
"nome utente" è quello che non esiste. :)
Garen

1
Fare riferimento a pg_user view doc. Non esiste una colonna "nome utente" nelle versioni 7.4-9.6, "nome utente" è quella corretta.
Sheva

1

La stessa soluzione di Simulate CREATE DATABASE IF NOT EXISTS per PostgreSQL? dovrebbe funzionare - invia un CREATE USER …a \gexec.

Soluzione alternativa dall'interno di psql

SELECT 'CREATE USER my_user'
WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'my_user')\gexec

Soluzione alternativa dalla shell

echo "SELECT 'CREATE USER my_user' WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'my_user')\gexec" | psql

Vedi la risposta accettata lì per maggiori dettagli.


La tua soluzione ha ancora una condizione di gara che ho descritto nella mia risposta stackoverflow.com/a/55954480/7878845 Se esegui il tuo script di shell in parallelo più volte ottieni ERRORE: il ruolo "mio_utente" esiste già
Pali
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.