注释
EF6 及更高版本 - 实体框架 6 中介绍了本页中讨论的功能、API 等。 如果使用早期版本,某些或全部信息不适用。
默认情况下,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 属性重命名给定属性的列,则此名称用于参数而不是属性名称。
- 插入存储过程 将为每个属性提供参数,标记为存储生成的属性(标识或计算)除外。 存储过程应返回一个结果集,其中包含每个存储生成的属性的列。
- 除了标记为“Computed”的存储模式,更新存储过程将为每个属性设置一个参数。 某些并发令牌需要原始值的参数,有关详细信息,请参阅下面的 并发令牌 部分。 存储过程应返回一个结果集,其中包含每个计算属性的列。
- 删除存储过程 应具有实体键值的参数(如果实体具有复合键,则为多个参数)。 此外,删除过程还应具有目标表上任何独立关联外键的参数(实体中未声明相应的外键属性的关系)。 某些并发令牌需要原始值的参数,有关详细信息,请参阅下面的 并发令牌 部分。
使用下列类作为示例:
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>。
例如,以下类定义将导致存储过程中需要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 的角度来看),则指定受影响的输出参数非常有用。 - 对于每个并发令牌,将有一个名为 <property_name>的参数_Original (例如,Timestamp_Original)。 这将传递此属性的原始值 – 从数据库查询时的值。
- 数据库计算的并发令牌(例如时间戳)将仅包含原始值参数。
- 作为并发令牌设置的非计算属性在更新过程中也将包含用于设置新值的参数。 这使用了已讨论的新值的命名规范。 此类令牌的一个示例是使用博客的 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
重写默认值
可以选择性地引入受影响的行参数。
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")));