Code First 插入、更新和删除存储过程
注意
仅限 EF6 及更高版本 - 此页面中讨论的功能、API 等已引入实体框架 6。 如果使用的是早期版本,则部分或全部信息不适用。
默认情况下,Code First 会将所有实体配置为使用直接表访问来执行插入、更新和删除命令。 从 EF6 开始,你可以将 Code First 模型配置为对模型中的部分或所有实体使用存储过程。
基本实体映射
你可以选择通过 Fluent API 使用存储过程进行插入、更新和删除。
modelBuilder
.Entity<Blog>()
.MapToStoredProcedures();
这样做会导致 Code First 使用一些约定来生成数据库中存储过程的预期形状。
- 三个名为 <type_name>_Insert、<type_name>_Update 和 <type_name>_Delete 的存储过程(例如,Blog_Insert、Blog_Update 和 Blog_Delete)。
- 参数名称对应于属性名称。
注意
如果使用 HasColumnName() 或 Column 特性重命名给定属性的相应列,则此名称用于参数而不是属性名称。
- 插入存储过程将为每个属性提供一个参数,标记为存储生成(标识或计算)的属性除外。 该存储过程应该返回一个结果集,其中每个存储生成的属性都有一列。
- 更新存储过程将为每个属性提供一个参数,标记为“计算”存储生成模式的属性除外。 某些并发标记需要原始值的参数,有关详细信息,请参阅下面的“并发标记”部分。 该存储过程应该返回一个结果集,其中每个计算属性都有一列。
- 删除存储过程应为实体的键值提供一个参数(如果实体具有复合键,则应该提供多个参数)。 此外,删除过程还应该为目标表上的任何独立关联外键(未在实体中声明相应外键属性的关系)提供参数。 某些并发标记需要原始值的参数,有关详细信息,请参阅下面的“并发标记”部分。
使用以下类作为示例:
public class Blog
{
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
}
默认存储过程如下所示:
CREATE PROCEDURE [dbo].[Blog_Insert]
@Name nvarchar(max),
@Url nvarchar(max)
AS
BEGIN
INSERT INTO [dbo].[Blogs] ([Name], [Url])
VALUES (@Name, @Url)
SELECT SCOPE_IDENTITY() AS BlogId
END
CREATE PROCEDURE [dbo].[Blog_Update]
@BlogId int,
@Name nvarchar(max),
@Url nvarchar(max)
AS
UPDATE [dbo].[Blogs]
SET [Name] = @Name, [Url] = @Url
WHERE BlogId = @BlogId;
CREATE PROCEDURE [dbo].[Blog_Delete]
@BlogId int
AS
DELETE FROM [dbo].[Blogs]
WHERE BlogId = @BlogId
替代默认值
你可以替代默认配置的部分或全部值。
你可以更改一个或多个存储过程的名称。 此示例仅重命名更新存储过程。
modelBuilder
.Entity<Blog>()
.MapToStoredProcedures(s =>
s.Update(u => u.HasName("modify_blog")));
此示例重命名所有三个存储过程。
modelBuilder
.Entity<Blog>()
.MapToStoredProcedures(s =>
s.Update(u => u.HasName("modify_blog"))
.Delete(d => d.HasName("delete_blog"))
.Insert(i => i.HasName("insert_blog")));
在这些示例中,调用链接在一起,但你也可以使用 lambda 块语法。
modelBuilder
.Entity<Blog>()
.MapToStoredProcedures(s =>
{
s.Update(u => u.HasName("modify_blog"));
s.Delete(d => d.HasName("delete_blog"));
s.Insert(i => i.HasName("insert_blog"));
});
此示例重命名更新存储过程中的 BlogId 属性的参数。
modelBuilder
.Entity<Blog>()
.MapToStoredProcedures(s =>
s.Update(u => u.Parameter(b => b.BlogId, "blog_id")));
这些调用都是可链接、可组合的。 下面的示例将重命名所有三个存储过程及其参数。
modelBuilder
.Entity<Blog>()
.MapToStoredProcedures(s =>
s.Update(u => u.HasName("modify_blog")
.Parameter(b => b.BlogId, "blog_id")
.Parameter(b => b.Name, "blog_name")
.Parameter(b => b.Url, "blog_url"))
.Delete(d => d.HasName("delete_blog")
.Parameter(b => b.BlogId, "blog_id"))
.Insert(i => i.HasName("insert_blog")
.Parameter(b => b.Name, "blog_name")
.Parameter(b => b.Url, "blog_url")));
你还可以更改结果集中包含数据库生成值的列的名称。
modelBuilder
.Entity<Blog>()
.MapToStoredProcedures(s =>
s.Insert(i => i.Result(b => b.BlogId, "generated_blog_identity")));
CREATE PROCEDURE [dbo].[Blog_Insert]
@Name nvarchar(max),
@Url nvarchar(max)
AS
BEGIN
INSERT INTO [dbo].[Blogs] ([Name], [Url])
VALUES (@Name, @Url)
SELECT SCOPE_IDENTITY() AS generated_blog_id
END
在类中没有外键的关系(独立关联)
当类定义中包含外键属性时,可以像任何其他属性一样重命名相应的参数。 如果某个关系在类中没有外键属性,则默认参数名称为 <navigation_property_name>_<primary_key_name>。
例如,以下类定义将导致存储过程中出现一个用于插入和更新 Post 的 Blog_BlogId 参数。
public class Blog
{
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public Blog Blog { get; set; }
}
替代默认值
通过向 Parameter 方法提供主键属性的路径,可以更改未包含在类中的外键的参数。
modelBuilder
.Entity<Post>()
.MapToStoredProcedures(s =>
s.Insert(i => i.Parameter(p => p.Blog.BlogId, "blog_id")));
如果依赖实体上没有导航属性(即,没有 Post.Blog 属性),可以使用 Association 方法标识关系的另一端,然后配置与每个键属性对应的参数。
modelBuilder
.Entity<Post>()
.MapToStoredProcedures(s =>
s.Insert(i => i.Navigation<Blog>(
b => b.Posts,
c => c.Parameter(b => b.BlogId, "blog_id"))));
并发标记
更新和删除存储过程可能还需要处理并发操作:
- 如果实体包含并发标记,存储过程可以选择提供一个输出参数,用于返回更新/删除的行数(受影响的行数)。 此类参数必须使用 RowsAffectedParameter 方法配置。
默认情况下,EF 使用 ExecuteNonQuery 的返回值来确定受影响的行数。 如果在 sproc 中执行任何导致 ExecuteNonQuery 的返回值在执行结束时不正确(从 EF 的角度来看)的逻辑,则指定 rows affected 输出参数很有用。 - 对于每个并发标记,都会有一个名为 <property_name>_Original 的参数(例如 Timestamp_Original)。 系统会向其传递此属性的原始值 - 从数据库查询时的值。
- 由数据库计算的并发标记(例如时间戳)将只有原始值参数。
- 设置为并发标记的非计算属性还将具有更新过程中新值的参数。 对于新值,将使用已经讨论过的命名约定。 此类标记的一个示例是使用 Blog 的 URL 作为并发标记,新值是必需的,因为该标记可以通过代码更新为新值(与仅由数据库更新的时间戳标记不同)。
下面是使用时间戳并发标记的示例类和更新存储过程。
public class Blog
{
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
[Timestamp]
public byte[] Timestamp { get; set; }
}
CREATE PROCEDURE [dbo].[Blog_Update]
@BlogId int,
@Name nvarchar(max),
@Url nvarchar(max),
@Timestamp_Original rowversion
AS
UPDATE [dbo].[Blogs]
SET [Name] = @Name, [Url] = @Url
WHERE BlogId = @BlogId AND [Timestamp] = @Timestamp_Original
下面是使用非计算并发标记的示例类和更新存储过程。
public class Blog
{
public int BlogId { get; set; }
public string Name { get; set; }
[ConcurrencyCheck]
public string Url { get; set; }
}
CREATE PROCEDURE [dbo].[Blog_Update]
@BlogId int,
@Name nvarchar(max),
@Url nvarchar(max),
@Url_Original nvarchar(max),
AS
UPDATE [dbo].[Blogs]
SET [Name] = @Name, [Url] = @Url
WHERE BlogId = @BlogId AND [Url] = @Url_Original
替代默认值
你可以选择引入 rows affected 参数。
modelBuilder
.Entity<Blog>()
.MapToStoredProcedures(s =>
s.Update(u => u.RowsAffectedParameter("rows_affected")));
对于数据库计算并发标记(仅传递原始值),只能使用标准参数重命名机制重命名原始值的参数。
modelBuilder
.Entity<Blog>()
.MapToStoredProcedures(s =>
s.Update(u => u.Parameter(b => b.Timestamp, "blog_timestamp")));
对于非计算并发标记(同时传递原始值和新值),可以使用 Parameter 的重载,它允许你为每个参数提供一个名称。
modelBuilder
.Entity<Blog>()
.MapToStoredProcedures(s => s.Update(u => u.Parameter(b => b.Url, "blog_url", "blog_original_url")));
多对多关系
我们将在此部分中使用以下类作为示例。
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public List<Tag> Tags { get; set; }
}
public class Tag
{
public int TagId { get; set; }
public string TagName { get; set; }
public List<Post> Posts { get; set; }
}
可以使用以下语法将多对多关系映射到存储过程。
modelBuilder
.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(t => t.Posts)
.MapToStoredProcedures();
如果未提供其他配置,则默认使用以下存储过程形状。
- 两个名为 <type_one><type_two>_Insert 和 <type_one><type_two>_Delete 的存储过程(例如 PostTag_Insert 和 PostTag_Delete)。
- 对于每种类型,参数将为键值。 每个参数的名称为 <type_name>_<property_name>(例如 Post_PostId 和 Tag_TagId)。
下面是插入和更新存储过程的示例。
CREATE PROCEDURE [dbo].[PostTag_Insert]
@Post_PostId int,
@Tag_TagId int
AS
INSERT INTO [dbo].[Post_Tags] (Post_PostId, Tag_TagId)
VALUES (@Post_PostId, @Tag_TagId)
CREATE PROCEDURE [dbo].[PostTag_Delete]
@Post_PostId int,
@Tag_TagId int
AS
DELETE FROM [dbo].[Post_Tags]
WHERE Post_PostId = @Post_PostId AND Tag_TagId = @Tag_TagId
替代默认值
可以采用类似于实体存储过程的方式配置过程和参数名称。
modelBuilder
.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(t => t.Posts)
.MapToStoredProcedures(s =>
s.Insert(i => i.HasName("add_post_tag")
.LeftKeyParameter(p => p.PostId, "post_id")
.RightKeyParameter(t => t.TagId, "tag_id"))
.Delete(d => d.HasName("remove_post_tag")
.LeftKeyParameter(p => p.PostId, "post_id")
.RightKeyParameter(t => t.TagId, "tag_id")));