请注意:这是我的“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, 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
);
问题是如何?
答案 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
所以我们必须按类型收集所有数据(GrandRecord
,Record
和ChildRecord
),创建ADO.NET DataTables并将它们传递给存储过程。
但是!因为我们在数据库中的表是通过外键GrandRecordId
和RecordId
链接的,所以我们在将对象图转换为单独的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.Id
和source.Id
等于现有ID。
如果表中不存在记录,MERGE会执行INSERT,inserted.Id
等于新ID,source.Id
等于否定身份。
如果源(参数)表中不存在记录,则MERGE执行DELETE,inserted.Id
和source.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上关于该技术的文章,并从那里下载源代码以了解其工作原理。