如何使用 SQLXML 修改设置多值 XML 属性

How to set a multi-valued XML attribute using SQLXML modify

我正在尝试使用 SQLXML 修改函数来更新多值 (xs:list) 属性。我可以在构造 XML(从字符串)时设置多个值,但是 SQLXML 修改不允许我设置多个值。

初始XML:

<AccessControlList xmlns="http://www.acme.com/Authorization/2013/01">
  <AccessControlRecord Permissions="Fullcontrol" />
  <AccessControlRecord Permissions="DenyCreate DenyRead DenyUpdate DenyDelete" />
</AccessControlList>

设置 SINGLE 值效果很好:

DECLARE @SingleValue NVARCHAR(100) = 'DenyCreate';
WITH XMLNAMESPACES ( 'http://www.acme.com/Authorization/2013/01' AS A )
UPDATE dbo.Widget
SET ACL.modify('replace value of (/A:AccessControlList/A:AccessControlRecord/@Permissions)[1] with sql:variable("@SingleValue") cast as A:AccessPermissions ?')
FROM dbo.Widget;

设置多个值失败:

DECLARE @MultipleValues NVARCHAR(100) = 'DenyCreate DenyRead DenyUpdate DenyDelete';
WITH XMLNAMESPACES ( 'http://www.acme.com/Authorization/2013/01' AS A )
UPDATE dbo.Widget
SET ACL.modify('replace value of (/A:AccessControlList/A:AccessControlRecord/@Permissions)[1] with sql:variable("@MultipleValues") cast as A:AccessPermissions ?')
FROM dbo.Widget;

出现此错误:

XQuery: Replacing the value of a node with an empty sequence is allowed only if '()' is used as the new value expression. The new value expression evaluated to an empty sequence but it is not '()'.

变量不为空。我还尝试了其他变体,但因不同的错误而失败。

完整 SQL 重现:

-- Drop table and schema collection
IF OBJECT_ID('dbo.Widget') IS NOT NULL
    DROP TABLE dbo.Widget;
IF EXISTS ( SELECT * FROM sys.xml_schema_collections WHERE SCHEMA_NAME(schema_id) = 'dbo' AND name = 'AccessControlList' )
    DROP XML SCHEMA COLLECTION dbo.AccessControlList;
GO

-- Create schema collection
CREATE XML SCHEMA COLLECTION dbo.AccessControlList AS N'
<xs:schema id="AccessControlList" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.acme.com/Authorization/2013/01" xmlns="http://www.acme.com/Authorization/2013/01">
    <xs:simpleType name="AccessPermissions">
        <xs:list>
            <xs:simpleType>
                <xs:restriction base="xs:string">
                    <xs:enumeration value="Create" />
                    <xs:enumeration value="Read" />
                    <xs:enumeration value="Update" />
                    <xs:enumeration value="Delete" />
                    <xs:enumeration value="Execute" />
                    <xs:enumeration value="Fullcontrol" />
                    <xs:enumeration value="DenyCreate" />
                    <xs:enumeration value="DenyRead" />
                    <xs:enumeration value="DenyUpdate" />
                    <xs:enumeration value="DenyDelete" />
                    <xs:enumeration value="DenyExecute" />
                    <xs:enumeration value="FullDeny" />
                </xs:restriction>
            </xs:simpleType>
        </xs:list>
    </xs:simpleType>
    <xs:complexType name="AccessControlRecord">
        <xs:attribute name="Permissions" type="AccessPermissions" use="required" />
    </xs:complexType>
    <xs:complexType name="AccessControlList">
        <xs:sequence>
            <xs:element minOccurs="0" maxOccurs="unbounded" name="AccessControlRecord" type="AccessControlRecord" />
        </xs:sequence>
    </xs:complexType>
    <xs:element name="AccessControlList" nillable="true" type="AccessControlList" />
</xs:schema>
';
GO

-- Create table, insert test data, and display initial state of data
CREATE TABLE dbo.Widget
(
    WidgetId INT PRIMARY KEY IDENTITY(1,1),
    ACL XML(DOCUMENT dbo.AccessControlList)
);
INSERT INTO dbo.Widget
    ( ACL )
VALUES
    ( N'<AccessControlList xmlns="http://www.acme.com/Authorization/2013/01" >
            <AccessControlRecord Permissions="Fullcontrol" />
            <AccessControlRecord Permissions="DenyCreate DenyRead DenyUpdate DenyDelete" />
        </AccessControlList>' );
WITH XMLNAMESPACES ( 'http://www.acme.com/Authorization/2013/01' AS A )
SELECT *
    ,Acr1Permissions = CAST(ACL AS XML).value('(/A:AccessControlList/A:AccessControlRecord)[1]/@Permissions', 'NVARCHAR(128)')
    ,Acr2Permissions = CAST(ACL AS XML).value('(/A:AccessControlList/A:AccessControlRecord)[2]/@Permissions', 'NVARCHAR(128)')
FROM dbo.Widget;

-- Setting a SINGLE value works fine
DECLARE @SingleValue NVARCHAR(100) = 'DenyCreate';
WITH XMLNAMESPACES ( 'http://www.acme.com/Authorization/2013/01' AS A )
UPDATE dbo.Widget
SET ACL.modify('replace value of (/A:AccessControlList/A:AccessControlRecord/@Permissions)[1] with sql:variable("@SingleValue") cast as A:AccessPermissions ?')
FROM dbo.Widget;

-- Display values after
WITH XMLNAMESPACES ( 'http://www.acme.com/Authorization/2013/01' AS A )
SELECT *
    ,Acr1Permissions = CAST(ACL AS XML).value('(/A:AccessControlList/A:AccessControlRecord)[1]/@Permissions', 'NVARCHAR(128)')
    ,Acr2Permissions = CAST(ACL AS XML).value('(/A:AccessControlList/A:AccessControlRecord)[2]/@Permissions', 'NVARCHAR(128)')
FROM dbo.Widget;

/* Setting MULTIPLE values *FAILS*
DECLARE @MultipleValues NVARCHAR(100) = 'DenyCreate DenyRead DenyUpdate DenyDelete';
WITH XMLNAMESPACES ( 'http://www.acme.com/Authorization/2013/01' AS A )
UPDATE dbo.Widget
SET ACL.modify('replace value of (/A:AccessControlList/A:AccessControlRecord/@Permissions)[1] with sql:variable("@MultipleValues") cast as A:AccessPermissions ?')
FROM dbo.Widget;
*/

当我尝试使用 "sql:column".

设置多个值时,我遇到了同样的失败

我发现这个资源 (https://docs.microsoft.com/en-us/sql/xquery/type-casting-rules-in-xquery?view=sql-server-2017) 说不允许转换到列表类型或从列表类型转换;我希望有解决方案或解决方法。

这可以使用 SQLXML 吗?怎么样?

提前致谢

我必须承认,我以前从未遇到过这种情况...

而且我必须承认,我没有找到一个简单的解决方案。如果你找到了,请告诉我。

即使使用文字也会导致同样的问题:字符串被强制转换为枚举 整体 ,它与允许的值之一不匹配,因此返回为空.

但你可以做到

replace value of (/A:AccessControlList/A:AccessControlRecord/@Permissions)[1] 
with for $p in ("DenyCreate","DenyUpdate","DenyDelete") 
     return $p cast as A:AccessPermissions ?

这将使用 XQuery for 来 运行 遍历列表和 return 每个值 一个一个 ,每个都被转换分别到需要的类型。

但我发现将其与外部变量一起使用的唯一方法是动态 SQL。所以这有效,但相当丑陋:

DECLARE @MultipleValues VARCHAR(100)='DenyCreate DenyUpdate DenyDelete';

DECLARE @cmd NVARCHAR(MAX)=
'WITH XMLNAMESPACES ( ''http://www.acme.com/Authorization/2013/01'' AS A )
 UPDATE dbo.Widget
 SET ACL.modify(''replace value of (/A:AccessControlList/A:AccessControlRecord/@Permissions)[1] with for $p in ("' + REPLACE(@MultipleValues,' ','","') + '") return $p cast as A:AccessPermissions ?'')
 FROM dbo.Widget;';

EXEC(@cmd);