23

I'm having a Entity-Set Countries, reflecting a database table '<'char(2),char(3),nvarchar(50> in my database.

Im having a parser that returns a Country[] array of parsed countries, and is having issues with getting it updated in the right way. What i want is: Take the array of countries, for those countries not already in the database insert them, and those existing update if any fields is different. How can this be done?

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

    
    }
}

I was thinking something like

for(var c in data.Except(db.Countries)) but it wount work as it compares on wronge fields.

Hope anyone have had this issues before, and have a solution for me. If i cant use the Country object and insert/update an array of them easy, i dont see much benefict of using the framework, as from performers i think its faster to write a custom sql script that inserts them instead of ect checking if an country is already in the database before inserting?

Solution

See answer of post instead.

I added override equals to my country class:

    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;
    }
}

and then did:

        var data = e.ParsedData as Country[];
        using (var db = new entities())
        {
            foreach (var item in data.Except(db.Countries))
            {
                db.AddToCountries(item); 
            }
            db.SaveChanges();
        }
Community
  • 1
  • 1
Poul K. Sørensen
  • 16,950
  • 21
  • 126
  • 283
  • Also, you can use newly released library which will automatically set the satet of all entities in the entity graph. You can read [my answer to the similar question](http://stackoverflow.com/questions/5557829/update-row-if-it-exists-else-insert-logic-with-entity-framework/39609020#39609020). – Farhad Jabiyev Sep 21 '16 at 06:43

4 Answers4

26

I would do it straightforward:

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();
     }
}

I don't know how often your application must run this or how many countries your world has. But I have the feeling that this is nothing where you must think about sophisticated performance optimizations.

Edit

Alternative approach which would issue only one query:

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();
     }
}
Slauma
  • 175,098
  • 59
  • 401
  • 420
  • The solution is fine, do you know if it querys the database for each time it hits the var countryInDB = ... or only the first time. – Poul K. Sørensen Aug 06 '11 at 11:41
  • Didnt know about the ApplyCurrentValues – Poul K. Sørensen Aug 06 '11 at 11:41
  • @s093294: It hits the DB every time `SingleOrDefault` is called. The solution of @Miroprocessor is fine as well, it would hit the DB only once (when `ToList` is called). However it would potentially load too many countries you don't need. For example: If your `data` collection contains only a few (say 2) countries and in the DB are already 200 countries, `ToList` would load all 200 which is a lot of unnecessary overhead. I have provided an alternative query which would hit the DB only once, see my Edit. – Slauma Aug 06 '11 at 11:59
  • It looks like the best solution yes, any comment on how data.Except(db.Countries) works behind the scene ? It dont fetch all the countries in db to do teh except does it? – Poul K. Sørensen Aug 06 '11 at 12:08
  • @s093294: Your solution with `Except` fetches first *all* countries from the DB and then performs `Except` in memory (I've just tested it). It's not surprising because you're calling `Except` on `data` which is an `IEnumerable` but not an `IQueryable`. I find the Equals/HashCode/Except method much too bloated for such a simple task. But it might be a matter of taste. – Slauma Aug 06 '11 at 12:18
  • @s093294: Also don't forget: Your `Except` procedure doesn't update existing countries at all. It simply ignores the existing countries and leaves their values as they are in the database. – Slauma Aug 06 '11 at 12:26
  • do your countriesInDB submit the names list to the database to do the query, or would it not ask for the hole countries list and then do the filter with where? – Poul K. Sørensen Aug 06 '11 at 12:45
  • 1
    @s093294: Yes, `names.Contains(...)` is translated into an `IN` clause in SQL which will contain all the names of the list in one query. – Slauma Aug 06 '11 at 12:50
  • 1
    Do you know if this is an atomic transaction? – rudimenter Nov 20 '13 at 11:58
  • 2
    @rudimenter: The single `SaveChanges` call is a transaction and therefore atomic. I think the question is more if the countries are locked when they are read, but I don't know the answer to this question. – Slauma Nov 20 '13 at 13:31
11

The modern form, using later EF versions would be:

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

AlreadyExists can come from checking the key or by querying the database to see whether the item already exists there.

Gábor
  • 9,466
  • 3
  • 65
  • 79
  • 3
    Your solution looks interesting. Could you please provide more context or a reference to a complete implementation of this "modern form"? I would really appreciate it! – Rymnel Jun 12 '16 at 20:11
  • 1
    MSDN, for instance: https://msdn.microsoft.com/en-us/data/jj592676.aspx (scroll down to the bottom for this exact operation). – Gábor Jun 13 '16 at 22:30
0

You can implement your own IEqualityComparer<Country> and pass that to the Except() method. Assuming your Country object has Id and Name properties, one example of that implementation could look like this:

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();
    }
}

and use it as

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

Although, in your case it looks like you just need to extract new objects, you can use var newCountries = data.Where(c => c.Id == Guid.Empty); if your Id is Guid.

The best way is to inspect the Country.EntityState property and take actions from there regarding on value (Detached, Modified, Added, etc.)

You need to provide more information on what your data collection contains i.e. are the Country objects retrieved from a database through the entityframework, in which case their context can be tracked, or are you generating them using some other way.

Tomislav Markovski
  • 12,331
  • 7
  • 50
  • 72
  • Looks like my post left out that part, its char(2) char(3) and nvarchar(50). The key is char(2). – Poul K. Sørensen Aug 06 '11 at 11:11
  • Well the array of countries returend to my method is created based on the parser, so they all contains a key and is detached. I then want to take the detached country[] and update if update or insert if it dont exist in the database. – Poul K. Sørensen Aug 06 '11 at 11:16
0

I am not sure this will be the best solution but I think you have to get all countries from DB then check it with your parsed data

 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();
   }
  }
Amir Ismail
  • 3,865
  • 3
  • 20
  • 33
  • That is also what i concluded, and as you state- not knowing if its the best solution. Now there's not that many countries in the world, but if it was another case with millions of rows - fetching them all might seem a bit overdoing. – Poul K. Sørensen Aug 06 '11 at 11:13