【问题标题】:SQL Merge a List of Parent Child dataSQL合并父子数据列表
【发布时间】:2015-03-11 17:05:00
【问题描述】:

我有一个要作为 XML 传递给存储过程的数据列表。数据是Widget 的列表,小部件包含WidgetItem 的列表(父子数据)。我想做一个基于ParentIDWidget 子集的MERGEParentID 的一些数据已更新,一些已被删除(因此从 xml 中丢失)并且一些数据是新的。

更新的数据永远不需要更新子数据,因为Widget记录只能调整,不能调整其中的项目(子数据)。插入总是会有一个或多个子记录(WidgetItems)。

我似乎无法弄清楚如何在 MERGE 中执行此操作,因为与处理数据层中的合并相比,这似乎是最好的方法。

这是我到目前为止所拥有的……我在卡住的地方放了一个 cmets:

CREATE PROCEDURE dbo.pWidgetsMerge
    @Widgets XML
AS

/*
Assumed XML input @Widgets xml:
<Widgets>
    <Widget>
        <WidgetID>
        <ParentID>
        <StartDate>
        <EndDate>
        <Details>
            <WidgetDetailItem>
                <WidgetDetailItemID>
                <WidgetID>
                <SomeID>                
                <SomeData>
*/

MERGE
    [dbo].[Widget] as w  
USING
    (
        SELECT
            'WidgetID' = P.value('WidgetID[1]', 'INT'),
            'ParentID' = P.value('ParentID[1]', 'INT'),
            'StartDate' = P.value('EffectiveStartDate[1]', 'DATETIME'),
            'EndDate' = P.value('EffectiveEndDate[1]', 'DATETIME')
        FROM
            @Widgets.nodes('/Widgets/Widget') PROPERTYFEED(P)
    ) 
    AS xmlIn
    (
        [WidgetID],
        [StartDate],
        [EndDate]
    )
    ON
        w.[WidgetID] = xmlIn.[WidgetID]
    WHEN
        NOT MATCHED
    THEN
        INSERT 
        (
            [ParentID],
            [StartDate],
            [EndDate]
        ) 
        VALUES
        (
            xmlIn.[ParentID],
            xmlIn.[StartDate],
            xmlIn.[EndDate]
        )


        /*STUCK HERE: After the insert, need to put in the child
            records into a new table [WidgetItems].  Maybe it's another
            operation outside of the merge?*/

    WHEN
        MATCHED AND (
            (w.[StartDate] <> xmlIn.[StartDate]) OR 
            (w.[EndDate] <> xmlIn.[EndDate]))
    THEN
        UPDATE SET
            w.[StartDate] = xmlIn.[StartDate],
            w.[EndDate] = xmlIn.[EndDate]
    WHEN
        NOT MATCHED BY SOURCE AND w.[ParentID] = xmlIn.[ParentID]
    THEN
        UPDATE SET
            w.[DeletedDate] = GETDATE()

另外,如果我遇到了这个错误,我们将不胜感激,或者我确实需要在数据层处理这个问题。

【问题讨论】:

    标签: sql sql-server merge parent-child master-detail


    【解决方案1】:

    以下是可以回答您问题的更新代码。我添加了 cmets 来解释发生了什么。希望这是有道理的。

    正如您所说,ParentID 对于传入的所有小部件都是相同的,因此我将其视为参数而不是 XML 的元素

    DECLARE @ParentID INT = 1
    
    DECLARE @Widgets AS XML = 
    N'<Widgets>
        <Widget>
            <WidgetID />
            <StartDate />
            <EndDate />
            <Details>
                <WidgetDetailItem>
                    <WidgetDetailItemID></WidgetDetailItemID>
                    <WidgetID/>
                    <SomeID>4</SomeID>             
                    <SomeData/>
                </WidgetDetailItem>
                <WidgetDetailItem>
                    <WidgetDetailItemID></WidgetDetailItemID>
                    <WidgetID/>
                    <SomeID>323</SomeID>             
                    <SomeData/>
                </WidgetDetailItem>
                <WidgetDetailItem>
                    <WidgetDetailItemID></WidgetDetailItemID>
                    <WidgetID/>
                    <SomeID>1</SomeID>            
                    <SomeData/>
                </WidgetDetailItem>
            </Details>
        </Widget>
        <Widget>
            <WidgetID>10</WidgetID>
            <StartDate>January 1, 2015</StartDate>
            <EndDate />
            <Details>
                <WidgetDetailItem>
                    <WidgetDetailItemID></WidgetDetailItemID>
                    <WidgetID/>
                    <SomeID>4</SomeID>         
                    <SomeData/>
                </WidgetDetailItem>
                <WidgetDetailItem>
                    <WidgetDetailItemID></WidgetDetailItemID>
                    <WidgetID/>
                    <SomeID>99</SomeID>         
                    <SomeData/>
                </WidgetDetailItem>
                <WidgetDetailItem>
                    <WidgetDetailItemID></WidgetDetailItemID>
                    <WidgetID/>
                    <SomeID>6</SomeID>            
                    <SomeData/>
                </WidgetDetailItem>
            </Details>
        </Widget>
    </Widgets>';
    
    --Used to hold the pseudoID -> WidgetID relationship for inserting the details
    DECLARE @WidgetIds AS TABLE ([Action] varchar(10), PseudoID INT, WidgetID INT);
    
    ; 
    --Use a CTE of the subset of data to be more performant. If we just went straight to the 
    --merge we'd be operating on the entire table and that can have some major performance hits
    WITH T AS (
                  SELECT 
                         w.* 
                  FROM
                         [dbo].[Widget] as w 
                  WHERE
                         w.[ParentID] = @ParentID
    )
    MERGE INTO T 
    USING (
            SELECT
                --Generate a pseudoid based on the order of the Widget elements so that we have some way of 
                --linking the detail records to the master
                row_number() OVER(ORDER BY PROPERTYFEED.P) PseudoID,
                'WidgetID' = P.value('WidgetID[1]', 'INT'),
                'ParentID' = @ParentID,
                'StartDate' = P.value('StartDate[1]', 'DATETIME'),
                'EndDate' = P.value('EndDate[1]', 'DATETIME')
            FROM
                @Widgets.nodes('/Widgets/Widget') PROPERTYFEED(P)
        ) 
        AS xmlIn
        (
               [PseudoID],
            [WidgetID],
            [ParentID],
            [StartDate],
            [EndDate]
        )
        ON
            T.[WidgetID] = xmlIn.[WidgetID]
        WHEN
            NOT MATCHED
        THEN
            INSERT 
            (
                [ParentID],
                [StartDate],
                [EndDate]
            ) 
            VALUES
            (
                xmlIn.[ParentID],
                xmlIn.[StartDate],
                xmlIn.[EndDate]
            )
        WHEN
            MATCHED AND (
                (T.[StartDate] <> xmlIn.[StartDate]) OR 
                (T.[EndDate] <> xmlIn.[EndDate]))
        THEN
            UPDATE SET
                T.[StartDate] = xmlIn.[StartDate],
                T.[EndDate] = xmlIn.[EndDate]
        WHEN
            NOT MATCHED BY SOURCE AND T.[DeletedDate] IS NULL 
        THEN
            UPDATE SET
                T.[DeletedDate] = GETDATE()         
    OUTPUT  $action, xmlIn.PseudoID, INSERTED.WidgetID INTO @WidgetIds
    
    ;
    
    --This is some magic to generate a temp table of numbers from 1 to COUNT(Widget)
    --This is so we can reference the parent Widget row in the same order as the pseudoid generated above
    --http://stackoverflow.com/a/1134379/4375845
    ;WITH Total(TotalWidgets) AS (SELECT COUNT(1) TotalWidgets FROM @Widgets.nodes('/Widgets/Widget') PROPERTYFEED(P))
           , Numbers(Num) as (
                  SELECT 1 AS Num
                  UNION ALL 
                  SELECT Num+1 
                  FROM Numbers
                  JOIN Total t ON 1 = 1
                  WHERE Num < t.TotalWidgets )
    INSERT INTO WidgetDetailItem (WidgetID,SomeID,SomeData)
    SELECT 
           w.WidgetID
           ,Details.SomeID
           ,Details.SomeData
    FROM 
        (SELECT 
            P.value('WidgetDetailItemID[1]','int')  WidgetDetailItemID           
            , P.value('SomeID[1]','int') SomeID
            , P.value('SomeData[1]','varchar(5)') SomeData
            , n.Num AS PsuedoID
        FROM Numbers n
        --This is what gives us our pseudo ID to link to the row_number() function from the first merge statement
        CROSS APPLY @Widgets.nodes('/Widgets/Widget[sql:column("n.Num")]/Details/WidgetDetailItem') AS M(P)
        ) Details
    JOIN @WidgetIds w on Details.PsuedoID = w.PseudoID
    WHERE w.Action = 'INSERT' --We only want inserts by your spec
    
    SELECT * FROM Widget;
    SELECT * FROM WidgetDetailItem;
    

    【讨论】:

      【解决方案2】:

      我已将传入的 XML 反序列化为一个表。这样就有机会验证 XML 字符串中的数据。

      第 1 项:新的反序列化表将允许一种简单的方法来过滤哪些数据包含在 MERGE 中。

      第 2 项:将数据插入子表必须在单独的调用中进行。 MERGE 只能处理一个表上的 crud(创建更新删除)操作。

      注意:MERGE 将使用 DESTINATION 表中的所有记录。因此,当您与 Source 不匹配时,这将作用于表中未包含的所有记录。

      【讨论】:

      • 自从您阅读问题以来,我可能一直在进行大量编辑,但我认为以下内容解决了对所有记录采取行动的部分(仅影响删除):WHEN NOT MATCHED BY SOURCE AND w.[ParentID] = xmlIn.[ParentID] THEN UPDATE SET w.[DeletedDate] = GETDATE()
      • 不是说在数据库表中不匹配,数据库表的父ID必须匹配一个XML父ID吗?如果我没看错,那么 MATCHED BY Source 将不会触发。我在 XML 中为应该从数据库中删除的记录添加了一个标志。
      • 是的,如果NOT MATCHEDParentID 不匹配,删除操作将被忽略。至少这是我希望的,因为我只想将删除与基于ParentID 的数据子集进行比较。
      • 但看起来情况并非如此。不在 XML 文件中并且具有匹配的 ParentIDs
      • 传入 XML 中的每个 Widget 记录都将具有相同的 ParentID
      猜你喜欢
      • 2017-01-28
      • 2019-05-09
      • 1970-01-01
      • 2013-12-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多