【问题标题】:SQL Server : Merge across multiple tables with foreign keySQL Server:使用外键合并多个表
【发布时间】:2013-12-24 23:18:04
【问题描述】:

这就是我想要做的:基本上将 XML 发送到 SQL Server 以更新/插入(合并)我的数据作为我的代码中的“保存”函数。

如果我使用以下 XML 在 XML 中发送一个“项目”,我已经成功地做到了这一点:

<root>
<Formula1>
<M_iFormula1Id>0</M_iFormula1Id>
<M_bDataInUse>0</M_bDataInUse>
<M_bActive>1</M_bActive>
<M_lstItem>
    <M_iItemId>0</M_iItemId>
    <M_iItemTypeId>1</M_iItemTypeId>
    <M_sItemValue>German</M_sItemValue>
    <M_iRaceId>1</M_iRaceId>
    <M_iDriverId>50</M_iDriverId>
</M_lstItem>
</Formula1>
</root>

在这个存储过程中:

ALTER PROCEDURE [dbo].[spFormula1_Save]
    @Formula1Xml xml--Formula1 as xml
AS
BEGIN 
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

    IF DATALENGTH(@Formula1Xml) = 0
        RETURN 0

BEGIN TRANSACTION
BEGIN TRY

    DECLARE @hDoc INT
    EXEC sp_xml_preparedocument @hDoc OUTPUT, @Formula1Xml

-------------------
--Formula1 Table
-------------------
DECLARE @Formula1Id bigint = 0;

    MERGE INTO Formula1 AS tab
    USING 
    OPENXML (@hDoc, '/root/Formula1', 2)
     WITH (
        M_iFormula1Id bigint,
        M_bDataInUse bit,
        M_bActive bit
        ) AS [xml]
    ON (tab.Formula1Id = [xml].[M_iFormula1Id])
    WHEN MATCHED THEN UPDATE SET tab.DataInUse = [xml].M_bDataInUse,
                                 tab.Active = [xml].M_bActive,
                                 @Formula1Id = [xml].M_iFormula1Id
    WHEN NOT MATCHED THEN INSERT (DataInUse,
                                  Active)
                                 VALUES([xml].M_bDataInUse,
                                        [xml].M_bActive
                                        );

IF(@Formula1Id = 0)--then we haven''t updated so get inserted rowid
BEGIN
 SET @Formula1Id = SCOPE_IDENTITY();--get the inserted identity
END



-------------------
--Formula1Item Table
-------------------
    MERGE INTO Formula1Item AS tab
    USING 
    OPENXML (@hDoc, '/root/Formula1/M_lstItem', 2)
     WITH (
        M_iItemId bigint,
        M_iItemTypeId bit,
        M_sItemValue varchar(1000),
        M_iRaceId int,
        M_iDriverId int
        ) AS [xml]
    ON (tab.ItemId = [xml].M_iItemId)
    WHEN MATCHED THEN UPDATE SET tab.ItemTypeId = [xml].M_iItemTypeId,
                                 tab.ItemValue = [xml].M_sItemValue,
                                 tab.RaceId = [xml].M_iRaceId,
                                 tab.DriverId = [xml].M_iDriverId
    WHEN NOT MATCHED THEN INSERT (Formula1Id,
                                  ItemTypeId,
                                  ItemValue,
                                  RaceId,
                                  DriverId)
                                 VALUES(@Formula1Id,
                                        [xml].M_iItemTypeId,
                                        [xml].M_sItemValue,
                                        [xml].M_iRaceId,
                                        [xml].M_iDriverId
                                        );   
 COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    ROLLBACK TRANSACTION;
END CATCH;  

END

当我在 XML 中有多个记录时,@Formula1Id 被设置为插入第一个合并语句中的最后一个记录,因此 XML 中的所有子数据都使用此 ID 合并,这意味着所有子数据都属于一个父级!

<root>
<Formula1>
<M_iFormula1Id>0</M_iFormula1Id>
<M_bDataInUse>0</M_bDataInUse>
<M_bActive>1</M_bActive>
<M_lstItem>
    <M_iItemId>0</M_iItemId>
    <M_iItemTypeId>1</M_iItemTypeId>
    <M_sItemValue>German</M_sItemValue>
    <M_iRaceId>1</M_iRaceId>
    <M_iDriverId>50</M_iDriverId>
</M_lstItem>
</Formula1>
<Formula1>
<M_iFormula1Id>0</M_iFormula1Id>
<M_bDataInUse>0</M_bDataInUse>
<M_bActive>1</M_bActive>
<M_lstItem>
    <M_iItemId>0</M_iItemId>
    <M_iItemTypeId>1</M_iItemTypeId>
    <M_sItemValue>French</M_sItemValue>
    <M_iRaceId>2</M_iRaceId>
    <M_iDriverId>50</M_iDriverId>
</M_lstItem>
</Formula1>
</root>

有什么方法可以保持外键关系正确。

也许 Merge 语句是错误的方式,但它似乎是一次处理大量插入/更新的最佳方式。

也许您可以建议一种替代方法-主要标准是性能,因为可能有数千个要“保存”的项目-我尝试查看 SqlBulkCopy 但这似乎也不能很好地处理外键关系。 .. 我知道我可以一次保存到一张表,但是如果“保存”的一部分出错,我就会失去 ROLLBACK 功能!

非常感谢任何帮助/建议。提前致谢。

【问题讨论】:

    标签: sql sql-server xml merge


    【解决方案1】:

    尝试使用以下解决方案(未经测试;我假设您可以拥有许多“Formula1”元素;您应该仔细阅读我的笔记):

    ALTER PROCEDURE [dbo].[spFormula1_Save]
        @Formula1Xml xml--Formula1 as xml
    AS
    BEGIN 
        -- SET NOCOUNT ON added to prevent extra result sets from
        -- interfering with SELECT statements.
        SET NOCOUNT, XACT_ABORT ON;
    
        IF DATALENGTH(@Formula1Xml) = 0
            RETURN 0
    
    ------------------------
    --Xml shredding
    ------------------------
    -- I prefer using the new XML methods (nodes, value, exist) instead of sp_xml_preparedocument + OPENXML 
    -- because you may get memory leaks if we don't use sp_xml_removedocument
    DECLARE @Formula1_Table TABLE
    (
        M_iFormula1Id bigint,
        Rnk bigint primary key, -- It's used to unique identify the old and the new rows
        M_bDataInUse bit,
        M_bActive bit
    );
    INSERT  @Formula1_Table (M_iFormula1Id, Rnk, M_bDataInUse, M_bActive)
    SELECT  x.XmlCol.value('(M_iFormula1Id)[1]', 'BIGINT') AS M_iFormula1Id,
            ROW_NUMBER() OVER(ORDER BY x.XmlCol) AS Rnk, -- It's used to unique identify the old and the new rows
            x.XmlCol.value('(M_bDataInUse)[1]', 'BIT') AS M_bDataInUse,
            x.XmlCol.value('(M_bActive)[1]', 'BIT') AS M_bActive
    FROM    @Formula1Xml.nodes('/root/Formula1') x(XmlCol);
    
    DECLARE @Formula1_M_lstItem_Table TABLE
    (
        M_iFormula1Id bigint,
        Rnk bigint, -- It's used to unique identify new "Formula1" rows (those rows having M_iFormula1Id=0)
        M_iItemId bigint,
        M_iItemTypeId bit,
        M_sItemValue varchar(1000),
        M_iRaceId int,
        M_iDriverId int
    );
    INSERT  @Formula1_M_lstItem_Table 
    (
        M_iFormula1Id,
        Rnk, 
        M_iItemId,
        M_iItemTypeId,
        M_sItemValue,
        M_iRaceId,
        M_iDriverId
    )
    SELECT  /*x.XmlCol.value('(M_iFormula1Id)[1]', 'BIGINT')*/ 
            -- At this moment we insert only nulls
            NULL AS M_iFormula1Id,
            DENSE_RANK() OVER(ORDER BY x.XmlCol) AS Rnk, -- It's used to unique identify new and old "Formula1" rows
            y.XmlCol.value('(M_iItemId)[1]', 'BIGINT') AS M_iItemId,
            y.XmlCol.value('(M_iItemTypeId)[1]', 'BIT') AS M_iItemTypeId,
            y.XmlCol.value('(M_sItemValue)[1]', 'VARCHAR(1000)') AS M_sItemValue,
            y.XmlCol.value('(M_iRaceId)[1]', 'INT') AS M_iRaceId,
            y.XmlCol.value('(M_iDriverId)[1]', 'INT') AS M_iDriverId
    FROM    @Formula1Xml.nodes('/root/Formula1') x(XmlCol)
    CROSS APPLY x.XmlCol.nodes('M_lstItem') y(XmlCol);
    ------------------------
    --End of Xml shredding
    ------------------------
    
    
    BEGIN TRANSACTION
    BEGIN TRY
    
    -------------------
    --Formula1 Table
    -------------------
    DECLARE @Merged_Rows TABLE
    (
        Merge_Action nvarchar(10) not null,
        Rnk bigint not null,
        M_iFormula1Id bigint -- The old id's and the new inserted id's.
    );
    DECLARE @Formula1Id bigint = 0;
    
        MERGE INTO Formula1 WITH(HOLDLOCK) AS tab -- To prevent race condition. http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
        USING @Formula1_Table AS [xml]
        ON (tab.Formula1Id = [xml].[M_iFormula1Id])
        WHEN MATCHED THEN UPDATE SET tab.DataInUse = [xml].M_bDataInUse,
                                     tab.Active = [xml].M_bActive
                                     -- We no more need this line because of OUTPUT clause
                                     -- @Formula1Id = [xml].M_iFormula1Id 
        WHEN NOT MATCHED THEN INSERT (DataInUse,
                                      Active)
                                     VALUES([xml].M_bDataInUse,
                                            [xml].M_bActive
                                            )
        -- This OUTPUT clause will insert into @Merged_Rows the Rnk and the new M_iFormula1Id for every /root/Formula1 element  
        -- http://msdn.microsoft.com/en-us/library/ms177564.aspx
        OUTPUT $action, [xml].Rnk, inserted.M_iFormula1Id INTO @Merged_Rows (Merge_Action, Rnk, M_iFormula1Id);
    
    -- This is replaced by previous OUTPUT clause
    /*
    IF(@Formula1Id = 0)--then we haven''t updated so get inserted rowid
    BEGIN
     SET @Formula1Id = SCOPE_IDENTITY();--get the inserted identity
    END
    */
    
    -- At this moment we replace all previously inserted NULLs with the real (old and new) id's
    UPDATE  x
    SET     M_iFormula1Id = y.M_iFormula1Id
    FROM    @Formula1_M_lstItem_Table x
    JOIN    @Merged_Rows y ON x.Rnk = y.Rnk;
    
    -------------------
    --Formula1Item Table
    -------------------
        MERGE INTO Formula1Item AS tab
        USING @Formula1_M_lstItem_Table AS [xml]
        ON (tab.ItemId = [xml].M_iItemId) 
        -- Maybe you should need also this join predicate (tab.M_iFormula1Id = [xml].M_iFormula1Id)
        WHEN MATCHED THEN UPDATE SET tab.ItemTypeId = [xml].M_iItemTypeId,
                                     tab.ItemValue = [xml].M_sItemValue,
                                     tab.RaceId = [xml].M_iRaceId,
                                     tab.DriverId = [xml].M_iDriverId
        WHEN NOT MATCHED THEN INSERT (Formula1Id,
                                      ItemTypeId,
                                      ItemValue,
                                      RaceId,
                                      DriverId)
                                     VALUES([xml].M_iFormula1Id,
                                            [xml].M_iItemTypeId,
                                            [xml].M_sItemValue,
                                            [xml].M_iRaceId,
                                            [xml].M_iDriverId
                                            );   
     COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION;
        -- The caller should be informed when an error / exception is catched
        -- THROW
    END CATCH;  
    
    END
    

    【讨论】:

    • 您好 Bogdan,非常感谢您对此的帮助 - 我设法对其进行了调整以使其成功运行。我刚刚使用大约 480 个全新(插入)Formula1 节点进行了测试,每个节点在 M_lstItem 节点中包含 2 个项目节点。完成时间为 1m28s。你觉得这次演出怎么样?考虑到可能有 1000 个 Formula1 节点,但在我没有什么可比较的时间之前从未做过这样的事情,这对我来说似乎很慢。我还应该提到我将添加到数据中,因此它将最终成为具有 4 个子关系表的 Formula1 节点
    • 同一个SP的老版本性能如何?如果您认为存在性能问题,那么您应该发布执行计划(实际执行计划:SSMS > Include Actual Execution Plan / Ctrl+M)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-01-24
    • 2016-04-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多