简体   繁体   English

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

[英]SQL Server : Merge across multiple tables with foreign key

Here's what I am trying to do: basically send XML to SQL Server to update/insert (Merge) my data as a "save" function in my code. 这是我要执行的操作:基本上将XML发送到SQL Server,以将代码更新/插入(合并)数据,作为代码中的“保存”功能。

I have managed to successfully do this if I send one "item" in the XML using the following XML: 如果我使用以下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>

in this stored procedure: 在此存储过程中:

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

When I have multiple records in the XML the @Formula1Id gets set to the last one inserted in the first merge statement so all the Child data in the XML gets merged using this id, meaning all child data belongs to one parent! 当我在XML中有多个记录时, @Formula1Id会被设置为在第一个merge语句中插入的最后一个记录,因此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>

Is there any way to perform this keeping the foreign key relationship correct. 有什么方法可以执行此操作以保持外键关系正确。

Perhaps the Merge statement is the wrong way to go but it seems like the best way to handle a lot of inserts/updates at once. 也许Merge语句是错误的处理方法,但它似乎是一次处理大量插入/更新的最佳方法。

Maybe you could suggest an alternative method - the main criteria is performance as there could be thousands of items to "save" - I have tried to look at SqlBulkCopy but this doesn't seem to handle foreign key relationships very well either... I know I could save to one table at a time but then I lose the ROLLBACK functionality should one part of the "save" go wrong! 也许您可以建议一种替代方法-主要标准是性能,因为可能有成千上万的项目需要“保存”-我尝试查看SqlBulkCopy,但这似乎也不能很好地处理外键关系...我我知道我一次可以保存到一张表,但是如果“保存”的一部分出错,我将失去ROLLBACK功能!

Any help/suggestions are greatly appreciated. 任何帮助/建议,我们将不胜感激。 Thanks in advance. 提前致谢。

Try using following solution (it's not tested; I assumed that you can have many "Formula1" elements; you should carefully read my notes): 尝试使用以下解决方案(未经测试;我假设您可以有很多“ 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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM