如何使用一个存储过程保存对象图
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# 对象图。
我想在没有 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】:当然,存储过程的表值参数是答案的一部分!
拥有这些用户定义的表类型:
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 数据表并将它们传递给存储过程。
但是!因为我们在数据库中的表是通过外键 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);
所以在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.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 上查看我关于该技术的文章并从那里下载源代码以了解其工作原理。
【讨论】:
以上是关于如何使用一个存储过程保存对象图的主要内容,如果未能解决你的问题,请参考以下文章
如何同时将视频上传到 s3 为其创建缩略图并使用 nodejs 将其保存到同一存储桶中的另一个文件夹中?
在.net 中使用EF 连接oracle 数据库,如何使用存储过程。求高手指点,help!