I pull student data from 2 databases. 1 from an online SOAP API which can handle async calls and 1 from a local DB with an older services that doesnt support async.
I compare these databases and write the differences in a local sqlDB through EF.
Problem:
I get double entries in my EF DB. He puts the correct data and amount in arrays inside the method, but it looks like once he hits the db.savechanges() he jumps back up a few line and saves again.
I don't even know where this extra thread comes from.
Some code might be still there from numerous tries to solve it. For instance I tried with addrange but I get an error when he tries to add the FullVarianceList.
public async Task<bool> FullStudentCompare(string date) //format DD/MM/YYYY
{
try
{
//DB context
using (var db = new SchoolDbContext())
{
//GET DATA
//SMT (async)
List<SmtStudent> smtStdudentList = await GetAllSmartschoolStudents();
//Wisa (sync)
//on date, or if emty on current systemdate
List<WisaStudent> wisaList;
if (date == "")
{
wisaList = GetWisaStudentData(DateTime.Now.ToShortDateString());
}
else
{
wisaList = GetWisaStudentData(date);
}
//Flags and props needed for DB entry after compare
bool existsInLocalDb = false;
List<Variance> vList = new List<Variance>();
//Full list to add to DB outside foreach
List<Variance> fullVarianceList = new List<Variance>();
//Full List of new Students to write to DB outside foreach
List<DbStudent> fullStudentList = new List<DbStudent>();
//Compare lists
foreach (WisaStudent wstd in wisaList)
{
//determine correct classCode
string klasCode;
if (wstd.klasgroep.Trim() == "Klasgroep 00")
{
klasCode = wstd.klas.Trim();
}
else
{
klasCode = wstd.klasgroep.Trim();
}
//Create SmtStudent object for compare
SmtStudent tempStd = new SmtStudent(true,
wstd.voornaam.Trim(),
wstd.naam.Trim(),
wstd.stamboeknummer.Trim(),
wstd.geslacht.Trim(),
wstd.geboortedatum.Trim(),
wstd.straat.Trim(),
wstd.huisnummer.Trim(),
wstd.busnummer.Trim(),
wstd.postcode.Trim(),
wstd.gemeente.Trim(),
wstd.emailadres.Trim(),
wstd.GSM_nummer.Trim(),
wstd.levensbeschouwing.Trim(),
wstd.coaccountmoedervoornaam.Trim(),
wstd.coaccountmoedernaam.Trim(),
wstd.coaccountmoederemailadres.Trim(),
wstd.coaccountmoederGSM_nummer.Trim(),
wstd.coaccountvadervoornaam.Trim(),
wstd.coaccountvadernaam.Trim(),
wstd.coaccountvaderemailadres.Trim(),
wstd.coaccountvaderGSM_nummer.Trim(),
klasCode,
wstd.nationaliteit,
wstd.geboorteGemeente,
wstd.geboorteLand
);
//Find matching SmtStudent
SmtStudent smtStd = smtStdudentList.Find(i => i.Internnummer == wstd.stamboeknummer);
//Find matching Std in local DB
DbStudent dbStd = await db.Students.Where(i => i.Stamboeknummer == wstd.stamboeknummer).FirstOrDefaultAsync();
//if none exists in the local DB create an entity to update and write to DB
if (dbStd == null)
{
dbStd = new DbStudent(wstd.voornaam.Trim(),
wstd.naam.Trim(),
wstd.stamboeknummer.Trim(),
wstd.geslacht.Trim(),
wstd.geboortedatum.Trim(),
wstd.straat.Trim(),
wstd.huisnummer.Trim(),
wstd.busnummer.Trim(),
wstd.postcode.Trim(),
wstd.gemeente.Trim(),
wstd.emailadres.Trim(),
wstd.GSM_nummer.Trim(),
wstd.levensbeschouwing.Trim(),
wstd.coaccountmoedervoornaam.Trim(),
wstd.coaccountmoedernaam.Trim(),
wstd.coaccountmoederemailadres.Trim(),
wstd.coaccountmoederGSM_nummer.Trim(),
wstd.coaccountvadervoornaam.Trim(),
wstd.coaccountvadernaam.Trim(),
wstd.coaccountvaderemailadres.Trim(),
wstd.coaccountvaderGSM_nummer.Trim(),
klasCode,
wstd.loopbaanDatum,
wstd.nationaliteit,
wstd.geboorteGemeente,
wstd.geboorteLand
);
db.Students.Add(dbStd);
fullStudentList.Add(dbStd);
}
else
{
existsInLocalDb = true;
}
if (smtStd == null)
{
//Std doesn't exist in Smt -> New student
dbStd.IsNewStudent = true;
dbStd.ClassMovement = true;
//remove from wisaList
wisaList.Remove(wstd);
}
else
{
//clear vlist from previous iterations
vList.Clear();
//get all properties on the obj, cycle through them and find differences
PropertyInfo[] props = smtStd.GetType().GetProperties();
vList.AddRange(props.Select(f => new Variance
{
Property = f.Name,
ValueA = f.GetValue(smtStd),
ValueB = f.GetValue(tempStd),
Checked = false
})
.Where(v => !v.ValueA.Equals(v.ValueB) && v.ValueB != null)
.ToList());
//If the users allrdy exists in LocalDb delete all previously recorded variances
if (existsInLocalDb)
{
if (db.Variances.Where(j => j.Student.StudentId.Equals(dbStd.StudentId)).FirstOrDefault() != null)
{ //if the student allready exists we will recreate the variancelist, hence deleting all current items first
List<Variance> existingList = db.Variances.Where(j => j.Student.StudentId.Equals(dbStd.StudentId)).ToList();
foreach (Variance v in existingList)
{
db.Variances.Remove(v);
}
}
}
//Add new variances if vList is not empty
if (vList.Count > 0)
{
//Check if KlasCode is a variance -> set classmovement to true
if (vList.Where(i => i.Property == "KlasCode").FirstOrDefault() != null)
{
dbStd.ClassMovement = true;
}
else
{
dbStd.ClassMovement = false;
}
//add the StudentObject to the variance to link them 1-many
foreach (Variance v in vList)
{
v.Student = dbStd;
fullVarianceList.Add(v);
db.Variances.Add(v);
}
}
}
}
//add the full lists of variances and new students to DB
//db.Variances.AddRange(fullVarianceList);
//db.Students.AddRange(fullStudentList);
db.SaveChanges();
return true;
}
}
catch(Exception ex)
{
return false;
}
}
A couple of things:
It is important to understand that EF uses a unit of work pattern where none of the changes to the entities are persisted until SaveChanges is called which explains the "once he hits the db.Savechanges() he jumps back up" phenomenon.
When you have a 1 to many relationsship and you assign a collection of entities to a navigation property on another entity and then add that parent entity to the DbContext, EF marks those child entities to be added too. In your case dbStd is added at the line "db.Students.Add(dbStd);" and at the line "v.Student = dbStd;". This is most likely what is causing your duplicates.
Related
first I know this question has been asked but I really couldn't find an answer nor find the root of the problem so maybe a someone points me in the right direction.
I'm having the An entity object cannot be referenced by multiple instances of IEntityChangeTracker. error when trying to save into the log tables.
for the log table, I'm using
https://github.com/thepirat000/Audit.NET/tree/master/src/Audit.EntityFramework
so inside my DbContext class where I define the dbset, I have to override the onscopecreated function
the problem here is that when context.Savechanges run for the first audit record for each table it works but after first record, I get the multiple reference error.
so let's say I have the following tables
Languages table. with the following values
English,French,German
Countries Table with the following values
UK,France,Germany
for languages table, if I change English to English3 and save it works It records to the audit table but then for languages table, I can not do any changes at any records it's the same in every table
what am I missing?
private void SaveToLogTable(AuditScope auditScope)
{
foreach (var entry in ((AuditEventEntityFramework)auditScope.Event).EntityFrameworkEvent.Entries)
{
if(entry.Action is null) return;
if (TABLES.Any(x => x.T_TABLE_NAME.Equals(entry.Table)))
{
var newLog = new LOGS
{
LOG_ACTION = ACTIONS.FirstOrDefault(x => x.A_DESC == entry.Action)?.A_CODE,
LOG_DATE = DateTime.Now,
USERS = MyGlobalSettings.MyUser
};
if (entry.Changes != null)
{
foreach (var changes in entry.Changes)
{
var ch = new CHANGES
{
CH_COLUMN = changes.ColumnName,
CH_NEW_VALUE = changes.NewValue.ToString(),
CH_ORIGINAL_VALUE = changes.OriginalValue.ToString()
};
newLog.CHANGES.Add(ch);
}
}
if (entry.ColumnValues != null)
{
foreach (var kv in entry.ColumnValues)
{
var val = new VALUES
{
ColumnName = kv.Key,
ColumnValue = kv.Value.ToString()
};
newLog.VALUES.Add(val);
}
}
TABLES.First(x => x.T_TABLE_NAME.Equals(entry.Table)).LOGS.Add(newLog);
}
else
{
var table = new TABLES {T_TABLE_NAME = entry.Table};
var newLog = new LOGS
{
LOG_ACTION = ACTIONS.FirstOrDefault(x => x.A_DESC.Equals(entry.Action))?.A_CODE,
LOG_DATE = DateTime.Now,
LOG_USER_REFNO = MyGlobalSettings.MyUser.U_ROWID
//USERS = MyGlobalSettings.MyUser
};
if (entry.Changes != null)
{
foreach (var changes in entry.Changes)
{
var ch = new CHANGES
{
CH_COLUMN = changes.ColumnName,
CH_NEW_VALUE = changes.NewValue.ToString(),
CH_ORIGINAL_VALUE = changes.OriginalValue.ToString()
};
newLog.CHANGES.Add(ch);
}
}
if (entry.ColumnValues != null)
{
foreach (var kv in entry.ColumnValues)
{
var val = new VALUES
{
ColumnName = kv.Key,
ColumnValue = kv.Value is null? "": kv.Value.ToString()
};
newLog.VALUES.Add(val);
}
}
table.LOGS.Add(newLog);
//TABLES.Attach(table);
//TABLES.First(x => x.T_TABLE_NAME.Equals(entry.Table)).LOGS.Add(newLog);
TABLES.Add(table);
//TablesList.Add(table);
}
//entry.Entity
}
}
What is the best way to update multiple records in a list to speed up processing?
Currently, I'm updating about 15000 products, each with 3 different price sets and it takes the whole day to complete.
I need to update the prices all at once in code side, then commit those changes to the database in 1 go, instead of fetching each inventory item, updating its values, then attaching it to the context. Every single fetch is causing the delays.
Code
public void UpdatePricesFromInventoryList(IList<Domain.Tables.Inventory> invList)
{
var db = new UniStockContext();
foreach (var inventory in invList)
{
Domain.Tables.Inventory _inventory = db.Inventories
.Where(x => x.InventoryID == inventory.InventoryID)
.FirstOrDefault();
if (inventory.Cost.HasValue)
_inventory.Cost = inventory.Cost.Value;
else
_inventory.Cost = 0;
foreach (var inventoryPrices in inventory.AccInventoryPrices)
{
foreach (var _inventoryPrices in _inventory.AccInventoryPrices)
{
if (_inventoryPrices.AccInventoryPriceID == inventoryPrices.AccInventoryPriceID)
{
_inventoryPrices.ApplyDiscount = inventoryPrices.ApplyDiscount;
_inventoryPrices.ApplyMarkup = inventoryPrices.ApplyMarkup;
if (inventoryPrices.Price.HasValue)
_inventoryPrices.Price = inventoryPrices.Price.Value;
else
_inventoryPrices.Price = _inventory.Cost;
if (inventoryPrices.OldPrice.HasValue)
{
_inventoryPrices.OldPrice = inventoryPrices.OldPrice;
}
}
}
}
db.Inventories.Attach(_inventory);
db.Entry(_inventory).State = System.Data.Entity.EntityState.Modified;
}
db.SaveChanges();
db.Dispose();
}
I've also tried working my code according to this SOQ Entity Framework update/insert multiple entities
and it gave me and error. Here are the details:
Code:
public void UpdatePricesFromInventoryListBulk(IList<Domain.Tables.Inventory> invList)
{
var accounts = new List<Domain.Tables.Inventory>();
var db = new UniStockContext();
db.Configuration.AutoDetectChangesEnabled = false;
foreach (var inventory in invList)
{
accounts.Add(inventory);
if (accounts.Count % 1000 == 0)
{
db.Set<Domain.Tables.Inventory>().AddRange(accounts);
accounts = new List<Domain.Tables.Inventory>();
db.ChangeTracker.DetectChanges();
db.SaveChanges();
db.Dispose();
db = new UniStockContext();
}
}
db.Set<Domain.Tables.Inventory>().AddRange(accounts);
db.ChangeTracker.DetectChanges();
db.SaveChanges();
db.Dispose();
}
Error:
An entity object cannot be referenced by multiple instances of IEntityChangeTracker.
I would suggest changing the following:
Domain.Tables.Inventory _inventory = db.Inventories
.Where(x => x.InventoryID == inventory.InventoryID)
.FirstOrDefault();
To
Domain.Tables.Inventory _inventory = db.Inventories
.Single(x => x.InventoryID == inventory.InventoryID);
I'd still add the db.Configuration.AutoDetectChangesEnabled = false; after getting the context, and also use AsNoTracking:
Turn off EF change tracking for any instance of the context
that is because you are hit the database context at every loop to increase the performance you should get all the Inventories by one hit ,this is your problem try the below code and you will notice the performance :
public void UpdatePricesFromInventoryList(IList<Domain.Tables.Inventory> invList)
{
var db = new UniStockContext();
invIdsArray = invList.select(x => x.InventoryID).ToArray();
IList<Domain.Tables.Inventory> invListFromDbByOneHit = db.Inventories.Where(x => invIdsArray.Contains(x.InventoryID)).Tolist();
foreach (var inventory in invListFromDbByOneHit)
{
//Domain.Tables.Inventory _inventory = db.Inventories
//.Where(x => x.InventoryID == inventory.InventoryID)
//.FirstOrDefault();
if (inventory.Cost.HasValue)
_inventory.Cost = inventory.Cost.Value;
else
_inventory.Cost = 0;
foreach (var inventoryPrices in inventory.AccInventoryPrices)
{
foreach (var _inventoryPrices in _inventory.AccInventoryPrices)
{
if (_inventoryPrices.AccInventoryPriceID == inventoryPrices.AccInventoryPriceID)
{
_inventoryPrices.ApplyDiscount = inventoryPrices.ApplyDiscount;
_inventoryPrices.ApplyMarkup = inventoryPrices.ApplyMarkup;
if (inventoryPrices.Price.HasValue)
_inventoryPrices.Price = inventoryPrices.Price.Value;
else
_inventoryPrices.Price = _inventory.Cost;
if (inventoryPrices.OldPrice.HasValue)
{
_inventoryPrices.OldPrice = inventoryPrices.OldPrice;
}
}
}
}
db.Inventories.Attach(_inventory);
db.Entry(_inventory).State = System.Data.Entity.EntityState.Modified;
}
db.SaveChanges();
db.Dispose();
}
I have method in controller
It receive data from post request and write to table
Here is code
[ResponseType(typeof(TimeTable))]
public IHttpActionResult PostTimeTable(TimeTable timeTable)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (ModelState.IsValid)
{
DateTime dt = DateTime.Today;
TimeTable c = (from x in db.TimeTables
where x.Company == timeTable.Company && x.INN == timeTable.INN
select x).First();
c.StartPause = timeTable.StartPause;
c.StartDay = timeTable.StartDay;
c.EndPause = timeTable.EndPause;
c.EndDay = timeTable.EndDay;
db.SaveChanges();
}
db.TimeTables.Add(timeTable);
db.SaveChanges();
return CreatedAtRoute("DefaultApi", new { id = timeTable.Id }, timeTable);
}
But it works well when record with INN and Company already in db.
But if it not in database I need to create new entry.
How I need to modify this method?
You can use a flag (exisingCompanyFlag) for edit mode or add new mode like this
bool existingCompanyFlag = true;
TimeTable c = (from x in db.TimeTables
where x.Company == timeTable.Company && x.INN == timeTable.INN
select x).FirstOrDefult();
if (c == null)
{
existingCompanyFlag = false;
c = new TimeTable();
}
c.StartPause = timeTable.StartPause;
c.StartDay = timeTable.StartDay;
c.EndPause = timeTable.EndPause;
c.EndDay = timeTable.EndDay;
if (!existingCompanyFlag)
db.TimeTables.Add(c);
You need a separate branch in your code for the insert case.
if (ModelState.IsValid) {
if (addingNewRow) {
TimeTable tt = new TimeTable {
// Populate properties (except identity columns)
};
db.TimeTables.Add(tt);
} else {
// update
}
db.SaveChanges();
}
To link to other entities use one of:
Assign instances:
x.Company = theCompany;
or, assign the instance id
x.CompanyId = companyId;
(#1 is easier if you already have the other entity loaded or are creating it – EF will sort out the ids – while #2 saves loading the whole other entity.)
I have the following code which updates / adds data to a SQL Server CE database with EF6, which is working fine for small number of records. However when the volume of records exceeds 1000~2000 the transaction become very slow (10~15sec). Is there any way to optimize it?
Database.DefaultConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0", "", MyProject.ConnectionString);
ProjectContext context = new ProjectContext();
context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ValidateOnSaveEnabled = false;
using (var db = new ProjectContext())
{
foreach (var item in MyProject.Brands)
{
if (!db.Brands.Any(i => i.Name == item.Name))
{
// Add
db.Brands.Add(item);
}
else
{
// Update
var found = db.Brands.First(i => i.Name == item.Name);
found = item;
}
}
db.SaveChangesAsync();
This way you save 50% on your Linq queries, and your update might actually work (your MyProject.Brands are not attached in any way to your new ProjectContext that you are using, the found that you extract from there is attached, but in your original code, you overwrote it with yuor item, meaning it will be ignored.
context I have left out completely, as you were not doing anything with it, as your using creates a new ProjectContext anyway.
using (var db = new ProjectContext())
{
foreach (var item in MyProject.Brands)
{
var found = db.Brands.FirstOrdefault(i => i.Name == item.Name);
if (found == null)
{
// Add
db.Brands.Add(item);
}
else
{
// Update
found.prop1 = item.prop1;
found.prop2 = item.prop2;
// ... etc, for all and any updatable properties
}
}
db.SaveChangesAsync();
}
I have a ProductLevel table in an SQL database. It contains the products for a store. I want to copy these records into a ProductLevelDaily table at the time the user logs onto the hand held device in the morning.
As they scan items the bool goes from false to true so at anytime they can see what items are left to scan/check.
From the mobile device I pass the siteID and date to the server:
int userID = int.Parse(oWebRequest.requestData[5]); and a few other things
IEnumerable<dProductLevelDaily> plditems
= DSOLDAL.CheckProductDailyLevelbySiteCount(siteID, currentDate);
This checks if there are any records already moved into this table for this store. Being the first time this table should be empty or contain no records for this store on this date.
if (plditems.Count() == 0) // is 0
{
IEnumerable<dProductLevel> ppitems = DSOLDAL.GetProductsbySite(siteID);
// this gets the products for this store
if (ppitems.Count() > 0)
{
dProduct pi = new dProduct();
foreach (dProductLevel pl in ppitems)
{
// get the product
pi = DSOLDAL.getProductByID(pl.productID, companyID);
dProductLevelDaily pld = new dProductLevelDaily();
pld.guid = Guid.NewGuid();
pld.siteID = siteID;
pld.statusID = 1;
pld.companyID = companyID;
pld.counted = false;
pld.createDate = DateTime.Now;
pld.createUser = userID;
pld.productID = pl.productID;
pld.name = "1000"; // pi.name;
pld.description = "desc"; // pi.description;
DSOLDAL.insertProductLevelDailyBySite(pld);
}
}
}
On the PDA the weberequest response returns NULL
I can't see what the problem is and why it wont work.
The insert is in DSOLDAL:
public static void insertProductLevelDailyBySite(dProductLevelDaily pld)
{
dSOLDataContext dc = new dSOLDataContext();
try
{
dc.dProductLevelDailies.InsertOnSubmit(pld);
// dProductLevelDailies.Attach(pld, true);
dc.SubmitChanges();
}
catch (Exception exc)
{
throw new Exception(getExceptionMessage(exc.Message));
}
finally
{
dc = null;
}
}
This code works until I put the foreach loop inside with the insert
IEnumerable<dProductLevelDaily> plditems
= DSOLDAL.CheckProductDailyLevelbySiteCount(siteID, s);
if (plditems.Count() == 0) // plditems.Count() < 0)
{
IEnumerable<dProductLevel> ppitems = DSOLDAL.GetProductsbySite(siteID);
if (ppitems.Count() > 0)
{
oWebResponse.count = ppitems.Count().ToString();
oWebResponse.status = "OK";
}
else
{
oWebResponse.count = ppitems.Count().ToString();
oWebResponse.status = "OK";
}
}
else
{
oWebResponse.count = "2"; // plditems.Count().ToString();
oWebResponse.status = "OK";
}
These kind of bulk operations aren't very well matched to what Linq-to-SQL does.
In my opinion, I'd do this using a stored procedure, which you could include in your Linq-to-SQL DataContext and call from there.
That would also leave the data on the server and just copy it from one table to the other, instead of pulling down all data to your client and re-uploading it to the server.
Linq-to-SQL is a great tool - for manipulating single objects or small sets. It's less well suited for bulk operations.