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")));