最近做了个多对多对实体对象,结果发现每次只要增加一个子实体,就会自动添加一个父实体进去,而不管该父实体是否已经存在.

找了好久,终于找到这篇文章,照文章内容来看,应该是断开连接导致的.

原文地址:http://msdn.microsoft.com/zh-cn/magazine/dn166926.aspx

------------------------------------------------------------------------------

在为本期专栏的主题构思的时候,有三位朋友通过 twitter 和邮件问我,实体框架为什么向他们的数据库再次插入已有对象。

看来,我不用为本期专栏写什么而头疼了。

由于实体框架具有状态管理能力,因此当它处理图形时,其实体状态行为并不总是符合你的期望。

我们来看一个典型示例。

假定有两个类:Screencast 和 Topic 类,且为每个 Screencast 对象分配一个 Topic 对象,如图 1 所示。

图 1 Screencast 和 Topic 类             

public class Screencast

{

  public int Id { get; set; }

  public string Title { get; set; }

  public string Description { get; set; }

  public Topic Topic { get; set; }

  public int TopicId { get; set; }

}

public class Topic

{

  public int Id { get; set; }

  public string Name { get; set; }

}

如果我想要检索 Topic 的列表,并将其中一个对象分配给新的 Screencast 对象然后保存(整个操作集都包含在一个上下文中),整个过程不会有任何问题,如下例所示:

        using (var context = new ScreencastContext())
{
var dataTopic =
context.Topics.FirstOrDefault(t=>t.Name.Contains("Data"));
context.Screencasts.Add(new Screencast
{
Title="EF101",
Description = "Entity Framework 101",
Topic = dataTopic
});
context.SaveChanges();
}

于是,数据库中就会插入一个 Screencast 对象,并且具有指向所选 Topic 的相应外键。

如果你是在客户端应用程序中工作,或是在上下文跟踪所有活动的单个工作单元内执行这些步骤,那么上述处理方式可能正是你期望的。

不过,如果您正在处理已断开连接的数据,那么其处理方式将会迥然不同,结果也可能会让许多开发者大吃一惊。

在断开连接的场景中包含图形的处理方式

我在处理引用列表时通常采用的一种模式是使用独立的上下文,当保存任何用户修改时该上下文将不再处于可访问范围内。

这对 Web 应用程序和 Web 服务来说是常见的情景,但也可能发生在客户端应用程序中。

下面的例子使用一个存储库来存储引用数据,通过下面的 GetTopicList 方法来检索 Topic 的列表:

       public class SimpleRepository

{

  public List<Topic> GetTopicList()

  {

    using (var context = new ScreencastContext())

    {

      return context.Topics.ToList();

    }

  }

 ...

          }

然后你可以将这些 Topic 对象以列表形式展现在一个 Windows Presentation Foundation (WPF) 表单中,以便让用户可以新建 Screencast 对象,例如图 2 所示的表单。

           图 2 用来输入新 Screencast 对象的 Windows Presentation Foundation 表单          

然后,在客户端应用程序中(如图 2 所示的 WPF 表单),将下拉列表中选定的条目赋给新 Screencast 对象的 Topic 属性,代码如下:

          private void Save_Click(object sender, RoutedEventArgs e)

{

  repo.SaveNewScreencast(new Screencast

                {

                  Title = titleTextBox.Text,

                  Description = descriptionTextBox.Text,

                  Topic = topicListBox.SelectedItem as Topic

                });

}

此时 Screencast 变量是一个包含了新建的 Screencast 和 Topic 实例的图形。

将该变量传递给存储库的 SaveNewScreencast 方法,即可将此图形添加到新建的上下文实例中并随即保存到数据库,如下列代码所示:

          public void SaveNewScreencast(Screencast screencast)

{

  using (var context = new ScreencastContext())

  {

    context.Screencasts.Add(screencast);

    context.SaveChanges();

  }

}

对数据库活动进行分析,我们发现以上代码不仅向数据库插入了 Screencast 对象,而且在此之前,还向 Topics 表插入了关于 Data Dev 主题的一行新记录,即使该主题已经存在:

          exec sp_executesql N'insert [dbo].[Topics]([Name])

values (@)

select [Id]

from [dbo].[Topics]

where @@ROWCOUNT >  and [Id] = 

  scope_identity()',N'@ nvarchar(max) ',@0=N'Data Dev'

这种行为使许多开发者感到困惑。

发生这种情况的原因是,当你调用 DBSet.Add 方法(即 Screencasts.Add)时,不仅根实体的状态标记为“Added”,图形中上下文之前未知的所有实体的状态也都标记为 Added。

尽管开发者可能注意到 Topic 对象已经有一个 Id 值,但实体框架则以其 EntityState (Added) 状态为准,无视已有的 Id,仍然为该 Topic 对象创建一条 Insert 数据库命令。

虽然许多开发者可能会预测到这种行为,但是还有许多人并不了解。

在后一种情况下,如果你没有对数据库活动进行分析,可能不会意识到发生了什么,直到下次你(或用户)在 Topics 列表中发现重复条目才知道出了问题。

注: 如果你对实体框架如何插入新记录不太了解,可能会对上文所述的 SQL 中的 select 语句感到好奇。

它是用来确保实体框架能够取回新创建的 Screencast 记录的 Id 值,以便在 Screencast 实例中设置此值。

当加入整个图形时,这不仅只是个问题

我们来看看另一种可能发生此问题的场景。

如果不向存储库传递图形,而是让存储库方法将新建的 Screencast 和选定的 Topic 同时作为请求参数,会怎么样?

这样一来,不再是添加整个图形,而是添加 Screencast 实体,然后设置其 Topic 导航属性:

public void SaveNewScreencastWithTopic(Screencast screencast,

  Topic topic)

{

  using (var context = new ScreencastContext())

 {

    context.Screencasts.Add(screencast);

    screencast.Topic = topic;

    context.SaveChanges();

  }

}

在本例中,SaveChanges 的行为与已添加图形的行为没什么两样。

您可能已经熟悉如何使用实体框架的 Attach 方法将未跟踪的实体附加到上下文。

在本例中,实体的初始状态是 Unchanged。

但在这里,当我们把 Topic 赋给 Screencast 实例而非上下文时,实体框架会把它看成是未识别的实体,而实体框架对无状态的未识别实体的默认处理方式是将其标记为 Added。

这样一来,Topic 将在调用 SaveChanges 时被再次插入数据库。

我们可以对状态进行控制,但这需要对实体框架的行为有更深入的理解。

例如,如果你准备将 Topic 直接附加到上下文,而不是附加到状态为 Added 的 Screencast 对象,那么其 EntityState 状态的初始值将会是 Unchanged。

此时将 Topic 赋值给 screencast.Topic 将不会引起状态变化,因为上下文已经意识到 Topic 的存在了。

下面是展示这一逻辑的修改后的代码:

using (var context = new ScreencastContext())

{

  context.Screencasts.Add(screencast);

  context.Topics.Attach(topic);

  screencast.Topic = topic;

  context.SaveChanges();

}

还有另外一种处理方法:不调用 context.Topics.Attach(topic),而是代之以在此前或此后设置 Topic 的状态,明确地将其状态设置为 Unchanged:

context.Entry(topic).State = EntityState.Unchanged

如果在上下文意识到 Topic 的存在之前调用上述代码,会导致上下文附加该 Topic,并随即设置其状态。

尽管上述这些做法是处理该问题的正确模式,但我们不会自然而然地想到这么做。

除非你已经预先了解实体框架的这种处理方式,并知道所需的代码模式,否则你可能会更倾向于编写看起来符合正常逻辑的代码,然后在实际运行中遇到这个问题,只有到这时候你才会开始研究到底出了什么事。

避免麻烦,使用外键

但还有一种简单得多的方法,利用外键属性,可以避免这种迷惑/混淆(原谅我的俏皮话)。

与其设置 Topic 这个导航属性并且不得不为其状态操心,不如只设置 TopicId 属性,因为你确实可以在 Topic 实例中访问到它的值。

这是我经常给开发者建议的做法。

甚至在 Twitter 上,我也看到这样的问题: “为什么实体框架会插入已经存在的数据?”而我在回复中经常猜对了: “你是不是在对新建实体设置导航属性,而没有用外键?           J”

因此,让我们回顾一下 WPF 表单中的 Save_Click 方法,并改为设置 TopicId 属性而非 Topic 导航属性:

 repo.SaveNewScreencast(new Screencast

               {

                 Title = titleTextBox.Text,

                 Description = descriptionTextBox.Text,

                 TopicId = (int)topicListBox.SelectedValue)

               });

此时,发送给存储库方法的 Screencast 就不再是图形,只是单个实体。

实体框架可以用该外键属性来直接设置表的 TopicId。

这样一来,对实体框架来说,为包含 TopicId 值(在本例中,其值为 2)的 Screencast 实体创建一个 insert 方法就简单了(而且更快了):

 exec sp_executesql N'insert [dbo].[Screencasts]([Title], [Description], [TopicId])

values (@0, @1, @2)

select [Id]

from [dbo].[Screencasts]

where @@ROWCOUNT > 0 and [Id] = scope_identity()',

N'@0 nvarchar(max) ,@1 nvarchar(max) ,@2 int',

  @0=N'EFFK101',@1=N'Using Foreign Keys When Setting Navigations',@2=2

如果你想把这段构造逻辑限制在存储库内,而且不想让用户界面开发者操心外键的设置,可以把 Topic 的 Id 和 Screencast 指定为存储库方法的参数,如下所示:

         public void SaveNewScreencastWithTopicId(Screencast screencast, 

  int topicId)

{

  using (var context = new ScreencastContext())

  {

    screencast.TopicId = topicId;

    context.Screencasts.Add(screencast);

    context.SaveChanges();

  }

}

我们需要担心的不止于此,还需要考虑到,开发者可能还会设置 Topic 导航属性。

换言之,即使我们想用外键来避免 EntityState 问题,但万一 Topic 实例是图形的一部分怎么办?例如以下所示 Save_Click 按钮的另一种代码实现:

       repo.SaveNewScreencastWithTopicId(new Screencast

   {

     Title = titleTextBox.Text,

      Description = descriptionTextBox.Text,

      Topic=topicListBox.SelectedItem as Topic

    },

  (int) topicListBox.SelectedValue);

不幸的是,这将让你回到问题的原点: 实体框架将 Topic 实体看成是图形,并将该实体与 Screencast 一起添加到上下文中,即使已经设置了 Screencast.TopicId 属性也是如此。           而且 Topic 实例的 EntityState 再次造成了混淆: 实体框架将插入一条新的 Topic 记录,并在插入 Screencast 记录时用该值作为新记录的 Id。

避免这一问题的最安全方法,是在设置外键的值时将 Topic 属性设置为 null。

如果有其他用户界面要使用存储库方法,而您又无法确保只会用到已有的 Topic,那么你甚至可能想在这种可能的情况下新建一个 Topic 传递过去。

图 3 展示了为完成这一任务而再次修改的存储库方法。

图 3 旨在防止向数据库意外插入导航属性的存储库方法            

    public void SaveNewScreencastWithTopicId(Screencast screencast, 

 int topicId)

{

  if (topicId > )

  {

    screencast.Topic = null;

    screencast.TopicId = topicId;

  }

  using (var context = new ScreencastContext())

  {

    context.Screencasts.Add(screencast);

    context.SaveChanges();

  }

}

此时我的存储库方法就可以应对若干种场景,甚至还提供了相应的逻辑,可以提供新的 Topic 并传递给该方法。

用 ASP.NET MVC 4 基架生成的代码来避免这一问题

尽管断开连接的应用程序天生存在这个问题,但如果你用 ASP.NET MVC 4 基架来生成视图和 MVC 控制器,就可以避免导航实体被重复插入数据库的问题。

鉴于 Screencast 与 Topic 以及 TopicId 属性(该属性是 Screencast 类型中的外键)之间是一对多关系,基架在控制器中生成以下 Create 方法:

public ActionResult Create()

{

  ViewBag.TopicId = new SelectList(db.Topics, "Id", "Name");

  return View();

}

这段代码构建了一个 Topic 列表,命名为 TopicId(与外键属性同名),并将其传递给视图。

基架也在 Create 视图的标记中包含了以下列表:

 <div class="editor-field">

  @Html.DropDownList("TopicId", String.Empty)

  @Html.ValidationMessageFor(model => model.TopicId)

</div>

当该视图将数据提交回来时,HttpRequest.Form 中包含了一个名为 TopicId 的查询字符串值,该值来自 ViewBag 属性。

TopicId 的值是 DropDownList 中选定条目的值。

因为查询字符串的名称与 Screencast 的属性名匹配,所以 ASP.NET MVC 模型绑定将使用所创建的 Screencast 实例的 TopicId 属性值作为方法参数,如图 4 所示。

图 4 新的 Screencast 从匹配的 HttpRequest 查询字符串值来获取其 TopicId 值          

为了检验这一点,你可以将控制器的 TopicId 变量改为其他名字,例如 TopicIdX,然后在视图的 @Html.DropDownList 中对“TopicId”字符串作同样修改,则查询字符串值(现在是 TopicIdX)将被忽略,screencast.TopicId 的值将为 0。

这时,将不会有 Topic 实例通过管道传递回来。

因此 ASP.NET MVC 默认根据外键属性,从而避免了向数据库重复插入已有的 Topic。

这不是你的错!

断开连接的图形太复杂了

尽管实体框架的开发团队在一版又一版的更新升级中做了大量工作,使断开连接的数据处理起来更容易,但它仍然是个让许多并不熟知实体框架预期行为的开发者为之气馁的问题。

在 Rowan Miller 和我共同编著的《Programming Entity Framework: DbContext》(实体框架编程:DbContext)一书(O' Reilly Media,2012)中,我们花了一整章讨论断开连接的实体和图形。           而且在制作近期的一集 Pluralsight 课程时,我额外增加了 25 分钟的时间,专门讲解断开连接的图形在存储库中的复杂性。

用图形进行数据查询和交互是非常方便的,但要建立图形与现有数据的关系时,外键是不可或缺的朋友!

请查阅我在 2012 年 1 月的专栏文章“设法应对缺少的外键”(msdn.microsoft.com/magazine/hh708747),其中也讨论了不用外键的一些编程陷阱。

在下一期专栏文章中,我将继续探索如何减轻开发者在断开连接的场景中与图形打交道所遇到的痛苦。

那期专栏是本主题的第二部分,将集中讨论如何在多对多关系和导航集合中对 EntityState 进行控制。

 

Julie Lerman

是 Microsoft MVP、.NET 导师和顾问,住在佛蒙特州的山区。 您可以在全球的用户组和会议中看到她对数据访问和其他 Microsoft .NET 主题的演示。 她是《Programming Entity Framework》(2010) 以及“代码优先”版 (2011) 和 DbContext 版 (2012)(均出自 O’Reilly Media)的作者,博客网址为 thedatafarm.com/blog。 请关注她的 Twitter:twitter.com/julielerman

EF为什么向我的数据库再次插入已有对象?(ZT)的更多相关文章

  1. wpf linq数据库无法插入

    最近做wpf应用程序,遇到一个很奇怪的问题,我用代码往数据库里插入数据成功了,但去vs的服务器资源管理器里查看数据库总是最开始的样子,什么都没有插入进去,然后就检查代码,打日志查看sql语句,发现都没 ...

  2. 向Oracle数据库中插入数据出错:ORA-01036 无效的变量名或数据

    向Oracle数据库中插入数据出错: 经过排查,因为Update数据时没有出错,所以OracleHelper没有问题: 看异常信息提示:无效的变量和数据,应该是SQL语句的问题,调试时所传的实例Use ...

  3. MySQL命令:创建数据库、插入数据

    简介: 学习mysql环境为ubantu,下面记录一些基本的创建数据库和插入数据的口令 打开MySQL 服务并使用 root 登录: --打开 MySQL 服务 sudo service mysql ...

  4. 在ef core中使用postgres数据库的全文检索功能实战之中文支持

    前言 有关通用的postgres数据库全文检索在ef core中的使用方法,参见我的上一篇文章. 本文实践了zhparser中文插件进行全文检索. 准备工作 安装插件,最方便的方法是直接使用安装好插件 ...

  5. ASP.NET MVC5利用EF,反向自动生成数据库

    1.在Model类里面,写好相应的属性. using System; using System.Collections.Generic; using System.Linq; using System ...

  6. java.sql.date与java.util.date区别以及数据库中插入带时分秒的时间

    java.sql.Date,java.sql.Time和java.sql.Timestamp三个都是java.util.Date的子类(包装类). java.sql.Date是java.util.Da ...

  7. 使用JDBC向数据库中插入一条数据

    原谅我是初学者,这个方法写的很烂,以后不会改进,谢谢 /** * 通过JDBC向数据库中插入一条数据 1.Statement 用于执行SQL语句的对象 1.1 通过Connection 的 * cre ...

  8. Oracle数据库中插入日期型数据(to_date的用法)(转载)

    往Oracle数据库中插入日期型数据(to_date的用法) INSERT  INTO  FLOOR  VALUES  ( to_date ( '2007-12-20 18:31:34' , 'YYY ...

  9. python_如何通过twisted实现数据库异步插入?

    如何通过twisted实现数据库异步插入? 1. 导入adbapi 2. 生成数据库连接池 3. 执行数据数据库插入操作 4. 打印错误信息,并排错 #!/usr/bin/python3 __auth ...

随机推荐

  1. 【BZOJ 3884】 上帝与集合的正确用法

    [题目链接] 点击打开链接 [算法] 通过欧拉拓展定理,列出递推公式 [代码] #include<bits/stdc++.h> using namespace std; typedef l ...

  2. bzoj 2083 Intelligence test —— 思路+vector

    题目:https://www.lydsy.com/JudgeOnline/problem.php?id=2083 先把所有子序列都存下来,总长度应该有限制,所以用 vector 存: 要做到 O(n) ...

  3. 从mysql高可用架构看高可用架构设计

    高可用HA(High Availability)是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间. 假设系统一直能够提供服务,我们说系统的可用性是100%.如果 ...

  4. POJ1050【DP】

    题意: 求一个最大子矩阵和. 思路: 枚举行区间,然后求一个最大子序列和. 贴一发挫code- #include <iostream> #include <cstdio> #i ...

  5. 【POJ - 3190 】Stall Reservations(贪心+优先队列)

    Stall Reservations 原文是English,这里直接上中文吧 Descriptions: 这里有N只 (1 <= N <= 50,000) 挑剔的奶牛! 他们如此挑剔以致于 ...

  6. 序列/树上差分小结 By cellur925

    首先我们需要注意一下的是,差分比较适用于修改比较多而查询比较少的情况. 一.序列上差分 借教室  这是一道二分答案,在check函数中用到差分技巧的一道题,譬如说我们要把一个序列中[l,r]区间都加上 ...

  7. 最小生成树Prim算法和Kruskal算法(转)

    (转自这位大佬的博客 http://www.cnblogs.com/biyeymyhjob/archive/2012/07/30/2615542.html ) Prim算法 1.概览 普里姆算法(Pr ...

  8. the little schemer 笔记(2)

    第二章 Do it, Do it Again, and Again, and Again... 假设l是 (Jack Sprat could eat no chicken fat) 那么 (lat? ...

  9. 跟我一起玩Win32开发(16):ListView的多个视图

    在上一个例子中,我们只用到了ListView的Report视图,也就是详细视图.本文我们再把上一篇文章中所用的例子进行一下扩展,例子源码可以到俺的资源区下载. 我们为ListView中显示的数据加上图 ...

  10. UVa第十章数学概念与方法

    Bryce1010模板 10.1数论初步 1.欧几里得算法和唯一分解定理 2.Eratosthenes筛法 补充素数筛选 const int MAXN=1e6+10; ll prime[MAXN]; ...