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

时间:2016-11-02 17:00:02

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

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

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

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

我想在没有 GUID EF 的情况下这样做! :)

假设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 个答案:

答案 0 :(得分: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 DataTables并将它们传递给存储过程。

但是!因为我们在数据库中的表是通过外键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});
}

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

@GrandRecords

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

@Records

+-------------+---------------+------+
|     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和Ids。

因此,“使用正确的外键保存所有三个表”的技术是收集第一个表中的InsertedId - ParamId对,并翻译这些值第二。然后对第二和第三个表格做同样的事情。

  • 如果表中存在记录,MERGE会更新,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上关于该技术的文章,并从那里下载源代码以了解其工作原理。