【问题标题】:How to save Object Graphs with one stored procedure如何使用一个存储过程保存对象图
【发布时间】:2016-11-02 17:00:01
【问题描述】:

请注意:这是我的“share your knowledge”问题!

我有一个 Web 客户端,它以 JSON 格式向我的 ASP.NET 应用程序发送数据。数据是对象图或对象图的集合。

使用 Web Api 控制器将数据反序列化为 C# 对象图。我想用 ADO.NET 和 one 存储过程保存那个 C# 对象图。

我想在没有 GUIDEF 的情况下执行此操作! :)

假设 Web 客户端发送由三个对象组成的对象图:

  • GrandRecord
  • Record
  • ChildRecord

让它成为GrandRecords 的集合,其中:

  • 每个GrandRecord 都有一个Records 的集合
  • 并且每个Record 都有一个ChildRecords 的集合
  • Id 值是整数,由数据库自动生成
  • 虽然对象未保存在数据库中,但 Id 的值 = 0

这是对象图集合(或对象图)的示例:

                      Id, Name
GrandRecord           1,  (A)
    Record            |-- 2, (A)A
        ChildRecord       |-- 3, (A)Aa
        ChildRecord       |-- 0, (A)Ab
    Record            |-- 0, (A)B
        ChildRecord       |-- 0, (A)Ba
        ChildRecord       |-- 0, (A)Bb
GrandRecord           0,  (B)
    Record            |-- 0, (B)A

或者同样的JSON格式:

grandRecords: [
    {
        id: 1,
        name: "(A)",
        records: [
            {
                id: 2,
                name: "(A)A",
                childRecords: [
                    {
                        id: 3,
                        name: "(A)Aa",
                    },
                    {
                        id: 0,
                        name: "(A)b",
                    },
                ]
            },
            {
                id: 0,
                name: "(A)B",
                childRecords: [
                    {
                        id: 0,
                        name: "(A)Ba",
                    },
                    {
                        id: 0,
                        name: "(A)Bb",
                    },
                ]
            }        
        ]        
    },
    {
        id: 0,
        name: "(B)",
        records: [
            {
                id: 0,
                name: "(B)A",
                childRecords: []
            }
        ]
    }
]

在 ASP.NET 控制器的 Web 服务器上,上述 JSON 字符串被反序列化为三个类的对象图:

public class GrandRecord
{
    public  Int32          Id       { get; set; }
    public  String         Name     { get; set; }
    public  IList<Record>  Records  { get; set; }
}

public class Record
{
    public  Int32               Id             { get; set; }
    public  Int32               GrandRecordId  { get; set; }        
    public  String              Name           { get; set; }
    public  IList<ChildRecord>  ChildRecords   { get; set; }
}

public class ChildRecord
{
    public  Int32   Id        { get; set; }
    public  Int32   RecordId  { get; set; }
    public  String  Name      { get; set; }
}

现在必须用一个存储过程将对象图保存到三个数据库表中:

create table dbo.GrandRecords
(
    Id    int          not null  identity  primary key clustered,
    Name  varchar(30)  not null
);

create table dbo.Records
(
    Id             int          not null  identity  primary key clustered,
    GrandRecordId  int          not null  foreign key (GrandRecordId) references dbo.GrandRecords (Id) on delete cascade,
    Name           varchar(30)  not null
);

create table dbo.ChildRecords
(
    Id        int          not null  identity  primary key clustered,
    RecordId  int          not null  foreign key (RecordId) references dbo.Records (Id) on delete cascade,
    Name      varchar(30)  not null
);

问题是如何

【问题讨论】:

    标签: c# sql-server stored-procedures orm ado.net


    【解决方案1】:

    当然,存储过程的表值参数是答案的一部分!

    拥有这些用户定义的表类型:

    create type dbo.GrandRecordTableType as table
    (
        Id    int          not null   primary key clustered,
        Name  varchar(30)  not null    
    );
    
    create type dbo.RecordTableType as table
    (
        Id             int          not null   primary key clustered,
        GrandRecordId  int          not null   ,
        Name           varchar(30)  not null
    
    );
    
    create type dbo.ChildRecordTableType as table
    (
        Id        int          not null   primary key clustered,
        RecordId  int          not null   ,
        Name      varchar(30)  not null    
    );
    

    保存上述对象图的存储过程以:

    create procedure dbo.SaveGrandRecords
        @GrandRecords  dbo.GrandRecordTableType  readonly,
        @Records       dbo.RecordTableType       readonly,
        @ChildRecords  dbo.ChildRecordTableType  readonly
    as
    

    所以我们必须按类型(GrandRecordRecordChildRecord)收集所有数据,创建 ADO.NET 数据表并将它们传递给存储过程。

    但是!因为我们在数据库中的表是通过外键 GrandRecordIdRecordId 链接的,所以我们必须在将对象图转换为单独的 DataTable 时以某种方式保持该链接。

    更重要的是,新对象的身份必须是唯一的!否则,我们无法区分 GrandRecord (A) 记录和 GrandRecord (B) 记录。

    但是,正如我们从问题中所记得的那样,新对象的 Id = 0!

    为了解决这个问题,让我们将不断增加的负标识分配给对象 ID,如果它们等于 0:

    var id = int.MinValue;
    
    foreach (var grandRecord in grandRecords)
    {
        if (grandRecord.Id == 0)
            grandRecord.Id = id++;
    
        foreach (var record in grandRecord.Records)
        {
            if (record.Id == 0)
                record.Id = id++;
    
            record.GrandRecordId = grandRecord.Id;
    
            foreach (var childRecord in record.ChildRecords)
            {
                if (childRecord.Id == 0)
                    childRecord.Id = id++;
    
                childRecord.RecordId = record.Id;
            }
        }
    }
    

    现在是填充数据表的时候了。

    例如,这里是如何准备一个带有Records数据的DataTable:

    var recordTable = new DataTable("RecordTableType");
    
    recordTable.Columns.Add( "Id"            , typeof( Int32  ));
    recordTable.Columns.Add( "GrandRecordId" , typeof( Int32  ));
    recordTable.Columns.Add( "Name"          , typeof( String ));
    
    
    var records = grandRecords.SelectMany(gr => gr.Records);
    
    foreach(var record in records) 
    {
        table.Rows.Add(new object[] {record.Id, record.GrandRecordId, record.Name});
    }
    

    所以在DataTables准备好之后,存储过程将在表值参数中接收以下数据:

    @GrandRecords

    +-------------+------+
    |     Id      | Name |
    +-------------+------+
    |           1 | (A)  |
    | -2147483648 | (B)  |
    +-------------+------+
    

    @记录

    +-------------+---------------+------+
    |     Id      | GrandRecordId | Name |
    +-------------+---------------+------+
    |           2 |             1 | (A)A |
    | -2147483647 |             1 | (A)B |
    | -2147483646 |   -2147483648 | (B)A |
    +-------------+---------------+------+
    

    @ChildRecords

    +-------------+-------------+-------+
    |     Id      |  RecordId   | Name  |
    +-------------+-------------+-------+
    |           3 |           2 | (A)Aa |
    | -2147483645 |           2 | (A)Ab |
    | -2147483644 | -2147483647 | (A)Ba |
    | -2147483643 | -2147483647 | (A)Bb |
    +-------------+-------------+-------+
    

    对象图保存技术

    为了更新现有数据、插入新数据和删除旧数据,SQL Server 使用 MERGE 语句。

    MERGE 语句具有 OUTPUT 子句。 MERGE 语句中的 OUTPUT 可以收集刚刚插入的 Id 以及来自源(参数)表的 Id。

    因此,“使用正确的外键保存所有三个表”的技术是从第一个表中收集 InsertedId - ParamId 对,然后为第二个表翻译这些值。然后对第二个和第三个表执行相同的操作。

    • 如果表中存在记录,则 MERGE 执行 UPDATE,inserted.Idsource.Id 等于现有 Id。

    • 如果表中不存在记录,则 MERGE 执行 INSERT,inserted.Id 等于新 ID,source.Id 等于否定标识。

    • 如果源(参数)表中不存在记录,则 MERGE 执行 DELETE,inserted.Idsource.Id 等于 NULL,但 deleted.Id 具有已删除记录的 ID。

    这是保存我们的对象图的存储过程:

    create procedure dbo.SaveGrandRecords
        @GrandRecords  dbo.GrandRecordTableType  readonly,
        @Records       dbo.RecordTableType       readonly,
        @ChildRecords  dbo.ChildRecordTableType  readonly
    as
    begin
        set nocount on;
    
        declare @GrandRecordIds table (  -- translation table
            InsertedId  int  primary key, 
            ParamId     int  unique
        );
    
        declare @RecordIds table (       -- translation table
            InsertedId  int     primary key, 
            ParamId     int     unique, 
            [Action]    nvarchar(10)
        );
    
        -- save GrandRecords 
    
        merge into dbo.GrandRecords as target
            using 
            (
                select Id, Name from @GrandRecords
            ) 
            as source on source.Id = target.Id
    
        when matched then
            update set                
                Name = source.Name        
    
        when not matched by target then                                                         
            insert ( Name )
            values ( source.Name )
    
        output            -- collecting translation Ids
            inserted.Id,
            source.Id
        into @GrandRecordIds ( 
            InsertedId , 
            ParamId    );
    
    
        -- save Records 
    
        merge into dbo.Records as target
        using 
        (
            select
                Id             , 
                GrandRecordId  =  ids.InsertedId,   -- Id translation target
                Name    
            from
                @Records r
                inner join @GrandRecordIds ids 
                    on ids.ParamId = r.GrandRecordId -- Id translation source
        ) 
        as source on source.Id = target.Id
    
        when matched then
            update set
                GrandRecordId  =  source.GrandRecordId, 
                Name           =  source.Name    
    
        when not matched by target then                                                         
            insert (    
                GrandRecordId , 
                Name          )
            values (
                source.GrandRecordId , 
                source.Name          )
    
        when not matched by source 
            and target.GrandRecordId in (select InsertedId from @GrandRecordIds) then
               delete
    
        output                 -- collecting translation Ids
            isnull(inserted.Id, deleted.Id),
            isnull(source.Id, deleted.Id), 
            $action
        into @RecordIds (
            InsertedId  , 
            ParamId     , 
            [Action]    );
    
    
        delete from @RecordIds where [Action] = 'DELETE';
    
    
        -- save ChildRecords
    
        merge into dbo.ChildRecords as target
            using 
            (
                select
                    Id        ,
                    RecordId  =  ids.InsertedId,    -- Id translation target
                    Name        
                from
                    @ChildRecords cr
                    inner join @RecordIds ids 
                        on ids.ParamId = cr.RecordId -- Id translation source
            ) 
            as source on source.Id = target.Id
    
        when matched then
            update set
                RecordId = source.RecordId , 
                Name     = source.Name
    
        when not matched by target then
            insert (    
                RecordId , 
                Name     )
            values (
                source.RecordId , 
                source.Name     )
    
        when not matched by source and target.RecordId in (select InsertedId from @RecordIds) then
            delete;
    end;
    

    重要提示

    在 MERGE 语句中,源表和目标表
    必须在其连接列上有聚集索引!
    这可以防止死锁并保证插入顺序。

    连接列在 MERGE 语句的as source on source.Id = target.Id 行中。

    这就是为什么上面的用户定义表类型在其定义中有primary key clustered

    这就是为什么负恒等式不断增加并以 MinValue 开头的原因。


    www.codeproject.com 上查看我关于该技术的文章并从那里下载源代码以了解其工作原理。

    【讨论】:

      猜你喜欢
      • 2012-12-06
      • 2010-11-14
      • 2011-05-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-09-17
      相关资源
      最近更新 更多