嗯,我想出了一个解决方案,但并不理想。对于 10,000 条记录,返回与安全配置文件匹配的所有行需要 5 秒。这不是一场彻头彻尾的灾难,它确实有效,但我必须稍后再回到这个问题来改进它。
这就是我解决它的方法。请记住,我只为此工作了几个小时。
在我真正做任何事情之前,我知道我需要一个函数来比较两个逗号分隔的列表。我需要在列表中包含用户的角色,并查看这些角色中是否有任何角色出现在存储在我的 xml 列中的授权设置中,如原始帖子中所述。为此,我做了两个函数。
第一个函数是常见的使用xml进行字符串拆分的函数:
IF EXISTS (
SELECT * FROM sysobjects WHERE id = object_id(N'ufnSplitStrings')
AND xtype IN (N'FN', N'IF', N'TF')
)
DROP FUNCTION ufnSplitStrings
GO
CREATE FUNCTION dbo.ufnSplitStrings
(
@List NVARCHAR(MAX),
@Delimiter NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
SELECT Item = y.i.value('(./text())[1]', 'nvarchar(4000)')
FROM
(
SELECT x = CONVERT(XML, '<i>'
+ REPLACE(@List, @Delimiter, '</i><i>')
+ '</i>').query('.')
) AS a CROSS APPLY x.nodes('i') AS y(i)
);
建立该函数后,我可以创建另一个函数,然后进行我想要的比较:
IF EXISTS (
SELECT * FROM sysobjects WHERE id = object_id(N'ufnContainsAny')
AND xtype IN (N'FN', N'IF', N'TF')
)
DROP FUNCTION ufnContainsAny
GO
CREATE FUNCTION dbo.ufnContainsAny(@List1 NVARCHAR(MAX), @List2 NVARCHAR(MAX))
RETURNS int
AS
BEGIN
DECLARE @Ret AS INT = 0
SELECT @Ret = COUNT(*) FROM dbo.ufnSplitStrings(@List1, ',') x
JOIN dbo.ufnSplitStrings(@List2, ',') y ON x.Item = y.Item
RETURN @Ret
END;
GO
最后,我可以使用该函数来组装我的主要 UserIsAuthorized 函数。
IF EXISTS (
SELECT * FROM sysobjects WHERE id = object_id(N'ufnUserIsAuthorized')
AND xtype IN (N'FN', N'IF', N'TF')
)
DROP FUNCTION ufnUserIsAuthorized
GO
CREATE FUNCTION dbo.ufnUserIsAuthorized(@SecurityRules XML, @UserName NVARCHAR(64), @UserRoles NVARCHAR(MAX))
RETURNS int
AS
BEGIN
DECLARE @ret int = 0;
DECLARE @AuthType NVARCHAR(32);
DECLARE @authRules Table (a nvarchar(32), u nvarchar(max), r nvarchar(max), o int)
INSERT INTO @authRules
SELECT
a = value.value('local-name(.[1])', 'varchar(32)'),
u = ',' + value.value('@users', 'varchar(max)') + ',',
r = ',' + value.value('@roles', 'varchar(max)') + ',',
o = value.value('for $i in . return count(../*[. << $i]) + 1', 'int')
FROM @SecurityRules.nodes('//allow,//deny') AS T(value)
SELECT TOP 1 @AuthType = a FROM @authRules
WHERE CHARINDEX(',' + @UserName + ',', u) > 0 OR CHARINDEX(',*,', u) > 0 OR dbo.ufnContainsAny(r, @UserRoles) > 0 OR CHARINDEX(',*,', r) > 0
GROUP BY a
ORDER BY MIN(o)
IF (@AuthType IS NOT NULL AND @AuthType = 'allow')
SET @ret = 1;
RETURN @ret;
END;
该函数将 xml 允许和拒绝节点拆分为一个表,其中包含授权类型(允许或拒绝)、用户列表、角色列表,最后是特定节点在文档中出现的顺序。最后,我可以抓取找到用户或用户角色之一的第一个节点。如果该节点是“允许”,那么我返回一个 1。
是的,这有点可怕,因为我们在每个调用中都声明了一个表。我尝试了各种小测试,我只查找用户名(以避免必须对 ufnContainsAny 进行任何调用),但性能没有改变。我还尝试将“o”列更改为简单的标识列,因为我正在选择所有节点 - 这将允许它跳过我认为可能是获取节点顺序的耗时计算。但这也不影响性能。
因此,这种方法需要工作也就不足为奇了。如果有人有任何建议,我会全力以赴。
我最初使用此功能的行数很少,因此我可以在中间使用它,直到我想出更好的解决方案(或完全放弃此方法)。
编辑:
只需跳过 DECLARE 表/INSERT 即可显着提高性能。相反,我们可以这样做:
SELECT TOP 1 @AuthType = a FROM
(
SELECT
a = value.value('local-name(.[1])', 'varchar(32)'),
u = ',' + value.value('@users', 'varchar(max)') + ',',
r = ',' + value.value('@roles', 'varchar(max)') + ',',
o = value.value('for $i in . return count(../*[. << $i]) + 1', 'int')
FROM @SecurityRules.nodes('//allow,//deny') AS T(value)
) AS sec
WHERE CHARINDEX(',' + @UserName + ',', u) > 0 OR CHARINDEX(',*,', u) > 0 OR dbo.ufnContainsAny(r, @UserRoles) > 0 OR CHARINDEX(',*,', r) > 0
GROUP BY a
ORDER BY MIN(o)