在Entity Framework 6中从集合中移除

7

附加内容(1):其中一条回复说它应该可行。因此,我在本文末尾添加了完整的异常信息

简述:我有一个对象序列,每个对象都有其他对象的集合。这个集合的一个例子是带有文章集合的博客。MSDN经常将其用作示例。

MSDN Code First到新数据库

我发现,如果我创建一个博客,可以添加一些文章,将博客添加到数据库并调用SaveChanges。实体框架会识别出这些文章应该在不同的表中,并与博客表相关联的外键。

这正是如果没有Entity Framework,我们将如何创建数据库。

从博客中获取所有文章并添加文章可以在不了解单独的文章表和外键到博客表的情况下完成。

然而,当我尝试从博客中删除文章时,突然出现有关外键的错误。

注意:以下内容与效率无关

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Post> Posts { get; set; } 
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int BlogId { get; set; }
    public virtual Blog Blog { get; set; } 
}

public class BloggingContext : DbContext
{
    public BloggingContext(string nameOrConnectionString)
        : base(nameOrConnectionString){}

    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
}

使用方法如下:

class Program
{
    static void Main(string[] args)
    {
        const string dbName = "MyTestDb";
        Database.SetInitializer(new DropCreateDatabaseAlways <BloggingContext> ());
        using (var context = new BloggingContext(this.DbName))
        {
            // create a blog:
            var blog = new Blog()
            {
                Name = "First Blog",
                Posts = new List<Post>()
                {
                    new Post() { Title = "My 1st Post", Content = "Hello World!" },
                    new Post() { Title = "My 2nd Post", Content = "All animals are equal but pigs are more equal"},
                    new Post() { Title = "My 3rd Post", Content = "Shall I compare thee to a summer's day" },
                 },
            };
            context.Blogs.Add(blog);
            context.SaveChanges();

客户端无需知道数据库的实际组织方式,这是件好事。客户端无需知道博客和帖子存储在不同的表中。对于客户端而言,博客拥有一系列的帖子。
同样地,如果客户端请求博客并访问其中的帖子,Entity Framework 就知道如何获取这些帖子。客户端无需知道这些帖子存储在另一个带有外键的表中。
blog = context.Blogs.First();
var lastPost = blog.Posts.Last();

Entity Framework非常智能,不会选择不需要的项。如果我不使用Post集合,则不会从数据库中检索到帖子

因此,我原以为以下代码可以工作:

blog.Posts.Remove(lastPost);
context.AddOrUpdate(blog);
context.SaveChanges();

我原本以为实体框架会知道最后一个帖子已被删除,因此会将该项从“帖子”表中删除。但它并没有这样做。我会得到一个异常,“无法更改关系,因为一个或多个外键属性是非空的。bla bla”,这意味着帖子应该已经从“帖子”表中删除了。
问题:客户端是否不需要了解实体框架创建的数据库模型,除非要删除数据?
补充说明:当然,我可以从DbSet中删除该项,但我的问题是:如果实体框架足够聪明,我不必将帖子添加到DbSet中,那么我为什么要将其删除?
有人要求提供异常文本:
System.InvalidOperationException未处理
_HResult=-2146233079 _message=操作失败:无法更改关系,因为一个或多个外键属性是非空的。当更改关系时,相关的外键属性将设置为空值。如果外键不支持空值,则必须定义新关系,将外键属性分配给另一个非空值,或删除无关对象。 HResult=-2146233079 IsTransient=false Message=操作失败:无法更改关系,因为一个或多个外键属性是非空的。当更改关系时,相关的外键属性将设置为空值。如果外键不支持空值,则必须定义新关系,将外键属性分配给另一个非空值,或删除无关对象。 Source=EntityFramework
StackTrace: at System.Data.Entity.Core.Objects.ObjectContext.PrepareToSaveChanges(SaveOptions options) at System.Data.Entity.Core.Objects.ObjectContext.SaveChangesInternal(SaveOptions options, Boolean executeInExistingTransaction) at System.Data.Entity.Core.Objects.ObjectContext.SaveChanges(SaveOptions options) at System.Data.Entity.Internal.InternalContext.SaveChanges() at System.Data.Entity.Internal.LazyInternalContext.SaveChanges() at System.Data.Entity.DbContext.SaveChanges() at TryInMemoryDbSet.TestDirectDbContext.Test2() in c:\Users\Harald\Documents\Visual Studio 2013\Projects\EntityFramework\TryInMemoryDbSet\TryInMemoryDbSet\TestDirectDbContext.cs:line 75 at TryInMemoryDbSet.Program.Main(String[] args) in c:\Users\Harald\Documents\Visual Studio 2013\Projects\EntityFramework\TryInMemoryDbSet\TryInMemoryDbSet\Program.cs:line 19 InnerException:

你尝试过使用 blog.Posts.Remove(lastPost),然后调用 context.SaveChanges() 吗? - rexcfnghk
哎呀,打错字了。我会在我的问题里进行更正。 - Harald Coppoolse
你能否在这里发布堆栈跟踪信息?根据我的经验,它应该按预期工作。另外,您是否尝试删除 AddOrUpdate() 调用?如果您的实体由 DbContext 跟踪,则 EF 足够智能,可以将实体检测为 EntityState.Modified - rexcfnghk
2个回答

4
答案:
你应该从DbSet中删除它,因为当你从集合中删除它时,实际上是在删除它们之间的“关系”。然而,在你的情况下,这个关系是强制性的,帖子的BlogId属性不为空,这意味着所有帖子都必须属于一个博客。当你从集合中删除时,EF会尝试将FK设置为null值,这就是你得到异常的原因。
这是EF在从Blog中删除一个Post时执行的生成SQL。
UPDATE [dbo].[Posts]
SET [BlogId] = NULL
WHERE ([Id] = @0)

如果BlogId是可空的,那么它将起作用。

我理解你的意思,并且同意这是解决它的方法。但我不明白的是,如果我向现有博客添加帖子,我不必将其添加到DbSet<Post>中。它会自动完成。但是如果我删除它,我必须记得从DbSet中删除它。 - Harald Coppoolse
2
你的第二个陈述是不正确的。你不需要从集合和DbSet中删除它,你只需要从DbSet中删除它(集合不是必需的)。唯一删除对象的方法是从DbSet中删除它。当你从集合中删除它时,你只是删除了它的关系。EF会识别出需要插入的内容,并自动执行。但是,它不会自动删除任何内容,可能是出于安全原因。 - Fabio Luz
我有两个关于集合属性状态跟踪的问题。假设我在检索特定博客时使用了include。我期望该博客会立即被dbcontext跟踪。但是,它是否也会跟踪我加载的每个Post实体?换句话说,如果我更改任何一个Post的状态,它会被持久化吗?第二个问题是,这是否意味着如果一个Blog被跟踪,对集合属性进行的任何修改都将导致Blog的状态变化? - Andes Lam

2
blog.Posts.Remove(lastPost);

blog.Posts集合中删除lastPost,而不是从数据库中删除。

我认为对于一个帖子,需要一个博客,帖子的PostId值不能为null。

至少有两种解决方案:

  • context.Set().Remove(lastPost),或者
  • 在帖子的复合主键中使用BlogId。当设置为null时,PostId将触发在数据库中删除。

1
在某种程度上你是对的。从博客的帖子集合中删除lastPost并不意味着lastPost就不应该再存在了。但我认为奇怪的是,即使我没有指定DbSet<Post>,Entity Framework仍然足够聪明,可以创建一个带有Posts和外键到Blogs的表。然而它却不够聪明去移除它。如果我没有添加它,为什么我还要去移除它。我会等一段时间,也许有人会提出解决方案,如果没有,我将不得不使用你的方法。 - Harald Coppoolse
我刚刚偶然发现了一篇文章,提供了另一种解决方案:http://www.kianryan.co.uk/2013/03/orphaned-child/ 该文章中的第三个解决方案帮助我稍微解决了这个问题。虽然不是理想的解决方案,但它可以工作。遗憾的是它不能像NHibernate那样开箱即用。 - Frederik Gheysels

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接