如果不存在,请创建PostgreSQL ROLE(用户)

EMP*_*EMP 96 sql postgresql roles dynamic-sql

如何编写一个SQL脚本来在PostgreSQL 9.1中创建一个ROLE,但如果它已经存在则不会引发错误?

当前脚本只有:

CREATE ROLE my_user LOGIN PASSWORD 'my_password';
Run Code Online (Sandbox Code Playgroud)

如果用户已存在,则会失败.我喜欢这样的东西:

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

...但是这不起作用 - IF在纯SQL中似乎不支持.

我有一个批处理文件,可以创建PostgreSQL 9.1数据库,角色和其他一些东西.它调用psql.exe,传入要运行的SQL脚本的名称.到目前为止,所有这些脚本都是纯SQL,我想尽可能避免使用PL/pgSQL等.

Erw*_*ter 137

以与您的想法类似的方式简化:

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

      CREATE ROLE my_user LOGIN PASSWORD 'my_password';
   END IF;
END
$do$;
Run Code Online (Sandbox Code Playgroud)

(基于@a_horse_with_no_name的回答并在@Gregory 的评论之后得到改进.)

例如,与(还)CREATE TABLE没有IF NOT EXISTS条款不同CREATE ROLE.并且您无法在纯SQL中执行动态DDL语句.

除了使用另一个PL之外,你的"避免PL/pgSQL"的请求是不可能的.该DO语句使用plpgsql作为默认过程语言.语法允许省略显式声明:

DO [ LANGUAGE lang_name ] code
... 编写代码的过程语言的名称.如果省略,则默认为.
lang_name
plpgsql

  • @Ken:如果`$`在客户端中有特殊含义,则需要根据客户端的语法规则对其进行转义。尝试在Linux Shell中使用\\来转义$。或开始一个新的问题-评论不是地方。您可以始终链接到该上下文。 (2认同)
  • @ErwinBrandstetter这对于具有NOLOGIN的角色不起作用.它们出现在pg_roles中但不出现在pg_user中. (2认同)
  • 此解决方案存在竞争条件。一个更安全的变体[记录在这个答案中](/sf/answers/3490115821/)。 (2认同)

Bor*_*rys 38

或者,如果角色不是任何数据库对象的所有者,则可以使用:

DROP ROLE IF EXISTS my_user;
CREATE ROLE my_user LOGIN PASSWORD 'my_password';
Run Code Online (Sandbox Code Playgroud)

但只有放弃这个用户不会造成任何伤害.

  • 另外,请确保您没有向“my_user”分配任何权限。否则,它将因“错误:无法删除,因为某些对象依赖于它”而失败。 (2认同)

blu*_*ubb 27

如果在同一Postgres集群(DB服务器)上同时执行两个这样的脚本,则接受的答案会受到竞争条件的影响,这在连续集成环境中很常见.

尝试创建角色并在创建时优雅地处理问题通常更安全:

DO $$
BEGIN
  CREATE ROLE my_role WITH NOLOGIN;
  EXCEPTION WHEN OTHERS THEN
  RAISE NOTICE 'not creating role my_role -- it already exists';
END
$$;
Run Code Online (Sandbox Code Playgroud)

  • 如果您不想用“OTHERS”捕获几乎所有条件,则“DUPLICATE_OBJECT”是这种情况下的精确条件。 (3认同)
  • 我喜欢这种方式,因为它通知存在。 (2认同)

Pal*_*ali 13

一些答案建议使用模式:检查角色是否不存在,如果不存在则发出CREATE ROLE命令。这有一个缺点:竞争条件。如果其他人在检查和发出CREATE ROLE命令之间创建了一个新角色,那么CREATE ROLE显然会因致命错误而失败。

为了解决上述问题,更多其他答案已经提到了 的用法PL/pgSQLCREATE ROLE无条件发出然后从该调用中捕获异常。这些解决方案只有一个问题。他们默默地删除任何错误,包括那些不是由角色已经存在的事实产生的错误。CREATE ROLE也可以抛出其他错误,IF NOT EXISTS当角色已经存在时,模拟应该只沉默错误。

CREATE ROLEduplicate_object当角色已经存在时抛出错误。并且异常处理程序应该只捕获这个错误。正如其他答案所提到的,将致命错误转换为简单通知是个好主意。其他 PostgreSQLIF NOT EXISTS命令会添加, skipping到它们的消息中,因此为了保持一致性,我也将其添加到此处。

这是用于模拟CREATE ROLE IF NOT EXISTS正确异常和 sqlstate 传播的完整 SQL 代码:

DO $$
BEGIN
CREATE ROLE test;
EXCEPTION WHEN duplicate_object THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
END
$$;
Run Code Online (Sandbox Code Playgroud)

测试输出(通过 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
Run Code Online (Sandbox Code Playgroud)

  • 谢谢。没有竞争条件,严格的异常捕获,包装 Postgres 自己的消息而不是重写您自己的消息。 (2认同)
  • 的确!这是目前唯一正确的答案,它不受竞争条件的影响,并使用必要的选择性错误处理。非常遗憾的是,这个答案是在(不完全正确的)最佳答案收集了超过 100 分之后出现的。 (2认同)

Edu*_*omo 11

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';"
Run Code Online (Sandbox Code Playgroud)

(不是问题的答案!只适用于那些可能有用的人)

  • 它应该读取“FROM pg_roles WHERE rolname”而不是“FROM pg_user WHERE usename” (3认同)

AHa*_*III 9

根据此处的其他答案,我希望能够psql对文件执行一次.sql以使其执行一组初始化操作。我还希望能够在执行时注入密码以支持 CI/CD 场景。

-- init.sql

CREATE OR REPLACE FUNCTION pg_temp.create_myuser(theUsername text, thePassword text)
RETURNS void AS
$BODY$
DECLARE
  duplicate_object_message text;
BEGIN
  BEGIN
    EXECUTE format(
      'CREATE USER %I WITH PASSWORD %L',
      theUsername,
      thePassword
    );
  EXCEPTION WHEN duplicate_object THEN
    GET STACKED DIAGNOSTICS duplicate_object_message = MESSAGE_TEXT;
    RAISE NOTICE '%, skipping', duplicate_object_message;
  END;
END;
$BODY$
LANGUAGE 'plpgsql';

SELECT pg_temp.create_myuser(:'vUsername', :'vPassword');
Run Code Online (Sandbox Code Playgroud)

调用方式psql

NEW_USERNAME="my_new_user"
NEW_PASSWORD="password with 'special' characters"

psql --no-psqlrc --single-transaction --pset=pager=off \
  --tuples-only \
  --set=ON_ERROR_STOP=1 \
  --set=vUsername="$NEW_USERNAME" \
  --set=vPassword="$NEW_PASSWORD" \
  -f init.sql
Run Code Online (Sandbox Code Playgroud)

这将允许init.sql在本地运行或通过 CI/CD 管道运行。


笔记:

  • 我没有找到:vPassword直接在DO匿名函数中引用文件变量()的方法,因此需要完整FUNCTION传递参数。(参见@Clodoaldo Neto 的回答
  • @Erwin Brandstetter 的回答解释了为什么我们必须使用 anEXECUTE而不能CREATE USER直接使用。
  • @Pali 的答案解释了防止竞争条件的必要性EXCEPTION(这就是\gexec不推荐该方法的原因)。
  • 该函数必须在语句中调用SELECT。使用命令中的-t/属性来清理日志输出,如@villy393 的回答中指出的。--tuples-onlypsql
  • 该函数是在临时模式中创建的,因此它将被自动删除。
  • 引用处理得当,因此密码中的特殊字符不会导致错误或更糟糕的安全漏洞。


Ing*_*her 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;
Run Code Online (Sandbox Code Playgroud)

用法:

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)
Run Code Online (Sandbox Code Playgroud)


a_h*_*ame 7

当您使用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$
;
Run Code Online (Sandbox Code Playgroud)


Chr*_*tti 7

我的团队遇到了一个服务器上有多个数据库的情况,这取决于您连接到哪个数据库,有问题的ROLE没有被返回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$
Run Code Online (Sandbox Code Playgroud)

它可能更具体,以排除其他例外.

  • pg_user 表似乎只包含具有 LOGIN 的角色。如果角色具有 NOLOGIN,则它不会出现在 pg_user 中,至少在 PostgreSQL 10 中。 (3认同)

vil*_*393 7

如果您有权访问 shell,则可以执行此操作。

psql -tc "SELECT 1 FROM pg_user WHERE usename = 'some_use'" | grep -q 1 || psql -c "CREATE USER some_user"
Run Code Online (Sandbox Code Playgroud)

对于那些想要解释的人:

-c = run command in database session, command is given in string
-t = skip header and footer
-q = silent mode for grep 
|| = logical OR, if grep fails to find match run the subsequent command
Run Code Online (Sandbox Code Playgroud)


Ale*_*war 6

Simulate CREATE DATABASE IF NOT EXISTS for PostgreSQL相同的解决方案应该工作 - 发送CREATE USER …\gexec.

从 psql 中解决方法

SELECT 'CREATE USER my_user'
WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'my_user')\gexec
Run Code Online (Sandbox Code Playgroud)

从外壳解决方法

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

有关更多详细信息,请参阅那里的接受答案