EF 7 / Core中的AddOrUpdate发生了什么变化?

44
我正在使用EntityFramework.Core 7.0.0-rc1-final编写一个种子方法。
DbSet的AddOrUpdate方法去哪了?
11个回答

24

它正在等待实现。请参见问题#629#4526

更新:根据下面的评论(未经验证),此功能最终计划在.NET Core 2.1中发布!


9
你的回答是一年半以前的。它还在等待实施吗?看起来已经被放弃了... - Konrad Viltersten
1
我们提出了一种备选的种子数据设计,因此它不会作为其中的一部分实施。另一个问题仍然有可能。 - bricelam
2
数据种子将在EF Core 2.1中推出。 - bricelam
2
数据种子提供了在模型创建时的upsert,但是在应用程序的其他地方呢?例如,如果您想在操作或服务端点内执行upsert呢? - user1477388
3
@user1477388 请查看文档中的断开的实体:保存单个实体 - bricelam
显示剩余4条评论

17
以下的 MS Docs 文章,断开连接的实体,说明从 EF Core 2.0 开始,仅使用 Update 就可以像 AddOrUpdate 一样操作了,只要数据库中的主键列有自动生成(如标识)的值即可。
引用文章中的内容:
如果知道是需要插入还是更新,则可以适当地使用 Add 或 Update。
但是,如果实体使用自动生成的键值,则可以为两种情况使用 Update 方法。
Update 方法通常将实体标记为更新而不是插入。然而,如果实体具有自动生成的键并且没有设置键值,则会自动将该实体标记为插入。
此行为在 EF Core 2.0 中引入。对于早期版本,必须明确选择 Add 或 Update。
如果实体不使用自动生成的键,则应用程序必须决定是否插入或更新实体。
我在一个测试项目中尝试了这个方法,并确认在 EF Core 2.2 中,对于自动生成的键,Update 方法可以同时添加和更新实体。上面链接的“断开的实体”文章还包括一段示例代码,用于自制InsertOrUpdate方法。适用于EF Core的早期版本或如果实体没有自动生成的键。示例代码特定于特定实体类,需要修改以使其通用化。

4
我认为现在应该接受这个答案。 我不相信其他答案对于大于 EF Core 2.0 的任何内容都是相关的。 - mattbloke
我正在考虑在这里和那里施展一些魔法......幸运的是,我继续阅读了下去。 - genuinefafa

8
我认为这就是您想要的内容。
public static class DbSetExtension
{
    public static void AddOrUpdate<T>(this DbSet<T> dbSet, T data) where T : class
    {
        var context = dbSet.GetContext();
        var ids = context.Model.FindEntityType(typeof(T)).FindPrimaryKey().Properties.Select(x => x.Name);

        var t = typeof(T);
        List<PropertyInfo> keyFields = new List<PropertyInfo>();

        foreach (var propt in t.GetProperties())
        {
            var keyAttr = ids.Contains(propt.Name);
            if (keyAttr)
            {
                keyFields.Add(propt);
            }
        }
        if (keyFields.Count <= 0)
        {
            throw new Exception($"{t.FullName} does not have a KeyAttribute field. Unable to exec AddOrUpdate call.");
        }
        var entities = dbSet.AsNoTracking().ToList();
        foreach (var keyField in keyFields)
        {
            var keyVal = keyField.GetValue(data);
            entities = entities.Where(p => p.GetType().GetProperty(keyField.Name).GetValue(p).Equals(keyVal)).ToList();
        }
        var dbVal = entities.FirstOrDefault();
        if (dbVal != null)
        {
            context.Entry(dbVal).CurrentValues.SetValues(data);
            context.Entry(dbVal).State = EntityState.Modified;
            return;
        }
        dbSet.Add(data);
    }

    public static void AddOrUpdate<T>(this DbSet<T> dbSet, Expression<Func<T, object>> key, T data) where T : class
    {
        var context = dbSet.GetContext();
        var ids = context.Model.FindEntityType(typeof(T)).FindPrimaryKey().Properties.Select(x => x.Name);
        var t = typeof(T);
        var keyObject = key.Compile()(data);
        PropertyInfo[] keyFields = keyObject.GetType().GetProperties().Select(p=>t.GetProperty(p.Name)).ToArray();
        if (keyFields == null)
        {
            throw new Exception($"{t.FullName} does not have a KeyAttribute field. Unable to exec AddOrUpdate call.");
        }
        var keyVals = keyFields.Select(p => p.GetValue(data));
        var entities = dbSet.AsNoTracking().ToList();
        int i = 0;
        foreach (var keyVal in keyVals)
        {
            entities = entities.Where(p => p.GetType().GetProperty(keyFields[i].Name).GetValue(p).Equals(keyVal)).ToList();
            i++;
        }
        if (entities.Any())
        {
            var dbVal = entities.FirstOrDefault();
            var keyAttrs =
                data.GetType().GetProperties().Where(p => ids.Contains(p.Name)).ToList();
            if (keyAttrs.Any())
            {
                foreach (var keyAttr in keyAttrs)
                {
                    keyAttr.SetValue(data,
                        dbVal.GetType()
                            .GetProperties()
                            .FirstOrDefault(p => p.Name == keyAttr.Name)
                            .GetValue(dbVal));
                }
                context.Entry(dbVal).CurrentValues.SetValues(data);
                context.Entry(dbVal).State = EntityState.Modified;
                return;
            }                
        }
        dbSet.Add(data);
    }
}

public static class HackyDbSetGetContextTrick
{
    public static DbContext GetContext<TEntity>(this DbSet<TEntity> dbSet)
        where TEntity : class
    {
        return (DbContext)dbSet
            .GetType().GetTypeInfo()
            .GetField("_context", BindingFlags.NonPublic | BindingFlags.Instance)
            .GetValue(dbSet);
    }
}

这是在.NET上吗?该死,写得真糟糕。尽管如此仍在使用。命运到来相同。 - Edgar Froes
但是复杂类型呢?或者 [Owned] 属性。你的代码示例只更新属性,而不更新引用的复杂类型。 - vlukham
1
我从这个代码示例中受益匪浅,欢迎查看:https://github.com/EdgarSalazar/salazar-efcore-dbset-addorupdate - Edgar Froes
8
我明白这是一个较早的回答,但它真的会从表中提取所有记录,在内存中进行过滤,然后更新上下文中的更改状态吗?那太糟糕了。 - M.Babcock
3
很遗憾这个答案居然得到了赞。它有多个缺陷,首先是a) 检索所有行,或者2) 访问私有的 _context 字段而不是使用 ´dbSet.GetService.GetService<ICurrentDbContext>()´,或者3) 在键成员上使用 for 循环而不是 dbSet.Find 等等... - g.pickardou

6
我从Tjaart的回答开始,并进行了两个修改:

我从Tjaart的答案开始,做了两个修改:

  1. I'm using the fluent api for key designation so I'm looking for the entity's primary key instead of an attribute on the entity
  2. I have change tracking turned on and was getting the error others have mentioned regarding that EF is already tracking it. This does a find on the already tracked entity and copies the values from the incoming entity to it, then updates the original entity

    public TEntity AddOrUpdate(TEntity entity)
    {
        var entityEntry = Context.Entry(entity);
    
        var primaryKeyName = entityEntry.Context.Model.FindEntityType(typeof(TEntity)).FindPrimaryKey().Properties
            .Select(x => x.Name).Single();
    
        var primaryKeyField = entity.GetType().GetProperty(primaryKeyName);
    
        var t = typeof(TEntity);
        if (primaryKeyField == null)
        {
            throw new Exception($"{t.FullName} does not have a primary key specified. Unable to exec AddOrUpdate call.");
        }
        var keyVal = primaryKeyField.GetValue(entity);
        var dbVal = DbSet.Find(keyVal);
    
        if (dbVal != null)
        {
            Context.Entry(dbVal).CurrentValues.SetValues(entity);
            DbSet.Update(dbVal);
    
            entity = dbVal;
        }
        else
        {
            DbSet.Add(entity);
        }
    
        return entity;
    }
    

到目前为止,我已经成功地使用它并且没有遇到任何问题。

我正在EFCore 2.1上使用这个。


6

我认为,如果假设基础实体类是一个合法的选项,那么这个解决方案是一个更简单的解决方案。这种简单性来自于你的领域实体实现DomainEntityBase,这减轻了其他建议解决方案中的许多复杂性。

public static class DbContextExtensions
{
    public static void AddOrUpdate<T>(this DbSet<T> dbSet, IEnumerable<T> records) 
        where T : DomainEntityBase
    {
        foreach (var data in records)
        {
            var exists = dbSet.AsNoTracking().Any(x => x.Id == data.Id);
            if (exists)
            {
                dbSet.Update(data);
                continue;
            }
            dbSet.Add(data);
        }
    }
}

public class DomainEntityBase
{
    [Key]
    public Guid Id { get; set; }
}

2
这会强制你让每个模型都从那个基类继承,我认为这对于领域来说并不是一个好主意。 - SuperJMN
好的观点。请注意,我们仅在播种测试/参考数据时使用add-or-update,并且我们播种的所有实体都具有Guid标识符,因此它恰好适用于我们的用例。 - MSC
你在第一次更新后就从循环中返回,这是有意为之吗? - Michael Freidgeim
好的,应该是一个继续。我已经更新了上面的示例。 - MSC
同时,为了解决SuperJMN的担忧,我们对所有实体使用基于替代GUID的键,因此这对我们来说是可行的。我们通过创建唯一的“自然键”索引来平衡这一点,以强制表的自然唯一性。 - MSC

5
有一个扩展方法叫做 Upsert
context.Upsert(new Role { Name = "Employee", NormalizedName = "employee" })
       .On(r => new { r.Name })
       .Run();

On Github


2
不幸的是,这会直接在数据库中保存更改,而添加/更新则会更新上下文。当某个地方使用时间戳类型字段时,它将无法正常工作。您应该提到此方法的限制,如此处所述:https://www.flexlabs.org/2018/02/adding-upsert-support-for-entity-framework-core - user4864425
这个在 .Net Core 3.0 中还能用吗?我是像这样在 Repository 模式中使用 _entities.UpsertRange(entities);,但是没有任何东西被插入到 Sqlite 数据库中。 - Endri

5

我不明白为什么其他回答中的人们要尝试寻找主键。 就像在EF 6的AddOrUpdate方法中一样,在调用该方法时传递它即可。

public static TEntity AddOrUpdate<TEntity>(this DbSet<TEntity> dbSet, DbContext context, Func<TEntity, object> identifier, TEntity entity) where TEntity : class
{
    TEntity result = dbSet.Find(identifier.Invoke(entity));
    if (result != null)
    {
        context.Entry(result).CurrentValues.SetValues(entity);
        dbSet.Update(result);
        return result;
    }
    else
    {
        dbSet.Add(entity);
        return entity;
    }
}

然后像这样稍后使用:

dbContext.MyModels.AddOrUpdate(dbContext, model => model.Id, new MyModel() { Id = 3 });

简洁高效。


context.Entry(result).CurrentValues.SetValues(entity); 这个不起作用。当尝试更新主键时失败了。 - Matthias Burger
@MatthiasBurger 为什么你想要更新主键?这是不可能的。 - Bamdad
这就是错误提示所说的。而这也是你正在做的事情。我不想改变主键。 - Matthias Burger
我不会这样做,代码通过ID查找数据库端的结果,如果存在则覆盖它。如果不存在,则添加它。请告诉我你是如何调用AddOrUpdate方法的。确保你正确地传递了标识符。它应该是主键。 - Bamdad

2
您可以使用我创建的这个扩展方法来为迁移到EF Core而修补我们的代码库:
   public static void AddOrUpdate<T>(this DbSet<T> dbSet, T data) where T : class
        {
            var t = typeof(T);
            PropertyInfo keyField = null;
            foreach (var propt in t.GetProperties())
            {
                var keyAttr = propt.GetCustomAttribute<KeyAttribute>();
                if (keyAttr != null)
                {
                    keyField = propt;
                    break; // assume no composite keys
                }
            }
            if (keyField == null)
            {
                throw new Exception($"{t.FullName} does not have a KeyAttribute field. Unable to exec AddOrUpdate call.");
            }
            var keyVal = keyField.GetValue(data);
            var dbVal = dbSet.Find(keyVal);
            if (dbVal != null)
            {
                dbSet.Update(data);
                return;
            }
            dbSet.Add(data);
        }

2
这实际上不起作用,因为EF Core只会抛出通常的跟踪错误。看起来EF Core在find()之后开始跟踪实体,并且当调用update时会抛出一个错误,表示它已经在跟踪该实体了。这很尴尬,因为EF可能已经在跟踪该实体,而find可以抓取到它,而不是去数据库。 - Douglas Gaskell
我刚在我的EF Core 1.1项目上检查了一下,它完全正常。这段代码自五月底以来一直在生产环境中运行良好。 - Tjaart
我同意Douglas Gaskell的观点,这个解决方案只有在上下文级别禁用更改跟踪时才有效,而对于我们来说,这不是一个选项。请参见下面的我的解决方案以进行替代实现。 - MSC
很奇怪,我们的变更跟踪没有被禁用,但这对我们起作用。也许有些特定于我们解决方案的东西使其能够运行。不确定。 - Tjaart

1

它会将所有现有记录加载到内存中,这一点完全不可扩展。 - Michael Freidgeim
你知道更好的方法吗? ;) - SuperJMN
请查看以下链接以了解有关编程的内容:https://github.com/mcshaz/PicuCalendars/blob/master/PicuCalendars/DataAccess/EFExtensions.cs。可能需要在此处运行实体的重复检查://stackoverflow.com/a/16647237。 - Michael Freidgeim

0

对于我使用的Entity Framework Core(2.0),以上所有答案都无效,以下是对我有效的解决方案:

public static class DbSetExtensions
{

    public static void AddOrUpdate<T>(this DbSet<T> dbSet, Expression<Func<T, object>> identifierExpression, params T[] entities) where T : class
    {
        foreach (var entity in entities)
            AddOrUpdate(dbSet, identifierExpression, entity);
    }


    public static void AddOrUpdate<T>(this DbSet<T> dbSet, Expression<Func<T, object>> identifierExpression, T entity) where T : class
    {
        if (identifierExpression == null)
            throw new ArgumentNullException(nameof(identifierExpression));
        if (entity == null)
            throw new ArgumentNullException(nameof(entity));

        var keyObject = identifierExpression.Compile()(entity);
        var parameter = Expression.Parameter(typeof(T), "p");

        var lambda = Expression.Lambda<Func<T, bool>>(
            Expression.Equal(
                ReplaceParameter(identifierExpression.Body, parameter),
                Expression.Constant(keyObject)),
            parameter);

        var item = dbSet.FirstOrDefault(lambda.Compile());
        if (item == null)
        {
            // easy case
            dbSet.Add(entity);
        }
        else
        {
            // get Key fields, using KeyAttribute if possible otherwise convention
            var dataType = typeof(T);
            var keyFields = dataType.GetProperties().Where(p => p.GetCustomAttribute<KeyAttribute>() != null).ToList();
            if (!keyFields.Any())
            {
                string idName = dataType.Name + "Id";
                keyFields = dataType.GetProperties().Where(p => 
                    string.Equals(p.Name, "Id", StringComparison.OrdinalIgnoreCase) || 
                    string.Equals(p.Name, idName, StringComparison.OrdinalIgnoreCase)).ToList();
            }

            // update all non key and non collection properties
            foreach (var p in typeof(T).GetProperties().Where(p => p.GetSetMethod() != null && p.GetGetMethod() != null))
            {
                // ignore collections
                if (p.PropertyType != typeof(string) && p.PropertyType.GetInterface(nameof(System.Collections.IEnumerable)) != null)
                    continue;

                // ignore ID fields
                if (keyFields.Any(x => x.Name == p.Name))
                    continue;

                var existingValue = p.GetValue(entity);
                if (!Equals(p.GetValue(item), existingValue))
                {
                    p.SetValue(item, existingValue);
                }
            }

            // also update key values on incoming data item where appropriate
            foreach (var idField in keyFields.Where(p => p.GetSetMethod() != null && p.GetGetMethod() != null))
            {
                var existingValue = idField.GetValue(item);
                if (!Equals(idField.GetValue(entity), existingValue))
                {
                    idField.SetValue(entity, existingValue);
                }
            }
        }
    }


    private static Expression ReplaceParameter(Expression oldExpression, ParameterExpression newParameter)
    {
        switch (oldExpression.NodeType)
        {
            case ExpressionType.MemberAccess:
                var m = (MemberExpression)oldExpression;
                return Expression.MakeMemberAccess(newParameter, m.Member);
            case ExpressionType.New:
                var newExpression = (NewExpression)oldExpression;
                var arguments = new List<Expression>();
                foreach (var a in newExpression.Arguments)
                    arguments.Add(ReplaceParameter(a, newParameter));
                var returnValue = Expression.New(newExpression.Constructor, arguments.ToArray());
                return returnValue;
            default:
                throw new NotSupportedException("Unknown expression type for AddOrUpdate: " + oldExpression.NodeType);
        }
    }
}

如果您有更复杂的identifierExpression,则可能需要更新ReplaceParameter()方法。这个实现可以很好地处理简单的属性访问器。例如:

context.Projects.AddOrUpdate(x => x.Name, new Project { ... })
context.Projects.AddOrUpdate(x => new { x.Name, x.Description }, new Project { ... })

然后 context.SaveChanges() 将会把数据提交到数据库


在末尾添加了一行代码,以确保context.SaveChanges()将数据实际提交到数据库。 - Dan Chenier
实体类型“联系人”上的属性“Id”是关键字的一部分,因此无法修改或标记为已修改。要更改具有识别外键的现有实体的主体,请先删除从属实体并调用“SaveChanges”,然后将从属实体与新主体相关联。 - Tzvi Gregory Kaidanov
对我来说,这会抛出以下异常: 无法跟踪实体类型“X”的实例,因为已经在跟踪另一个具有相同键值({'Id','Text'})的实例。 - Nick Farsi

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