数据点

设法应对缺少的外键

Julie Lerman

下载代码示例

Julie Lerman
这个月我正在写关于问题找到了自己帮助有经常晚的人:相关的类定义在代码第一次然后在大多数情况下,模型-视图-控制器 (MVC) 框架中使用的问题。开发商所遇到的问题并不是特定于代码第一次。他们的基础实体框架 (EF) 行为的事实上,常见的大多数对象-关系映射 (ORMs) 的结果。但似乎因为开发商要来代码第一次与某些预期问题浮出水面。MVC 造成他们痛苦因为其高度已断开连接的性质。

而不是只向您显示正确的代码,我会使用此列可以帮助您了解 EF 行为,这样就可以将这种知识应用于许多方案,您可能会遇到当设计您的类或编写您的数据访问代码与 EF Api。

代码的第一个公约是能够检测到并正确推断与您的类中的属性的不同组合的各种关系。在此示例中,而我写此信作为叶正在转向壮观的颜色在佛蒙特州在我家附近的树上,我将为我相关的类使用树和叶。一个一对多关系中,最简单的方法可以说明,在您的类中,并且具有代码首次承认你的意图是集合的要导航属性在树类表示某种类型的叶类型。叶类需要重新指向树没有属性。图 1 显示的树和叶类。

图 1 相关的树和叶类

public class Tree
{
  public Tree()
  {
    Leaves = new List<Leaf>();
  }
  public int TreeId { get; set; }
  public string Type { get; set; }
  public double Lat { get; set; }
  public double Long { get; set; }
  public string Notes { get; set; }
  public ICollection<Leaf> Leaves { get; set; }
}
public class Leaf
{
  public int LeafId { get; set; }
  public DateTime FellFromTreeDate { get; set; }
  public string FellFromTreeColor { get; set; }
}

按照约定,代码第一次会知道在数据库中的叶表需要外键。 它将假设一个外键字段名称为"Tree_TreeId,"并在元数据中创建的代码第一次运行时提供此信息,与 EF 会明白如何算出的查询和更新使用该外键。 EF 依靠相同的过程,它使用与"独立协会"利用了这种行为 — 唯一的我们可以使用之前向微软的关联类型。NET 框架 4 — 而不需要依赖类中的外键属性。

这是漂亮、 干净的方法定义类,当你有信心你不必过导航从叶回其在应用程序中的树。 不过,无需直接访问键,您需要进行编码时要额外勤奋。

创建新的相关类型没有外键或导航属性

虽然您可以轻松使用这些类,显示您的 ASP 中一棵树和叶。NET MVC 应用与编辑叶,开发人员经常会遇到问题通常设计 MVC 应用程序中创建新叶。 我使用的模板从 MVCScaffolding NuGet 包 (mvcscaffolding.codeplex.com),让 Visual Studio 自动­◆ 建立我控制器、 意见和简单的存储库,通过选择"MvcScaffolding:控制器具有读/写操作和视图,使用资料库"请注意因为叶类中有没有外键属性,脚手架模板不会承认一个一对多的关系。 我作出一些次要更改视图和控制器,以允许用户从一棵树导航到树叶,你可以看到在你下载的示例。

所创建的回发叶操作需要从创建视图返回的叶和告诉存储库中添加它,然后保存它,如图所示,在图 2

图 2 添加和保存到存储库中的叶

[HttpPost]
public ActionResult Create(Leaf leaf)
{
  if (ModelState.IsValid)
  {
    leafRepository.InsertOrUpdate(leaf);
    leafRepository.Save();
    return RedirectToAction("Index", 
      new { treeId = Request.Form["TreeId"] });
  }
  else
  {
    return View();
  }
}

存储库中采用的叶,检查,看看它是新,如果是这样,则将其添加到上下文实例创建的回发的结果:

public void InsertOrUpdate(Leaf leaf,int treeId){
  if (leaf.LeafId == default(int)) {
    // New entity
    context.Leaves.Add(leaf);
  } else {
    // Existing entity
    context.Entry(leaf).State = EntityState.Modified;
  }
}

当调用 Save 时,EF 创建插入的命令,向数据库中添加新的一页:

exec sp_executesql N'insert [dbo].[Leaves]([FellFromTreeDate], [FellFromTreeColor],
[Tree_TreeId]) values (@0, @1, null)
select [LeafId]
from [dbo].[Leaves]
where @@ROWCOUNT > 0 and [LeafId] = scope_identity()',
N'@0 datetime2(7),@1 nvarchar(max) ',
@0='2011-10-11 00:00:00',@1=N'Pale Yellow'

请注意命令的第二行中传递的值:@ 0 (表示日期) ; 1 (为修改后的颜色) ; 的 和 null。 空值将被用于 Tree_TreeId 字段。 请记住漂亮、 干净的叶类代表 TreeId,所以有没有办法传递该值在创建独立叶时没有外键的属性。

当依赖的类型 (在本例中,叶) 有没有知识及其主体类型 (树) 的时有做插入只有一种方式:叶实例和树实例必须添加到上下文一起作为同一个图表的一部分。 这将为 EF 提供找出正确的值将插入到数据库的外键 (例如,Tree_TreeId) 所需的所有信息。 但在这种情况下,您正在与叶,仅有没有 EF 确定树的关键属性的值为内存中的信息。

如果你有叶类中的外键属性,生活就会如此简单。 它不是太难控制器和视图之间移动时保持在手边单个值。 事实上,如果你看看创建行动图 2,您可以看到该方法具有访问要为其创建叶的 TreeId 的值。

有多种方法来传送 MVC 应用程序中的数据。 我选择了最简单的此演示:填充到 MVC ViewBag TreeId 和在必要时利用 Html.Hidden 字段。 这使值可以作为视图的 Request.Form 项目之一。

因为我有权访问 TreeId,我能够生成树/叶图形中将插入命令提供的 TreeId。 快速修改存储库类允许接受该 TreeId 变量从视图的 InsertOrUpdate 方法,并使用 DbSet.Find 方法从数据库中检索的树实例。 这里是方法的受影响的部分:

public void InsertOrUpdate(Leaf leaf,int treeId)
{
  if (leaf.LeafId == default(int)) {
    var tree=context.Trees.Find(treeId);
    tree.Leaves.Add(leaf);
  }
...

上下文现在跟踪树和意识到我到树中添加叶。 这一次,当上下文。被称为 SaveChanges,EF 是能够从叶导航到树中发现的密钥值,并在插入命令中使用它。

图 3 展示了使用新版本的 InsertOrUpdate 的修改的控制器代码。

图 3 新版本的 InsertOrUpdate

[HttpPost]
public ActionResult Create(Leaf leaf)
{
  if (ModelState.IsValid)
  {
    leafRepository.InsertOrUpdate(leaf);
    leafRepository.Save();
    return RedirectToAction("Index",
      new { treeId = Request.Form["TreeId"] });
  }
  else
  {
    return View();
  }
}
[HttpPost]
public ActionResult Create(Leaf leaf)
{
  if (ModelState.IsValid)
  {
    var _treeId = Request.Form["TreeId"] as int;
    leafRepository.InsertOrUpdate(leaf, _treeId);
    leafRepository.Save();
    return RedirectToAction("Index", new { treeId = _treeId });
  }
  else
  {
    return View();
  }
}

随着这些改变,insert 方法终于有外键,你可以看到在名为"2"的参数的值:

exec sp_executesql N'insert [dbo].[Leaves]([FellFromTreeDate], [FellFromTreeColor], [Tree_TreeId])
values (@0, @1, @2)
select [LeafId]
from [dbo].[Leaves]
where @@ROWCOUNT > 0 and [LeafId] = scope_identity()',
N'@0 datetime2(7),@1 nvarchar(max) ,
@2 int',@0='2011-10-12 00:00:00',@1=N'Orange-Red',@2=1

最后,此替代方法迫使我对数据库进行另一次旅行。 这是我会选择在这种情况下,在我不想在我依赖类的外键属性中支付的价格。

更新时有没有外键的问题

还有其他方法,您可以绘制自己到一个角落里当你没有您的类中的外键属性的绑定已确定。 这里是另一个例子。

我将添加一个名为 TreePhoto 的新域类。 因为我不想要从此类导航回树,没有导航属性,并再次,我是跟着模式在不使用外键属性:

[Table("TreePhotos")]
public class TreePhoto
{
  public int Id { get; set; }
  public Byte[] Photo { get; set; }
  public string Caption { get; set; }
}

树类提供了两个类之间的唯一连接并指定每一棵树必须有一张照片。 这是我到树类添加新属性:

[Required]
  public TreePhoto Photo { get; set; }

这不会离开的可能性,孤立的照片,但是我使用此示例,因为多次看到 — 随请求帮助 — 所以我想解决它。

再次,代码第一次公约 》 的阻吓­采,外键属性将需要在数据库中,并创建了一个,Photo_Id,以我的名义。 请注意它是不可为空。 这是因为 Leaf.Photo 属性是必需的 (请参见图 4)。

Using Code First Convention, Tree Gets a Non-Nullable Foreign Key to TreePhotos
图 4 使用代码第一次约定,树获取非可空 TreePhotos 外键

您的应用程序可能会让你创建树之前已经采取了照片,但树仍然需要填充该图片属性。 我会将逻辑添加到树存储库的插入­或更新方法来创建默认情况下,新树时并不提供一个空图片:

public void InsertOrUpdate(Tree tree)
{
  if (tree.TreeId == default(int)) {
    if (tree.Photo == null)
    {
      tree.Photo = new TreePhoto { Photo = new Byte[] { 0 },
                                   Caption = "No Photo Yet" };
    }
    context.Trees.Add(tree);
}
...

我想在这里集中更大的问题是这一问题如何影响更新。 想象一下你有一棵树和它所需的照片,已存储在数据库中。 你想要能够编辑一棵树并不需要与照片进行交互。 您要检索的树,也许代码如"上下文。特­­es。Find(someId)"。保存时间时,您将获得一个验证错误,因为树需要一张照片。 但树都有一张照片 ! 它是在数据库中 ! 这是怎么回事?

问题是:当你首先执行查询以检索表,忽视了相关的照片,将从数据库返回只树的标量值和照片将为空 (请参见图 5)。

A Tree Instance Retrieved from the Database Without Its Photo
从它的照片没有数据库中检索的图 5 树实例

MVC 模式粘结剂和 EF 有能力验证所需的注释。 当保存已编辑的树的时候,它的照片还将为空。 如果你让 MVC 控制器代码中执行 ModelState.IsValid 检查,就会看到这张照片是缺少。 IsValid 将虚假和控制器甚至不会去打扰调用存储库。 在我的应用程序,我已经删除模型联编程序验证,这样我可以让我负责任何服务器端验证的存储库代码。 当存储库调用 SaveChanges 时,EF 验证将检测丢失的照片,并引发异常。 但在存储库中,我们有机会处理这个问题。

如果树类有外键属性 — 例如,int PhotoId — 这是必需的 (允许您删除照片导航性能要求),从数据库外的键值会已经被用来填充 PhotoId 树实例的属性。 树将是有效的和 SaveChanges 将能够更新命令发送到数据库。 换句话说,如果有外键属性,树本来正确,甚至没有照片实例。

但是没有外键,您再次需要某种机制提供在保存更改前的照片。 如果您有您的代码第一类和上下文设置为执行延缓加载,任何提及的照片,在您的代码将导致 EF 将从数据库加载该实例。 我还是有些过时,懒的时候加载时,我个人的选择可能会从数据库中执行显式加载。 在新行的代码 (在以下示例中,在我打电话负载的最后一行) 使用 DbContext 方法加载相关的数据:

public void InsertOrUpdate(Tree tree)
{
  if (tree.TreeId == default(int)) {
  ...
} else {
    context.Entry(tree).State = EntityState.Modified;
    context.Entry(tree).Reference(t => t.Photo).Load();
  }
}

这使得 EF 快乐。 因为照片很重要,和 EF 将发送更新到数据库,修改树将会验证。 这里的关键是你要确保照片不是空的; 我已经给你们满足该约束的一种方法。

比较的角度

如果树类只是有一个 PhotoId 属性,这并不会有必要。 PhotoId 整型属性的直接影响是图片属性不再需要所需的注释。 为值类型,它必须始终具有价值,满足要求一棵树必须有一张照片,即使它并不表示为一个实例。 只要在 PhotoId 中有一个值,将满足要求,因此下面的代码工作:

public class Tree
{
  // ...
Other properties
  public int PhotoId { get; set; }
  public TreePhoto Photo { get; set; }
}

当该控制器的编辑方法从数据库中检索一棵树时,都将填充 PhotoId 标量属性。 只要您强制 MVC (或您使用的任何应用程序框架) 往返价值,更新树中,到时候 EF 将关心空图片属性。

更容易的但不是神奇

虽然 EF 团队提供了更多的 API 逻辑,以帮助断开连接的情况下,它仍然是你的工作,了解 EF 的工作原理和其期望是当您正在移动的数据。 是的编码是简单得多的如果您在您的类中包含外键,但他们是您的类和你是什么应该和不应该在他们的最好的判断。 不过,如果您的代码是我的责任,我一定会迫使你说服我,你的原因不外键属性包括抵销的好处包括他们。 EF 都会这样做的一些工作为您的外键都有。 但如果他们缺席,只要你了解 EF 所期望和如何满足这些期望,您应该能够获得您断开连接的应用程序行为您希望的方式。

Julie Lerman 是 Microsoft MVP。净的导师和顾问住在山上的佛蒙特。您可以找到她提交数据访问和其他的微软。用户组和世界各地的会议的净主题。在她的博客 thedatafarm.com/blog 和"编程实体框架"的作者是 (2010 年) 和"编程实体框架:代码第一次"(2011 年),都从 O'Reilly 介质。跟她在 Twitter 上 twitter.com/julielerman

多亏了以下技术专家,检讨这篇文章: Jeff DerstadtRick Strahl