【问题标题】:Recursive CTE query in EXISTSEXISTS 中的递归 CTE 查询
【发布时间】:2021-09-26 17:15:28
【问题描述】:

我在 mariaDB/mysql 上遇到了递归 cte 的问题。设置并不太复杂:有一个users 表、一个roles 表和一个为用户分配角色的user_roles 表。但是,角色可以嵌套,roles 表包含一个parent_id 字段来完成此操作。 parent_id 字段在它是根角色时将为 NULL,如果不是,则包含另一个角色的 ID。

这可以通过以下创建和插入进行设置:

CREATE TABLE users (
    id INT NOT NULL AUTO_INCREMENT,
    name VARCHAR(255),
    PRIMARY KEY (id)
);

CREATE TABLE roles (
    id INT NOT NULL AUTO_INCREMENT,
    name VARCHAR(255),
    parent_id INT,
    PRIMARY KEY (id)
);

CREATE TABLE user_roles (
    id INT NOT NULL AUTO_INCREMENT,
    user_id INT,
    role_id INT,
    PRIMARY KEY (id)
);

INSERT INTO users(name) VALUES ('Alice'),('Bob'),('Charlie');

INSERT INTO roles(name, parent_id) VALUES
('superuser',null),
('admin',1),
('ceo',1),
('employee', null);

INSERT INTO user_roles(user_id, role_id) VALUES
(1,2),
(2,3),
(3,4);

我的目标是创建一个查询,以选择其中一个用户角色(或其中一个管理员)与给定角色名称匹配的所有用户。我想出了这个:

SELECT * FROM users
WHERE EXISTS(
    SELECT 1 FROM roles
    INNER JOIN user_roles ON roles.id = user_roles.role_id
    WHERE users.id = user_roles.user_id
    AND EXISTS(
        WITH RECURSIVE cte AS (
            SELECT * FROM roles as roles_recursive
            WHERE roles_recursive.id = roles.id
            UNION ALL
            SELECT roles_recursive.* FROM roles as roles_recursive
            INNER JOIN cte ON cte.parent_id = roles_recursive.id
        )
        SELECT 1 from cte WHERE name='superuser'
    )
);

我设置了DB fiddle 来重现我的问题。请在那里查看完整的复制案例。 这会在不同的引擎上产生不同的结果:

  • MySQL 8.0.18:我目前的开发机器只运行macOS 10.13,所以我仅限于这个MySQL版本。在这个版本中,我只会得到“Alice”,而“Bob”也应该出现在结果中。
  • DB fiddle,MariaDB 10.3.30:产生错误:“ER_BAD_FIELD_ERROR:'where 子句'中的未知列 'roles.id'”。看起来角色范围在 CTE 中不可用。
  • dbfiddle.uk 似乎正在运行另一个版本的 MySQL,并且确实返回了正确的结果集。

有什么方法可以做到这一点,至少在 MariaDB 中有效?我不是在寻找具有简单连接的解决方案,角色应该支持可变深度。

【问题讨论】:

  • 您好,请将您的设置也添加到您的问题中,小提琴网站上的链接是临时的,这会使您的问题难以被未来的读者理解。
  • 供将来参考:MariaDB 并不真正支持这一点:jira.mariadb.org/browse/MDEV-19337

标签: mysql sql mariadb common-table-expression


【解决方案1】:

首先从给定的角色中找到层次结构中的所有角色,然后选择担任找到的任何角色的用户。

WITH RECURSIVE cte AS (
   -- The role and its descendants 
   SELECT * 
   FROM roles 
   WHERE name = 'superuser'
   UNION ALL
   SELECT roles.* 
   FROM roles 
   JOIN cte ON cte.id = roles.parent_id 
)
SELECT DISTINCT u.* 
FROM users u
JOIN user_roles ur ON u.id = ur.user_id
JOIN cte r ON r.id = ur.role_id

db<>fiddle

【讨论】:

    【解决方案2】:

    这是一个例子。

    all_roles 递归查找与其完整祖先配对的所有角色。

    all_roles.base_role 是起始子角色id,起始名称为base_name

    uRoles 查找与每个base_role 关联的对应user_id

    最后,我们找到所有具有与给定角色名称匹配的 uRoles 行的用户。

    注意:我添加了一个级别限制,以防万一。如果您愿意,可以删除它,或者根据需要增加它,以避免可能导致无限递归的问题。

    WITH RECURSIVE all_roles (base_role, base_name, id, name, parent_id, lev)  AS (
             SELECT r.id  AS base_role, r.name      , r.*, 1 AS lev
               FROM roles AS r
              UNION ALL
             SELECT r0.base_role      , r0.base_name, r.*, lev + 1
               FROM roles     AS r
               JOIN all_roles AS r0
                 ON r.id = r0.parent_id
                AND lev < 8
         )
       , uRoles AS (
             SELECT ur.*, ar.name, ar.base_name, ar.lev
               FROM all_roles  AS ar
               JOIN user_roles AS ur
                 ON ar.base_role = ur.role_id
         )
    SELECT *
      FROM users
     WHERE id IN (SELECT user_id FROM uRoles WHERE name = 'superuser')
    ;
    
    
    Result:
    
    +----+-------+
    | id | name  |
    +----+-------+
    |  1 | Alice |
    |  2 | Bob   |
    +----+-------+
    

    【讨论】: