Buat ROLE PostgreSQL (pengguna) jika tidak ada

123

Bagaimana cara menulis skrip SQL untuk membuat ROLE di PostgreSQL 9.1, tetapi tanpa menimbulkan kesalahan jika sudah ada?

Skrip saat ini hanya memiliki:

CREATE ROLE my_user LOGIN PASSWORD 'my_password';

Ini gagal jika pengguna sudah ada. Saya ingin sesuatu seperti:

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

... tapi itu tidak berhasil - IFsepertinya tidak didukung dalam SQL biasa.

Saya memiliki file batch yang membuat database PostgreSQL 9.1, peran, dan beberapa hal lainnya. Ini memanggil psql.exe, meneruskan nama skrip SQL untuk dijalankan. Sejauh ini semua skrip ini adalah SQL biasa dan saya ingin menghindari PL / pgSQL dan semacamnya, jika memungkinkan.

EMP
sumber

Jawaban:

157

Sederhanakan dengan cara yang mirip dengan yang Anda pikirkan:

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

(Membangun jawaban @a_horse_with_no_name dan ditingkatkan dengan komentar @ Gregory .)

Tidak seperti, misalnya, dengan CREATE TABLEtidak ada IF NOT EXISTSklausul untuk CREATE ROLE(hingga setidaknya pg 12). Dan Anda tidak dapat menjalankan pernyataan DDL dinamis dalam SQL biasa.

Permintaan Anda untuk "menghindari PL / pgSQL" tidak mungkin kecuali dengan menggunakan PL lain. The DOpernyataan menggunakan plpgsql sebagai default bahasa prosedural. Sintaks memungkinkan untuk menghilangkan deklarasi eksplisit:

DO [ LANGUAGE lang_name ] code
... Nama bahasa prosedural tempat kode ditulis. Jika dihilangkan, defaultnya adalah .
lang_name
plpgsql

Erwin Brandstetter
sumber
1
@Alberto: pg_user dan pg_roles keduanya benar. Masih terjadi di versi 9.3 saat ini dan itu tidak akan berubah dalam waktu dekat.
Erwin Brandstetter
2
@ Ken: Jika $memiliki arti khusus pada klien Anda, Anda perlu menghindarinya sesuai dengan aturan sintaks klien Anda. Cobalah kabur $dengan \$di shell Linux. Atau mulai pertanyaan baru - komentar bukanlah tempatnya. Anda selalu dapat menautkan yang ini untuk konteks.
Erwin Brandstetter
1
Saya menggunakan 9.6, dan jika pengguna dibuat dengan NOLOGIN, mereka tidak muncul di tabel pg_user, tetapi muncul di tabel pg_roles. Akankah pg_roles menjadi solusi yang lebih baik di sini?
Jess
2
@ErwinBrandstetter Ini tidak berfungsi untuk peran yang memiliki NOLOGIN. Mereka muncul di pg_roles tetapi tidak di pg_user.
Gregory Arenius
2
Solusi ini menderita kondisi-ras. Varian yang lebih aman didokumentasikan dalam jawaban ini .
blubb
61

Jawaban yang diterima menderita kondisi balapan jika dua skrip seperti itu dijalankan secara bersamaan di cluster Postgres yang sama (server DB), seperti yang umum di lingkungan integrasi berkelanjutan .

Biasanya lebih aman untuk mencoba membuat peran dan menangani masalah dengan baik saat membuatnya:

DO $$
BEGIN
  CREATE ROLE my_role WITH NOLOGIN;
  EXCEPTION WHEN DUPLICATE_OBJECT THEN
  RAISE NOTICE 'not creating role my_role -- it already exists';
END
$$;
blubb
sumber
2
Saya suka cara ini karena ada pemberitahuan.
Matias Barone
2
DUPLICATE_OBJECTadalah kondisi yang tepat dalam kasus ini, jika Anda tidak ingin menangkap hampir semua kondisi dengan OTHERS.
Danek Duvall
43

Atau jika role tersebut bukan pemilik objek db apa pun yang dapat digunakan:

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

Tetapi hanya jika menjatuhkan pengguna ini tidak akan merugikan.

Borys
sumber
10

Alternatif Bash (untuk skrip 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';"

(bukan jawaban untuk pertanyaannya! ini hanya untuk mereka yang mungkin berguna)

Eduardo Cuomo
sumber
3
Seharusnya dibaca FROM pg_roles WHERE rolnamesebagai penggantiFROM pg_user WHERE usename
Barth
8

Berikut adalah solusi umum menggunakan 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;

Pemakaian:

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)
Wolkenarchitekt
sumber
8

Beberapa jawaban menyarankan untuk menggunakan pola: periksa apakah peran tidak ada dan jika tidak maka keluarkan CREATE ROLEperintah. Ini memiliki satu kelemahan: kondisi balapan. Jika orang lain membuat peran baru antara check dan mengeluarkan CREATE ROLEperintah, makaCREATE ROLE jelas gagal dengan kesalahan fatal.

Untuk mengatasi masalah di atas, lebih banyak jawaban lain yang telah disebutkan penggunaan PL/pgSQL, mengeluarkan CREATE ROLEtanpa syarat dan kemudian menangkap pengecualian dari panggilan itu. Hanya ada satu masalah dengan solusi ini. Mereka diam-diam menjatuhkan semua kesalahan, termasuk yang tidak dihasilkan oleh fakta bahwa peran sudah ada. CREATE ROLEdapat membuang juga kesalahan lain dan simulasi IF NOT EXISTSharus membungkam kesalahan hanya jika peran sudah ada.

CREATE ROLEmelempar duplicate_objectkesalahan saat peran sudah ada. Dan penangan pengecualian harus menangkap hanya satu kesalahan ini. Seperti jawaban lain yang disebutkan, sebaiknya ubah kesalahan fatal menjadi pemberitahuan sederhana. IF NOT EXISTSPerintah PostgreSQL lainnya menambahkan, skipping ke dalam pesan mereka, jadi untuk konsistensi saya juga menambahkannya di sini.

Berikut adalah kode SQL lengkap untuk simulasi CREATE ROLE IF NOT EXISTSdengan pengecualian yang benar dan propagasi sqlstate:

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

Output tes (dipanggil dua kali melalui DO dan kemudian secara langsung):

$ 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
Pali
sumber
2
Terima kasih. Tidak ada kondisi balapan, pengecualian ketat, membungkus pesan Postgres sendiri alih-alih menulis ulang pesan Anda sendiri.
Stefano Taschini
1
Memang! Saat ini ini adalah satu-satunya jawaban yang benar di sini, yang tidak terpengaruh oleh kondisi balapan, dan menggunakan penanganan kesalahan selektif yang diperlukan. Sangat disayangkan bahwa jawaban ini muncul setelah jawaban teratas (tidak sepenuhnya benar) mengumpulkan lebih dari 100 poin.
vog
1
Sama sama! Solusi saya juga menyebarkan SQLSTATE jadi jika Anda memanggil pernyataan dari skrip PL / SQL lain atau bahasa lain dengan konektor SQL Anda akan menerima SQLSTATE yang benar.
Pali
6

Karena Anda berada di 9.x, Anda dapat menggabungkannya menjadi pernyataan 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$
;
seekor kuda tanpa nama
sumber
Pilih harus menjadi `SELECT count (*) menjadi num_users FROM pg_roles WHERE rolname = 'data_rw';` Jika tidak, itu tidak akan berfungsi
Miro
6

Tim saya mengalami situasi dengan beberapa database di satu server, tergantung pada database mana Anda terhubung, PERAN yang dimaksud tidak dikembalikan oleh SELECT * FROM pg_catalog.pg_user, seperti yang diusulkan oleh @ erwin-brandstetter dan @a_horse_with_no_name. Blok bersyarat dieksekusi, dan kami menekanrole "my_user" already exists .

Sayangnya kami tidak yakin dengan kondisi pastinya, tetapi solusi ini mengatasi masalah tersebut:

        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$

Mungkin bisa dibuat lebih spesifik untuk menyingkirkan pengecualian lain.

Chris Betti
sumber
3
Tabel pg_user tampaknya hanya menyertakan peran yang memiliki LOGIN. Jika peran memiliki NOLOGIN, peran itu tidak muncul di pg_user, setidaknya di PostgreSQL 10.
Gregory Arenius
2

Anda dapat melakukannya di file batch Anda dengan mengurai output dari:

SELECT * FROM pg_user WHERE usename = 'my_user'

lalu menjalankan psql.exesekali lagi jika peran tersebut tidak ada.

Sheva
sumber
2
Kolom "username" tidak ada. Ini harus menjadi "nama pengguna".
Mouhammed Soueidane
3
"usename" adalah salah satu yang tidak ada. :)
Garen
1
Silakan merujuk ke pg_user view doc. Tidak ada kolom "username" di versi 7.4-9.6, "usename" adalah yang benar.
Sheva
1

Solusi yang sama seperti untuk Simulasikan BUAT DATABASE JIKA TIDAK ADA untuk PostgreSQL? harus bekerja - kirim CREATE USER …ke \gexec.

Solusi dari dalam psql

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

Solusi dari shell

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

Lihat jawaban yang diterima di sana untuk lebih jelasnya.

Alexander Skwar
sumber
Solusi Anda masih memiliki kondisi balapan yang saya jelaskan dalam jawaban saya stackoverflow.com/a/55954480/7878845 Jika Anda menjalankan skrip shell Anda secara paralel lebih sering Anda mendapatkan ERROR: peran "my_user" sudah ada
Pali