This idea only works reliably if the target table is empty, or records are being inserted with ids higher than all already existing ids in the table!
3 years on and I hit a similar problem transferring production data into a test system. The users wanted to be able to copy the production data into the test system whenever they wanted to, so instead of setting up a transfer job in SQL Server I looked for a way to accomplish the transfer in the application using the existing EF classes. This way I could provide the users a menu item to start the transfer whenever they wanted.
The application uses a MS SQL Server 2008 database and EF 6. As the two databases generally have the same structure I thought I could easily transfer data from one DbContext instance to another by reading the records of each entity using AsNoTracking()
and just Add()
(or AddRange()
) the records to the appropriate property on the target DbContext instance.
Here is a DbContext with one entity to illustrate:
public class MyDataContext: DbContext
{
public virtual DbSet<Person> People { get; set; }
}
To copy the People data I did the following:
private void CopyPeople()
{
var records = _sourceContext.People.AsNoTracking().ToArray();
_targetContext.People.AddRange(records);
_targetContext.SaveChanges();
}
As long as the tables were copied in the right order (to avoid problems with foreign key constraints) this worked very well. Unfortunately tables using identity columns made things a little difficult, as EF ignored the id values and just let SQL Server insert the next identity value. For tables with identity columns I ended up doing the following:
- Read all the records of a given entity
- Order the records by id in ascending order
- set the identity seed for the table to the value of the first id
- keeping track of the next identity value, add the records one by one. If the id is not the same as the expected next identity value set the identity seed to the next required value
As long as the table is empty (or all the new records have ids higher that current hisghest id), and the ids are in ascending order, EF and MS SQL will insert the required ids and neither system will complain.
Here is a bit of code to illustrate:
private void InsertRecords(Person[] people)
{
// setup expected id - presumption: empty table therefore 1
int expectedId = 1;
// now add all people in order of ascending id
foreach(var person in people.OrderBy(p => p.PersonId))
{
// if the current person doesn't have the expected next id
// we need to reseed the identity column of the table
if (person.PersonId != expectedId)
{
// we need to save changes before changing the seed value
_targetContext.SaveChanges();
// change identity seed: set to one less than id
//(SQL Server increments current value and inserts that)
_targetContext.Database.ExecuteSqlCommand(
String.Format("DBCC CHECKIDENT([Person], RESEED, {0}", person.PersonId - 1)
);
// update the expected id to the new value
expectedId = person.PersonId;
}
// now add the person
_targetContext.People.Add(person);
// bump up the expectedId to the next value
// Assumption: increment interval is 1
expectedId++;
}
// now save any pending changes
_targetContext.SaveChanges();
}
Using reflection I was able to write a Load
and a Save
method that worked for all the entities in the DbContext.
It's a bit of a hack, but it allows me to use the standard EF methods for reading and writing entities and overcomes the problem of how to set identity columns to particular values under a set of given circumstances.
I hope this will be of help to someone else faced with a similar problem.