如何使用一个存储过程保存对象图

Posted

技术标签:

【中文标题】如何使用一个存储过程保存对象图【英文标题】: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
);

问题是如何

【问题讨论】:

【参考方案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 上查看我关于该技术的文章并从那里下载源代码以了解其工作原理。

【讨论】:

以上是关于如何使用一个存储过程保存对象图的主要内容,如果未能解决你的问题,请参考以下文章

如何对视图使用存储过程进行动态查询?

如何同时将视频上传到 s3 为其创建缩略图并使用 nodejs 将其保存到同一存储桶中的另一个文件夹中?

如何在 Oracle toad 中编辑和保存存储过程?

在.net 中使用EF 连接oracle 数据库,如何使用存储过程。求高手指点,help!

如何使用 project-lib 将 csv 文件保存到云对象存储?

如何将 UIImage 保存到文档目录?