SQL Server:使用外键合并多个表

时间:2013-12-07 16:26:16

标签: sql sql-server xml merge

以下是我要做的事情:基本上将XML发送到SQL Server,以便在我的代码中将我的数据更新/插入(合并)为“保存”功能。

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

<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中的所有Child数据都使用此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功能!

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

1 个答案:

答案 0 :(得分:0)

尝试使用以下解决方案(未经过测试;我认为您可以拥有许多“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