EntityFramework,如果不存在则插入,否则更新

23

我有一个实体集合“Countries”,反映了我的数据库表中的“<char(2), char(3), nvarchar(50)>”。

我有一个解析器,返回一个已解析的国家数组Country[],并且在正确更新它方面存在问题。我想要的是:对于不在数据库中的国家,请将它们插入,并对已存在的国家进行更新(如果任何字段不同)。这该怎么做?

void Method(object sender, DocumentLoadedEvent e)
{
    var data = e.ParsedData as Country[];
    using(var db = new DataContractEntities)
    {
       //Code missing

    
    }
}
我在想,类似于...
for(var c in data.Except(db.Countries)) but it wount work as it compares on wronge fields.

希望有人曾经遇到过这个问题,并能为我提供解决方案。如果我不能轻松地使用Country对象并插入/更新它们的数组,那么我认为使用该框架没有太多好处,因为从表现者的角度来看,编写自定义SQL脚本以插入它们可能更快,而不是在插入之前检查国家是否已存在于数据库中。

解决方法

请参见帖子的回答。

我向我的国家类添加了重写equals方法:

    public partial class Country
{
    
    public override bool Equals(object obj)
    {
        if (obj is Country)
        {
            var country = obj as Country;
            return this.CountryTreeLetter.Equals(country.CountryTreeLetter);
        }
        return false;
    }
    public override int GetHashCode()
    {
        int hash = 13;
        hash = hash * 7 + (int)CountryTreeLetter[0];
        hash = hash * 7 + (int)CountryTreeLetter[1];
        hash = hash * 7 + (int)CountryTreeLetter[2];
        return hash;
    }
}

然后做了:

        var data = e.ParsedData as Country[];
        using (var db = new entities())
        {
            foreach (var item in data.Except(db.Countries))
            {
                db.AddToCountries(item); 
            }
            db.SaveChanges();
        }

此外,您可以使用新发布的库,该库将自动设置实体图中所有实体的状态。您可以阅读我对类似问题的回答 - Farhad Jabiyev
4个回答

26

我会直接这样做:

void Method(object sender, DocumentLoadedEvent e)
{
    var data = e.ParsedData as Country[];
    using(var db = new DataContractEntities)
    {
        foreach(var country in data)
        {
            var countryInDb = db.Countries
                .Where(c => c.Name == country.Name) // or whatever your key is
                .SingleOrDefault();
            if (countryInDb != null)
                db.Countries.ApplyCurrentValues(country);
            else
                db.Countries.AddObject(country);
        }
        db.SaveChanges();
     }
}

我不知道你的应用程序必须运行多少次或者你的世界有多少个国家。但我有这样一种感觉,这并不是你必须考虑复杂性能优化的事情。

编辑

另一种方法只需要发出一个查询:

void Method(object sender, DocumentLoadedEvent e)
{
    var data = e.ParsedData as Country[];
    using(var db = new DataContractEntities)
    {
        var names = data.Select(c => c.Name);
        var countriesInDb = db.Countries
            .Where(c => names.Contains(c.Name))
            .ToList(); // single DB query
        foreach(var country in data)
        {
            var countryInDb = countriesInDb
                .SingleOrDefault(c => c.Name == country.Name); // runs in memory
            if (countryInDb != null)
                db.Countries.ApplyCurrentValues(country);
            else
                db.Countries.AddObject(country);
        }
        db.SaveChanges();
     }
}

解决方案很好,请问它在每次命中 "var countryInDB = ..." 的时候是否会查询数据库,还是仅在第一次查询? - Poul K. Sørensen
不知道 ApplyCurrentValues。 - Poul K. Sørensen
1
@s093294:是的,names.Contains(...) 会被翻译成 SQL 中的 IN 子句,该子句将在一个查询中包含列表中所有名称。 - Slauma
1
你知道这是否是原子事务吗? - rudimenter
2
@rudimenter:单个SaveChanges调用是一个事务,因此具有原子性。我认为问题更多的是当读取这些国家时是否会被锁定,但是我不知道这个问题的答案。 - Slauma
显示剩余5条评论

11

现代形式,使用较晚的EF版本将是:

context.Entry(record).State = (AlreadyExists ? EntityState.Modified : EntityState.Added);
context.SaveChanges();

AlreadyExists 可以是检查键是否存在或查询数据库以查看该项是否已经存在。

AlreadyExists 可能来自于检查键或通过查询数据库来查看该项是否已经存在。


3
您的解决方案看起来很有趣。您能否提供更多背景信息或“现代形式”的完整实现的参考资料?我会非常感激! - Rymnel
1
例如:MSDN https://msdn.microsoft.com/zh-cn/data/jj592676.aspx(向下滚动到此确切操作)。 - Gábor

0

您可以实现自己的 IEqualityComparer<Country> 并将其传递给 Except() 方法。假设您的 Country 对象具有 IdName 属性,那么一个示例实现可能如下所示:

public class CountryComparer : IEqualityComparer<Country>
{
    public bool Equals(Country x, Country y)
    {
        return x.Name.Equals(y.Name) && (x.Id == y.Id);
    }

    public int GetHashCode(Country obj)
    {
        return string.Format("{0}{1}", obj.Id, obj.Name).GetHashCode();
    }
}

并将其用作

data.Countries.Except<Country>(db, new CountryComparer());

虽然在您的情况下,看起来您只需要提取新对象,但如果您的Id是Guid,则可以使用var newCountries = data.Where(c => c.Id == Guid.Empty);

最好的方法是检查Country.EntityState属性,并根据其值(Detached、Modified、Added等)采取相应措施。

您需要提供有关您的data集合包含什么信息的更多信息,例如这些Country对象是通过entityframework从数据库中检索出来的,在这种情况下,它们的上下文可以被跟踪,还是您使用其他方式生成它们。


看起来我的帖子漏掉了那部分,它是char(2) char(3)和nvarchar(50)。关键是char(2)。 - Poul K. Sørensen
我的方法返回的国家数组是基于解析器创建的,所以它们都包含一个键并且是分离的。然后,我想要将这个分离的country[]数组更新(如果存在则更新,不存在则插入)到数据库中。 - Poul K. Sørensen

0

我不确定这是否是最佳解决方案,但我认为您需要从数据库中获取所有国家,然后与您解析的数据进行比对。

 void Method(object sender, DocumentLoadedEvent e)
 {
    var data = e.ParsedData as Country[];
    using(var db = new DataContractEntities)
    {
       List<Country> mycountries = db.Countries.ToList();
       foreach(var PC in data)
       {
          if(mycountries.Any( C => C.Name==PC.Name ))
          {
             var country = mycountries.Any( C => C.Name==PC.Name );
             //Update it here
          }
          else
          {
               var newcountry = Country.CreateCountry(PC.Name);//you must provide all required parameters
               newcountry.Name = PC.Name;
               db.AddToCountries(newcountry)
          }
       }
       db.SaveChanges();
   }
  }

这也是我得出的结论,正如你所说-不知道它是否是最佳解决方案。现在世界上的国家并不是很多,但如果有数百万行的情况下-获取所有行可能会显得有些过度。 - Poul K. Sørensen

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