在其图中附加具有现有和新实体混合的实体(Entity Framework Core 1.1.0)

时间:2017-01-04 08:42:20

标签: c# entity-framework entity-framework-core

在将包含引用属性的实体附加到现有实体时,我遇到了一个问题(我将现有实体称为数据库中已存在的实体,并正确设置了PK)。

问题在于使用Entity Framework Core 1.1.0时。这与Entity Framework 7(Entity Framework Core的初始名称)完美配合。

我没有尝试使用EF6或EF Core 1.0.0。

我想知道这是回归,还是故意改变行为。

模型

测试模型包含PlacePerson,以及Place和Person之间通过名为PlacePerson的加入实体之间的多对多关系。

public abstract class BaseEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Person : BaseEntity
{
    public int? StatusId { get; set; }
    public Status Status { get; set; }
    public List<PersonPlace> PersonPlaceCollection { get; set; } = new List<PersonPlace>();
}

public class Place : BaseEntity
{
    public List<PersonPlace> PersonPlaceCollection { get; set; } = new List<PersonPlace>();
}

public class PersonPlace : BaseEntity
{
    public int? PersonId { get; set; }
    public Person Person { get; set; }
    public int? PlaceId { get; set; }
    public Place Place { get; set; }
}

数据库上下文

明确定义所有关系(没有冗余)。

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        // PersonPlace
        builder.Entity<PersonPlace>()
            .HasAlternateKey(o => new { o.PersonId, o.PlaceId });
        builder.Entity<PersonPlace>()
            .HasOne(pl => pl.Person)
            .WithMany(p => p.PersonPlaceCollection)
            .HasForeignKey(p => p.PersonId);
        builder.Entity<PersonPlace>()
            .HasOne(p => p.Place)
            .WithMany(pl => pl.PersonPlaceCollection)
            .HasForeignKey(p => p.PlaceId);
    }

此模型中还公开了所有具体实体:

public DbSet<Person> PersonCollection { get; set; } 
public DbSet<Place> PlaceCollection { get; set; }
public DbSet<PersonPlace> PersonPlaceCollection { get; set; }

保理数据访问

我正在使用Repository样式的基类来计算所有与数据访问相关的代码。

public class DbRepository<T> where T : BaseEntity
{
    protected readonly MyContext _context;
    protected DbRepository(MyContext context) { _context = context; }

    // AsNoTracking provides detached entities
    public virtual T FindByNameAsNoTracking(string name) => 
        _context.Set<T>()
            .AsNoTracking()
            .FirstOrDefault(e => e.Name == name);

    // New entities should be inserted
    public void Insert(T entity) => _context.Add(entity);
    // Existing (PK > 0) entities should be updated
    public void Update(T entity) => _context.Update(entity);
    // Commiting
    public void SaveChanges() => _context.SaveChanges();
}

重现异常的步骤

创建一个人并保存。 创建一个地方并保存。

// Repo
var context = new MyContext()
var personRepo = new DbRepository<Person>(context);
var placeRepo = new DbRepository<Place>(context);

// Person
var jonSnow = new Person() { Name = "Jon SNOW" };
personRepo.Add(jonSnow);
personRepo.SaveChanges();

// Place
var castleblackPlace = new Place() { Name = "Castleblack" };
placeRepo.Add(castleblackPlace);
placeRepo.SaveChanges();

人和地点都在数据库中,因此定义了主键。 PK由SQL Server生成为标识列。

重新加载此人和地点,作为分离的实体(它们被分离的事实用于通过Web API模拟http发布实体的场景,例如在客户端使用angularJS)。 / p>

// detached entities
var jonSnow = personRepo.FindByNameAsNoTracking("Jon SNOW");
var castleblackPlace = placeRepo.FindByNameAsNoTracking("Castleblack");

将此人添加到该地点并保存:

castleblackPlace.PersonPlaceCollection.Add(
    new PersonPlace()  { Person = jonSnow }
);
placeRepo.Update(castleblackPlace);
placeRepo.SaveChanges();

SaveChanges上会抛出异常,因为EF Core 1.1.0会尝试插入现有的人而非执行更新(尽管其主键值已设定。)

例外详情

Microsoft.EntityFrameworkCore.DbUpdateException:更新条目时发生错误。有关详细信息,请参阅内部异常---&GT; System.Data.SqlClient.SqlException:当IDENTITY_INSERT设置为OFF时,无法在表'Person'中为identity列插入显式值。

以前的版本

此代码可以与EF Core(名为EF7)和DNX CLI的alpha版本完美配合(但不一定优化)。

解决方法

迭代根实体图并正确设置实体状态:

_context.ChangeTracker.TrackGraph(entity, node =>
    {
        var entry = node.Entry;
        var childEntity = (BaseEntity)entry.Entity;
        entry.State = childEntity.Id <= 0? EntityState.Added : EntityState.Modified;
    });

最后有什么问题???

为什么我们必须手动跟踪实体状态,而以前版本的EF会完全处理它,即使重新附加分离的实体也是如此?

完整复制源(EFCore 1.1.0 - 无法正常工作)

完整的复制源(包含上述解决方法,但其注释已被注释。取消注释它将使此源工作)。

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using Microsoft.EntityFrameworkCore;

namespace EF110CoreTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            // One scope for initial data
            using (var context = new MyContext())
            {
                // Repo
                var personRepo = new DbRepository<Person>(context);
                var placeRepo = new DbRepository<Place>(context);

                // Database
                context.Database.EnsureDeleted();
                context.Database.EnsureCreated();


                /***********************************************************************/

                // Step 1 : Create a person
                var jonSnow = new Person() { Name = "Jon SNOW" };
                personRepo.InsertOrUpdate(jonSnow);
                personRepo.SaveChanges();

                /***********************************************************************/

                // Step 2 : Create a place
                var castleblackPlace = new Place() { Name = "Castleblack" };
                placeRepo.InsertOrUpdate(castleblackPlace);
                placeRepo.SaveChanges();

                /***********************************************************************/
            }

            // Another scope to put one people in one place
            using (var context = new MyContext())
            {
                // Repo
                var personRepo = new DbRepository<Person>(context);
                var placeRepo = new DbRepository<Place>(context);

                // entities
                var jonSnow = personRepo.FindByNameAsNoTracking("Jon SNOW");
                var castleblackPlace = placeRepo.FindByNameAsNoTracking("Castleblack");

                // Step 3 : add person to this place
                castleblackPlace.AddPerson(jonSnow);
                placeRepo.InsertOrUpdate(castleblackPlace);
                placeRepo.SaveChanges();
            }

        }
    }

    public class DbRepository<T> where T : BaseEntity
    {
        public readonly MyContext _context;
        public DbRepository(MyContext context) { _context = context; }

        public virtual T FindByNameAsNoTracking(string name) => _context.Set<T>().AsNoTracking().FirstOrDefault(e => e.Name == name);

        public void InsertOrUpdate(T entity)
        {
            if (entity.IsNew) Insert(entity); else Update(entity);
        }

        public void Insert(T entity)
        {
            // uncomment to enable workaround
            //ApplyStates(entity);
            _context.Add(entity);
        }

        public void Update(T entity)
        {
            // uncomment to enable workaround
            //ApplyStates(entity);
            _context.Update(entity);
        }

        public void Delete(T entity)
        {
            _context.Remove(entity);
        }

        private void ApplyStates(T entity)
        {
            _context.ChangeTracker.TrackGraph(entity, node =>
            {
                var entry = node.Entry;
                var childEntity = (BaseEntity)entry.Entity;
                entry.State = childEntity.IsNew ? EntityState.Added : EntityState.Modified;
            });
        }

        public void SaveChanges() => _context.SaveChanges();
    }

    #region Models
    public abstract class BaseEntity
    {
        public int Id { get; set; }
        public string Name { get; set; }
        [NotMapped]
        public bool IsNew => Id <= 0;
        public override string ToString() => $"Id={Id} | Name={Name} | Type={GetType()}";
    }

    public class Person : BaseEntity
    {
        public List<PersonPlace> PersonPlaceCollection { get; set; } = new List<PersonPlace>();
        public void AddPlace(Place place) => PersonPlaceCollection.Add(new PersonPlace { Place = place });
    }

    public class Place : BaseEntity
    {
        public List<PersonPlace> PersonPlaceCollection { get; set; } = new List<PersonPlace>();
        public void AddPerson(Person person) => PersonPlaceCollection.Add(new PersonPlace { Person = person, PersonId = person?.Id, PlaceId = 0});
    }

    public class PersonPlace : BaseEntity
    {
        public int? PersonId { get; set; }
        public Person Person { get; set; }
        public int? PlaceId { get; set; }
        public Place Place { get; set; }
    }

    #endregion

    #region Context
    public class MyContext : DbContext
    {
        public DbSet<Person> PersonCollection { get; set; } 
        public DbSet<Place> PlaceCollection { get; set; }
        public DbSet<PersonPlace> PersonPlaceCollection { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);


            // PersonPlace
            builder.Entity<PersonPlace>()
                .HasAlternateKey(o => new { o.PersonId, o.PlaceId });
            builder.Entity<PersonPlace>()
                .HasOne(pl => pl.Person)
                .WithMany(p => p.PersonPlaceCollection)
                .HasForeignKey(p => p.PersonId);
            builder.Entity<PersonPlace>()
                .HasOne(p => p.Place)
                .WithMany(pl => pl.PersonPlaceCollection)
                .HasForeignKey(p => p.PlaceId);
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EF110CoreTest;Trusted_Connection=True;");
        }
    }
    #endregion
}

EFCore1.1.0项目的Project.json文件

{
  "version": "1.0.0-*",
  "buildOptions": {
    "emitEntryPoint": true
},

  "dependencies": {
    "Microsoft.EntityFrameworkCore": "1.1.0",
    "Microsoft.EntityFrameworkCore.SqlServer": "1.1.0",
    "Microsoft.EntityFrameworkCore.Tools": "1.1.0-preview4-final" 
},

  "frameworks": {
    "net461": {}
},

  "tools": {
    "Microsoft.EntityFrameworkCore.Tools.DotNet": "1.0.0-preview3-final"
  }
}

使用EF7 / DNX的工作源

using System.Collections.Generic;
using Microsoft.Data.Entity;
using System.Linq;
using System.ComponentModel.DataAnnotations.Schema;

namespace EF7Test
{
    public class Program
    {
        public static void Main(string[] args)
        {
            // One scope for initial data
            using (var context = new MyContext())
            {
                // Repo
                var personRepo = new DbRepository<Person>(context);
                var placeRepo = new DbRepository<Place>(context);

                // Database
                context.Database.EnsureDeleted();
                context.Database.EnsureCreated();


                /***********************************************************************/

                // Step 1 : Create a person
                var jonSnow = new Person() { Name = "Jon SNOW" };
                personRepo.InsertOrUpdate(jonSnow);
                personRepo.SaveChanges();

                /***********************************************************************/

                // Step 2 : Create a place
                var castleblackPlace = new Place() { Name = "Castleblack" };
                placeRepo.InsertOrUpdate(castleblackPlace);
                placeRepo.SaveChanges();

                /***********************************************************************/
            }

            // Another scope to put one people in one place
            using (var context = new MyContext())
            {
                // Repo
                var personRepo = new DbRepository<Person>(context);
                var placeRepo = new DbRepository<Place>(context);

                // entities
                var jonSnow = personRepo.FindByNameAsNoTracking("Jon SNOW");
                var castleblackPlace = placeRepo.FindByNameAsNoTracking("Castleblack");

                // Step 3 : add person to this place
                castleblackPlace.AddPerson(jonSnow);
                placeRepo.InsertOrUpdate(castleblackPlace);
                placeRepo.SaveChanges();
            }

        }
    }

    public class DbRepository<T> where T : BaseEntity
    {
        public readonly MyContext _context;
        public DbRepository(MyContext context) { _context = context; }

        public virtual T FindByNameAsNoTracking(string name) => _context.Set<T>().AsNoTracking().FirstOrDefault(e => e.Name == name);

        public void InsertOrUpdate(T entity)
        {
            if (entity.IsNew) Insert(entity); else Update(entity);
        }

        public void Insert(T entity) => _context.Add(entity);
        public void Update(T entity) => _context.Update(entity);
        public void SaveChanges() => _context.SaveChanges();
    }

    #region Models
    public abstract class BaseEntity
    {
        public int Id { get; set; }
        public string Name { get; set; }
        [NotMapped]
        public bool IsNew => Id <= 0;
        public override string ToString() => $"Id={Id} | Name={Name} | Type={GetType()}";
    }

    public class Person : BaseEntity
    {
        public List<PersonPlace> PersonPlaceCollection { get; set; } = new List<PersonPlace>();
        public void AddPlace(Place place) => PersonPlaceCollection.Add(new PersonPlace { Place = place });
    }

    public class Place : BaseEntity
    {
        public List<PersonPlace> PersonPlaceCollection { get; set; } = new List<PersonPlace>();
        public void AddPerson(Person person) => PersonPlaceCollection.Add(new PersonPlace { Person = person, PersonId = person?.Id, PlaceId = 0 });
    }

    public class PersonPlace : BaseEntity
    {
        public int? PersonId { get; set; }
        public Person Person { get; set; }
        public int? PlaceId { get; set; }
        public Place Place { get; set; }
    }

    #endregion

    #region Context
    public class MyContext : DbContext
    {
        public DbSet<Person> PersonCollection { get; set; }
        public DbSet<Place> PlaceCollection { get; set; }
        public DbSet<PersonPlace> PersonPlaceCollection { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            // PersonPlace
            builder.Entity<PersonPlace>()
                .HasAlternateKey(o => new { o.PersonId, o.PlaceId });
            builder.Entity<PersonPlace>()
                .HasOne(pl => pl.Person)
                .WithMany(p => p.PersonPlaceCollection)
                .HasForeignKey(p => p.PersonId);
            builder.Entity<PersonPlace>()
                .HasOne(p => p.Place)
                .WithMany(pl => pl.PersonPlaceCollection)
                .HasForeignKey(p => p.PlaceId);
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EF7Test;Trusted_Connection=True;");
        }
    }
    #endregion
}

以及相应的项目文件:

{
"version": "1.0.0-*",
"buildOptions": {
    "emitEntryPoint": true
},

"dependencies": {
    "EntityFramework.Commands": "7.0.0-rc1-*",
    "EntityFramework.Core": "7.0.0-rc1-*",
    "EntityFramework.MicrosoftSqlServer": "7.0.0-rc1-*"
},

"frameworks": {
    "dnx451": {}
},

"commands": {
    "ef": "EntityFramework.Commands"
}
}

1 个答案:

答案 0 :(得分:7)

经过一些研究,阅读评论,博客文章,最重要的是,EF团队成员对我在GitHub回购中提交的问题的回答,看来我在问题中注意到的行为不是错误,但是EF Core 1.0.0和1.1.0的一个功能。

  每当我们确定应该添加实体时,

[...] 1.1   因为它没有密钥集,所以发现的所有实体都是   该实体的子女也将被标记为已添加。

     

(亚瑟维克斯 - &gt; https://github.com/aspnet/EntityFramework/issues/7334

所以我称之为'变通方法'实际上是一种推荐的做法,正如Ivan Stoev在评论中所说的那样。

根据主要关键状态处理实体状态

DbContect.ChangeTracker.TrackGraph(object rootEntity, Action<EntityEntryGraphNodes> callback)方法接受根实体(发布,添加,更新,附加,等等),然后遍历根关系图中所有发现的实体,并执行回调行动。

这可以称为之前 _context.Add()_context.Update()方法。

_context.ChangeTracker.TrackGraph(rootEntity, node => 
{ 
    node.Entry.State = n.Entry.IsKeySet ? 
        EntityState.Modified : 
        EntityState.Added; 
});

但是(之前没有说过'但实际上很重要!)我失踪的时间太久了,这导致了我的HeadAcheExceptions:

  

如果发现已被上下文跟踪的实体,   该实体未被处理(并且它的导航属性不是   遍历)。

     

(来源:该方法的智能感知!)

因此,在发布断开连接的实体之前,确保上下文没有任何内容可能是安全的:

public virtual void DetachAll()
{
    foreach (var entityEntry in _context.ChangeTracker.Entries().ToArray())
    {
        if (entityEntry.Entity != null)
        {
            entityEntry.State = EntityState.Detached;
        }
    }
}

客户端状态映射

另一种方法是处理客户端的状态,发布实体(因此按设计断开连接),并根据客户端状态设置其状态。

首先,定义一个将客户端状态映射到实体状态的枚举(只缺少分离状态,因为没有意义):

public enum ObjectState
{
    Unchanged = 1,
    Deleted = 2,
    Modified = 3,
    Added = 4
}

然后,使用DbContect.ChangeTracker.TrackGraph(object rootEntity, Action<EntityEntryGraphNodes> callback)方法根据客户端状态设置实体状态:

_context.ChangeTracker.TrackGraph(entity, node =>
{
    var entry = node.Entry;
    // I don't like switch case blocks !
    if (childEntity.ClientState == ObjectState.Deleted) entry.State = EntityState.Deleted;
    else if (childEntity.ClientState == ObjectState.Unchanged) entry.State = EntityState.Unchanged;
    else if (childEntity.ClientState == ObjectState.Modified) entry.State = EntityState.Modified;
    else if (childEntity.ClientState == ObjectState.Added) entry.State = EntityState.Added;
});

使用这种方法,我使用BaseEntity抽象类,它共享我的实体的Id(PK),以及ClientState(类型ObjectState) (以及基于PK值的IsNew访问器)

public abstract class BaseEntity
{
    public int Id {get;set;}
    [NotMapped]
    public ObjectState ClientState { get;set; } = ObjectState.Unchanged;
    [NotMapped]
    public bool IsNew => Id <= 0;
}

乐观/启发式方法

这就是我实际实施的内容。我有旧方法的混合(意思是如果实体有未定义的PK,必须添加它,如果根有PK,它必须我更新),以及客户端状态方法:

_context.ChangeTracker.TrackGraph(entity, node =>
{
    var entry = node.Entry;
    // cast to my own BaseEntity
    var childEntity = (BaseEntity)node.Entry.Entity;
    // If entity is new, it must be added whatever the client state
    if (childEntity.IsNew) entry.State = EntityState.Added;
    // then client state is mapped
    else if (childEntity.ClientState == ObjectState.Deleted) entry.State = EntityState.Deleted;
    else if (childEntity.ClientState == ObjectState.Unchanged) entry.State = EntityState.Unchanged;
    else if (childEntity.ClientState == ObjectState.Modified) entry.State = EntityState.Modified;
    else if (childEntity.ClientState == ObjectState.Added) entry.State = EntityState.Added;
});