Создайте РОЛЬ PostgreSQL (пользователя), если она не существует


123

Как написать сценарий 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 и т.п.

Ответы:


157

Упростите аналогично тому, что вы имели в виду:

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 и улучшенном с помощью комментария @Gregory .)

В отличие, например, с CREATE TABLEне существует IF NOT EXISTSусловие для CREATE ROLE(по крайней мере до 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
Это решение страдает от состояния гонки. В этом ответе задокументирован более безопасный вариант .
blubb

61

Принятый ответ страдает от состояния гонки, если два таких сценария выполняются одновременно в одном кластере 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
Barth

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 ROLEвыдает duplicate_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 баллов.
ВОГ

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 (*) into num_users FROM pg_roles WHERE rolname = 'data_rw';` Иначе это не сработает
Миро

6

Моя команда столкнулась с ситуацией с несколькими базами данных на одном сервере, в зависимости от того, к какой базе данных вы подключились, рассматриваемая РОЛЬ не была возвращена SELECT * FROM pg_catalog.pg_user, как было предложено @ erwin-brandstetter и @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, кажется, включает только роли, у которых есть LOGIN. Если роль имеет NOLOGIN, она не отображается в pg_user, по крайней мере, в PostgreSQL 10.
Грегори Арениус

2

Вы можете сделать это в своем командном файле, проанализировав вывод:

SELECT * FROM pg_user WHERE usename = 'my_user'

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


2
Столбец "имя пользователя" не существует. Это должно быть «имя пользователя».
Mouhammed Soueidane

3
"usename" не существует. :)
Гарен

1
См. Документ просмотра pg_user . В версиях 7.4–9.6 нет столбца «имя пользователя», правильным является «имя пользователя».
Шева

1

То же решение, что и для Simulate CREATE DATABASE IF NOT EXISTS для 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.