ChatGPT解决这个技术问题 Extra ChatGPT

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

如何编写 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;

...但这不起作用 - 普通 SQL 似乎不支持 IF

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


E
Erwin Brandstetter

简单脚本(提问)

@a_horse_with_no_name 的回答为基础,并通过 @Gregory's comment 进行了改进:

DO
$do$
BEGIN
   IF EXISTS (
      SELECT FROM pg_catalog.pg_roles
      WHERE  rolname = 'my_user') THEN

      RAISE NOTICE 'Role "my_user" already exists. Skipping.';
   ELSE
      CREATE ROLE my_user LOGIN PASSWORD 'my_password';
   END IF;
END
$do$;

例如,与 CREATE TABLE 不同,CREATE ROLE 没有 IF NOT EXISTS 子句(至少 Postgres 14)。而且您不能在纯 SQL 中执行动态 DDL 语句。

您要求“避免 PL/pgSQL”是不可能的,除非使用另一个 PL。 DO statement 使用 PL/pgSQL 作为默认过程语言:

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

没有比赛条件

上述简单的解决方案允许在查找角色和创建角色之间的微小时间范围内出现竞争条件。如果并发事务在两者之间创建角色,我们毕竟会得到一个异常。在大多数工作负载中,这永远不会发生,因为创建角色是管理员很少执行的操作。但也有像 @blubb mentioned 这样有争议的工作负载。
@Pali 添加了一个捕获异常的解决方案。但是带有 EXCEPTION 子句的代码块很昂贵。 The manual:

包含 EXCEPTION 子句的块的进入和退出比没有的块要昂贵得多。因此,不要在没有必要的情况下使用 EXCEPTION。

实际上引发异常(然后捕获它)相对来说是比较昂贵的。所有这一切只对经常执行它的工作负载很重要——恰好是主要目标受众。优化:

DO
$do$
BEGIN
   IF EXISTS (
      SELECT FROM pg_catalog.pg_roles
      WHERE  rolname = 'my_user') THEN

      RAISE NOTICE 'Role "my_user" already exists. Skipping.';
   ELSE
      BEGIN   -- nested block
         CREATE ROLE my_user LOGIN PASSWORD 'my_password';
      EXCEPTION
         WHEN duplicate_object THEN
            RAISE NOTICE 'Role "my_user" was just created by a concurrent transaction. Skipping.';
      END;
   END IF;
END
$do$;

便宜得多:

如果角色已经存在,我们永远不会进入昂贵的代码块。

如果我们输入昂贵的代码块,那么角色只有在不太可能的竞争条件发生时才存在。所以我们几乎没有真正引发异常(并捕获它)。


@Alberto:pg_userpg_roles 都是正确的。当前版本 9.3 中的情况仍然如此,并且不会很快改变。
@Ken:如果 $ 在您的客户端中具有特殊含义,则需要根据客户端的语法规则对其进行转义。尝试在 Linux shell 中使用 \$ 转义 $。或者开始一个新问题 - 评论不是地方。您始终可以链接到此以获取上下文。
我使用的是 9.6,如果用户是使用 NOLOGIN 创建的,它们不会出现在 pg_user 表中,但会出现在 pg_roles 表中。 pg_roles 会是一个更好的解决方案吗?
@ErwinBrandstetter 这不适用于具有 NOLOGIN 的角色。它们出现在 pg_roles 中,但不在 pg_user 中。
该解决方案存在竞争条件。更安全的变体是 documented in this answer
B
Borys

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

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

但前提是放弃该用户不会造成任何伤害。


此外,请确保您没有为 my_user 分配任何权限。否则会因为 Error: cannot be dropped because some objects depend on it 而失败。
P
Pali

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

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

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

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

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

谢谢你。没有竞争条件,严格的异常捕获,包装 Postgres 自己的消息而不是重写你自己的。
的确!这是目前这里唯一正确的答案,它不受竞争条件的影响,并使用必要的选择性错误处理。很遗憾,这个答案出现在(不完全正确的)最佳答案收集超过 100 分之后。
不客气!我的解决方案还传播 SQLSTATE,因此如果您使用 SQL 连接器从其他 PL/SQL 脚本或其他语言调用语句,您将收到正确的 SQLSTATE。
这很棒。让我们希望它尽快投票到顶部!我编辑了自己的答案,以参考您的答案以加快流程。
R
Rob Bednark

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';"

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


它应该是 FROM pg_roles WHERE rolname 而不是 FROM pg_user WHERE usename
请注意,这不仅会受到竞争条件的影响,还会向数据库添加完整的往返。
W
Wolkenarchitekt

这是使用 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)

C
Chris Betti

正如@erwin-brandstetter 和@a_horse_with_no_name 所建议的那样,我的团队在一台服务器上遇到了多个数据库的情况,具体取决于您连接到哪个数据库,SELECT * FROM pg_catalog.pg_user 未返回相关角色。条件块执行,我们点击 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$

排除其他例外情况可能会更具体。


pg_user 表似乎只包括具有 LOGIN 的角色。如果一个角色有 NOLOGIN,它不会出现在 pg_user 中,至少在 PostgreSQL 10 中是这样。
A
Alexander Skwar

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

从外壳解决方法

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

有关详细信息,请参阅 accepted answer there


您的解决方案仍然存在我在回答 stackoverflow.com/a/55954480/7878845 中描述的竞争条件
a
a_horse_with_no_name

在 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 应该是 `SELECT count(*) into num_users FROM pg_roles WHERE rolname = 'data_rw';` 否则将不起作用
T
Tometzky

基于此处的其他答案,我希望能够对 .sql 文件执行一次 psql 以使其执行一组初始化操作。我还希望能够在执行时注入密码以支持 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');

使用 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

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

笔记:

我没有找到直接在 DO 匿名函数中引用文件变量(:vPassword)的方法,因此使用完整的 FUNCTION 来传递 arg。 (见@Clodoaldo Neto 的回答)

@Erwin Brandstetter 的回答解释了为什么我们必须使用 EXECUTE 而不能直接使用 CREATE USER。

@Pali 的回答解释了需要 EXCEPTION 来防止竞争条件(这就是不推荐使用 \gexec 方法的原因)。

该函数必须在 SELECT 语句中调用。正如@villy393 的回答中所指出的,使用 psql 命令中的 -t/--tuples-only 属性来清理日志输出。

该函数是在临时模式中创建的,因此将自动删除。

引号处理得当,因此密码中的特殊字符不会导致错误或更糟糕的安全漏洞。


S
Sheva

您可以通过解析以下输出在批处理文件中执行此操作:

SELECT * FROM pg_user WHERE usename = 'my_user'

如果角色不存在,则再次运行 psql.exe


“用户名”列不存在。它应该是“用户名”。
“用户名”是不存在的。 :)
请参阅 pg_user 查看文档。版本 7.4-9.6 中没有“用户名”列,“用户名”是正确的。
v
villy393

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

psql -tc "SELECT 1 FROM pg_user WHERE usename = 'some_use'" | grep -q 1 || psql -c "CREATE USER some_user"

对于那些想要解释的人:

-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

m
minusf

当用户存在时,我需要在 Makefile 中使用它以确保作业不会失败:

initdb:
    psql postgres -c "CREATE USER foo CREATEDB PASSWORD 'bar'" || true
    ...