Створіть PostgreSQL ROLE (користувача), якщо він не існує


122

Як написати сценарій SQL, щоб створити РОЛЬ у PostgreSQL 9.1, але без підвищення помилки, якщо він вже існує?

Поточний сценарій просто має:

CREATE ROLE my_user LOGIN PASSWORD 'my_password';

Це не вдається, якщо користувач вже існує. Мені б хотілося чогось типу:

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

... але це не працює - IFздається, не підтримується у звичайному SQL.

У мене є пакетний файл, який створює базу даних PostgreSQL 9.1, роль та кілька інших речей. Він викликає psql.exe, передаючи ім'я сценарію SQL для запуску. Поки всі ці сценарії є звичайними SQL, і я хотів би уникати PL / pgSQL і подібного, якщо це можливо.

Відповіді:


156

Спростіть аналогічно тому, що ви мали на увазі:

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

(Спираючись на відповідь @a_horse_with_no_name та покращивши коментар @ Грегорі .)

На відміну, наприклад, CREATE TABLEвідсутнє IF NOT EXISTSзастереження CREATE ROLE(до принаймні pg 12). І ви не можете виконувати динамічні оператори DDL у звичайному SQL.

Ваш запит "уникнути PL / pgSQL" неможливий, за винятком використання іншого PL. У DOзаяві використовується plpgsql в якості процедурної мови за умовчанням. Синтаксис дозволяє опустити явне оголошення:

DO [ LANGUAGE lang_name ] code
... Ім'я процедурної мови, на якому записаний код. Якщо цей пункт пропущено, типовим є .
lang_name
plpgsql


1
@Alberto: pg_user і pg_roles є правильними. Все ще справа в нинішній версії 9.3, і вона не скоро зміниться.
Ервін Брандстеттер

2
@Ken: Якщо $у вашому клієнті є особливе значення, вам потрібно уникнути цього відповідно до синтаксичних правил вашого клієнта. Спробуйте втекти $з \$оболонки Linux. Або почніть нове запитання - коментарям не місце. Ви завжди можете зв’язатись із цим для контексту.
Ервін Брандстеттер

1
Я використовую 9.6, і якщо користувач був створений за допомогою NOLOGIN, він не відображається в таблиці pg_user, але відображається в таблиці pg_roles. Буде pg_roles кращим рішенням тут?
Джесс

2
@ErwinBrandstetter Це не працює для ролей, які мають NOLOGIN. Вони відображаються в pg_roles, але не в pg_user.
Григорій Ареній

2
Це рішення страждає від перегонів. Більш безпечний варіант задокументований у цій відповіді .
блека

60

Прийнята відповідь страждає від умови перегонів, якщо два такі сценарії виконуються одночасно на одному кластері Postgres (сервер БД), як це часто в середовищах безперервної інтеграції .

Зазвичай безпечніше намагатися створити роль і витончено вирішувати проблеми при її створенні:

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
Мені подобається такий спосіб, тому що це повідомлення, яке існує.
Матіас Бароне

2
DUPLICATE_OBJECT- це точна умова в цьому випадку, якщо ви не хочете дотримуватися майже всіх умов OTHERS.
Данек Дюваль

43

Або якщо роль не є власником жодних db-об'єктів, ви можете використовувати:

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

Але тільки якщо випадання цього користувача не принесе шкоди.


10

Альтернатива Bash (для сценаріїв 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';"

(не відповідь на запитання! це лише для тих, хто може бути корисним)


3
Він повинен читати FROM pg_roles WHERE rolnameзамістьFROM pg_user WHERE usename
Барт

8

Ось загальне рішення з використанням 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;

Використання:

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

Деякі відповіді пропонують використовувати шаблон: перевірте, чи немає ролі, а якщо ні, то видайте CREATE ROLEкоманду. У цього є один недолік: стан перегонів. Якщо хтось інший створює нову роль між перевіркою та видачею CREATE ROLEкоманди, то, CREATE ROLEочевидно, не вдається з фатальною помилкою.

Щоб вирішити вищезгадану проблему, більше інших відповідей, про які вже згадувалося PL/pgSQL, видаючи CREATE ROLEбеззастережно і потім вибираючи винятки з цього дзвінка. З цими рішеннями існує лише одна проблема. Вони мовчки скидають будь-які помилки, в тому числі й ті, які не породжені фактом, що роль вже існує. CREATE ROLEможе кидати й інші помилки, і моделювання IF NOT EXISTSповинно замовчувати помилку лише тоді, коли роль вже існує.

CREATE ROLEduplicate_objectпомилка кидка, коли роль вже існує. І обробник винятків повинен виявити лише цю одну помилку. Як згадували інші відповіді, це гарна ідея перетворити фатальну помилку в просте повідомлення. Інші IF NOT EXISTSкоманди PostgreSQL додають , skippingу своє повідомлення, тому для послідовності додаю його і тут.

Ось повний код SQL для моделювання CREATE ROLE IF NOT EXISTSз правильним винятком та розповсюдження sqlstate:

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

Тестовий вихід (викликається два рази через DO, а потім безпосередньо):

$ 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
Дякую. Ніяких перегонових умов, чіткого вилучення виключень, загортання власного повідомлення Postgres замість того, щоб переписати власне.
Стефано Тащіні

1
Справді! На даний момент тут єдина правильна відповідь, яка не страждає від перегонових умов і використовує необхідні вибіркові поводження з помилками. Дуже шкода, що ця відповідь з’явилася після того, як (не повністю коректна) відповідь набрала більше 100 балів.
vog

1
Будь ласка! Моє рішення також поширює SQLSTATE, тож якщо ви викликаєте заяву з іншого сценарію PL / SQL або іншої мови з роз'ємом SQL, ви отримаєте правильний SQLSTATE.
Палій

6

Оскільки ви перебуваєте на 9.x, ви можете перетворити це в оператор 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$
;

Виберіть слід `SELECT count (*) у num_users ВІД pg_roles WHERE rolname = 'data_rw';` В іншому випадку це не буде працювати
Miro

6

Моя команда потрапила в ситуацію з декількома базами даних на одному сервері, залежно від того, до якої бази даних ви підключились, відповідна ROLE не була повернена SELECT * FROM pg_catalog.pg_user, як запропонували @ erwin-marketetter та @a_horse_with_no_name. Умовний блок виконали, і ми вдарили role "my_user" already exists.

На жаль, ми не впевнені в точних умовах, але це рішення вирішує проблему:

        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$

Можливо, це може бути конкретніше, щоб виключити інші винятки.


3
Таблиця pg_user, здається, містить лише ролі, які мають ВХОД. Якщо в ролі є NOLOGIN, вона не відображається в pg_user, принаймні, в PostgreSQL 10.
Грегорі Ареній

2

Ви можете зробити це у своєму пакетному файлі, розібравши вихід:

SELECT * FROM pg_user WHERE usename = 'my_user'

а потім psql.exeще раз запустити, якщо ролі не існує.


2
Стовпець "ім'я користувача" не існує. Це має бути "usename".
Мухаммед Суейдан

3
"usename" - це те, що не існує. :)
Гарен

1
Будь ласка, зверніться до документа перегляду pg_user . У версіях 7.4-9.6 не існує стовпця "ім'я користувача", "ім'я використання" є правильним.
Шева

1

Те саме рішення, що і для імітації СТВОРИТИ ДАТАБАЗУ, ЯКЩО НЕ МАЄТЬСЯ для PostgreSQL? повинен працювати - надіслати CREATE USER …на \gexec.

Обхід зсередини psql

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

Обхід із оболонки

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

Дивіться прийняту відповідь там для отримання більш детальної інформації.


У вашому рішенні все ще є умова гонки, про яку я описав у своїй відповіді stackoverflow.com/a/55954480/7878845 Якщо ви запускаєте сценарій оболонки паралельно більше разів, ви отримуєте ПОМИЛКУ: роль "my_user" вже існує
Палій
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.