使用 SQL 搜索一对多关系中的集合
Posted
技术标签:
【中文标题】使用 SQL 搜索一对多关系中的集合【英文标题】:Using SQL to search for a set in a one-to-many relationship 【发布时间】:2009-02-02 19:20:53 【问题描述】:给定表格:
角色:角色ID,名称 权限:permissionid,名称 role_permission: 角色id,permissionid
我有一组权限,我想查看是否存在具有这些权限的现有角色,或者我是否需要创建一个新角色。请注意,我已经知道权限 ID,因此权限表实际上可以忽略 - 为了清楚起见,我只是将其包含在此处。
这可以在 SQL 查询中执行吗?我想它必须是一个动态生成的查询。
如果没有,有没有比简单地遍历每个角色并查看它是否具有确切权限的蛮力方法更好的方法?
请注意,我正在寻找具有确切权限集的角色 - 不多也不少。
【问题讨论】:
【参考方案1】:您可以选择具有您要查找的权限子集的所有角色。数一数权限数,看看是否正好等于你需要的权限数:
select r.roleid
from role r
where not exists (select * from role_permissions rp where rp.roleid = r.roleid and rp.permissionid not in (1,2,3,4)) -- id of permissions
and (select count(*) from role_permissions rp where rp.roleid = r.roleid) = 4 -- number of permissions
【讨论】:
这假定 role_permission 中没有重复项,但这可能是该表的约束。 如果只应用权限的子集,这实际上也会选择角色 - 例如,如果您的角色具有权限 1、2、3、4,并且您运行此查询,它将在您尝试时匹配“1,2,3,4”或“1,2,3”。 @gregmac:诀窍是有子句,它将确保计数等于您正在查找的权限数,因此不会匹配子集。 它没有(至少在 SQL 服务器上)。我认为正在发生的事情是在 WHERE 限制权限 ID 之后发生计数 - 因此计数将始终与 WHERE 子句中提供的数字匹配,而不是实际存在的数字。 我解决了这个问题: select r.roleid from role r join role_permission rp on r.roleid = rp.roleid where rp.permissionid in (1,2,3,4) and (select count (*) from role_permission where roleid = r.roleid) = 4 group by r.roleid 但我不确定这是否是最好的方法..【参考方案2】:在对我对这个问题的第一个答案进行了哈希处理后,这里有一个稍微偏左的字段替代方案,它可以工作,但确实涉及向数据库添加数据。
诀窍是在权限表中添加一列,为每一行保存一个唯一值。
这是一种相当常见的模式,并且会给出精确的结果。缺点是您必须编写代码来隐藏数字等价物。
id int(10)
name varchar(45)
value int(10)
那么内容会变成:
Permission: Role Role_Permission
id name value id name roleid permissionid
-- ---- ----- -- ---- ------ ------------
1 Read 8 1 Admin 1 1
2 Write 16 2 DataAdmin 1 2
3 Update 32 3 User 1 3
4 Delete 64 1 4
2 1
2 3
2 4
那么每个角色组合都会给出一个唯一的值:
SELECT x.roleid, sum(value) FROM role_permission x
inner join permission p
on x.permissionid = p.id
Group by x.roleid
给予:
roleid sum(value)
------ ----------
1 120 (the sum of permissions 1+2+3+4 = 120)
2 104 (the sum of permissions 1+3+4 = 104)
现在我把那个开瓶器放在哪里了……
【讨论】:
【参考方案3】:这是一个古老的 sql 技巧(至少在 Oracle 中有效):
SELECT roleid FROM role_permission t1
WHERE NOT EXISTS (
(SELECT permissionid FROM role_permission t2 WHERE t2.roleid = t1.roleid
MINUS
SELECT permissionid FROM role_permission WHERE roleid = 'Admin')
UNION
(SELECT permissionid FROM role_permission t2 WHERE roleid = 'Admin'
MINUS
SELECT permissionid FROM role_permsission t2 WHERE t2.roleid = t1.roleid)
)
也未验证。红酒总是听起来不错。
【讨论】:
这很好..有人应该把这个和Mehrdad的建议放在一起...... 谢谢。我也喜欢 Mehrdad 的解决方案。这样做的好处是您不需要事先知道您尝试匹配的权限数量,只需知道您尝试匹配的角色 ID。缺点是它相当不透明。 我开始写一个新的答案..但基本上所有需要的只是将 roleid = 'Admin' 更改为 roleid IN (1,2,3,4),如 Mehrdad 的答案。剩下的就是解释为什么这样有效.. ;-) 对于Oracle,一个建议是从'with reqd_roles as ..'开始,这会让事情更容易理解。您还只需在一处更改所需的角色。 IronGoofy:原因很明显:A Δ B = Φ A = B【参考方案4】:您基本上需要检查是否有一个角色具有您正在检查的确切数量的不同权限。
我在 SQL Server 2005 上检查了这个存储过程,它只返回那些权限 ID 与传入的逗号分隔权限 ID 列表中的角色 ID 完全匹配的角色 ID -
CREATE PROC get_roles_for_permissions (@list nvarchar(max)) -- @list is a comma separated list of your permission ids
AS
SET NOCOUNT ON
BEGIN
DECLARE @index INT, @start_index INT, @id INT
DECLARE @permission_ids TABLE (id INT)
SELECT @index = 1
SELECT @start_index = 1
WHILE @index <= DATALENGTH(@list)
BEGIN
IF SUBSTRING(@list,@index,1) = ','
BEGIN
SELECT @id = CAST(SUBSTRING(@list, @start_index, @index - @start_index ) AS INT)
INSERT INTO @permission_ids ([id]) VALUES (@id)
SELECT @start_index = @index + 1
END
SELECT @index = @index + 1
END
SELECT @id = CAST(SUBSTRING(@list, @start_index, @index - @start_index ) AS INT)
INSERT INTO @permission_ids ([id]) VALUES (@id)
SELECT
r.roleid
FROM
role r
INNER JOIN
role_permission rp
ON r.roleid = rp.roleid
INNER JOIN
@permission_ids ids
ON
rp.permissionid = ids.id
GROUP BY r.roleid
HAVING(SELECT COUNT(*)
FROM role_permission
WHERE roleid = r.roleid) = (SELECT COUNT(*) FROM @permission_ids)
END
示例数据
CREATE TABLE [dbo].[role](
[roleid] [int] IDENTITY(1,1) NOT NULL,
[name] [nvarchar](50)
)
CREATE TABLE [dbo].[permission](
[permissionid] [int] IDENTITY(1,1) NOT NULL,
[name] [nvarchar](50)
)
CREATE TABLE [dbo].[role_permission](
[roleid] [int],
[permissionid] [int]
)
INSERT INTO role(name) VALUES ('Role1')
INSERT INTO role(name) VALUES ('Role2')
INSERT INTO role(name) VALUES ('Role3')
INSERT INTO role(name) VALUES ('Role4')
INSERT INTO permission(name) VALUES ('Permission1')
INSERT INTO permission(name) VALUES ('Permission2')
INSERT INTO permission(name) VALUES ('Permission3')
INSERT INTO permission(name) VALUES ('Permission4')
INSERT INTO role_permission(roleid, permissionid) VALUES (1, 1)
INSERT INTO role_permission(roleid, permissionid) VALUES (1, 2)
INSERT INTO role_permission(roleid, permissionid) VALUES (1, 3)
INSERT INTO role_permission(roleid, permissionid) VALUES (1, 4)
INSERT INTO role_permission(roleid, permissionid) VALUES (2, 2)
INSERT INTO role_permission(roleid, permissionid) VALUES (2, 3)
INSERT INTO role_permission(roleid, permissionid) VALUES (2, 4)
INSERT INTO role_permission(roleid, permissionid) VALUES (3, 3)
INSERT INTO role_permission(roleid, permissionid) VALUES (3, 4)
INSERT INTO role_permission(roleid, permissionid) VALUES (4, 4)
EXEC get_roles_for_permissions '3,4' -- RETURNS roleid 3
【讨论】:
【参考方案5】:也许使用子查询...
SELECT * FROM role r
WHERE r.rolid = (SELECT x.roledid
FROM role_permission
WHERE x.permissionid in (1,2,3,4);
抱歉,还没有验证这一点,但刚刚花了一个小时调试 php 代码以解决另一个问题,我觉得需要一杯红酒。
【讨论】:
不起作用。它将匹配具有其中一种权限的每个角色。 你是对的,Mehrdad,它甚至不是有效的 SQL,因为它最后缺少一个结束 ")",太着急发布一个明智的答案以上是关于使用 SQL 搜索一对多关系中的集合的主要内容,如果未能解决你的问题,请参考以下文章