I am using the Code First approach with my project.
Here is my object:
public class Account
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid ID { get; set; }
public string name { get;set; }
}
If I create a new record it is OK:
using(var context = new context())
{
context.Accounts.add(account);
context.savechanges(); //This saves fine
}
But when I change a property it saves another record in the database:
using (var context = new context())
{
var account = context.Account.where(x => x.ID == GUID).FirstOrDefault();
if (account != null)
{
account.name = "UpdatedName";
context.savechanges(); // This creates a new record!!
}
}
I am fairly new to Entity framework; why is it creating new records each time? Is it the attribute on the ID? If it is, then how can I use GUIDS as IDs instead of integers?
The attribute in your Account option should work fine to set up the ID column as the primary key for your objects.
If you are getting a new entry added to the database when you save changes, it is almost certainly the result of you having changed the primary key (ID property) of the object after having received it from the DB. Maybe you are trying to set the GUID property yourself in some piece of code that you haven't included? (You should be letting the DB assign it).
In any case, this simple console app uses your setup and works as expected. If you don't see an obvious place in your code where you are changing the GUID, maybe you can post your actual code? (I notice a couple of typos in what is pasted in, so it doesn't appear to be what you are actually using)
public class Account
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid ID { get; set; }
public string name { get; set; }
}
public class MyContext : DbContext
{
public DbSet<Account> Accounts { get; set; }
}
class Program
{
static Guid MyGuid = Guid.Empty;
static void Main(string[] args)
{
using (var context = new MyContext())
{
Account account = new Account { name = "OldName" };
context.Accounts.Add( account );
context.SaveChanges();
MyGuid = account.ID;
}
using (var context = new MyContext())
{
var account = context.Accounts.Where(x => x.ID == MyGuid).FirstOrDefault();
if (account != null)
{
account.name = "UpdatedName";
context.SaveChanges();
}
}
}
}
Related
I am trying to link Shipments to Road using a clean database, with fresh data unlinked, first time trying to relate these 2 entities.
public class Road
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public ShipmentMode ShipmentMode { get; set; }
public string RoadName { get; set; }
public virtual List<Shipment> Shipments { get; set; }
}
public void SaveToDatabase()
{
using (var db = new DbContext())
{
foreach (var road in this.Roads)
{
road.Shipments.ForEach(shipment => shipment = db.Shipments.FirstOrDefault(s => s.Id == shipment.Id));
var input = db.Roads.Add(road);
}
db.SaveChanges();
}
}
At the line var input = db.Roads.Add(road); it will throw the error Message "The instance of entity type 'Shipment' cannot be tracked because another instance with the key value '{Id: 46}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.'.
I had this error before, fixed it but I rearranged the code and now it's back to throwing this error. I am just trying to get Shipments to link to Road.
This code below has worked for me. If anyone has a better solution, please feel free to post.
public void SaveToDatabase()
{
using (var db = new DbContext())
{
foreach (var road in this.Roads)
{
var shipments = road.Shipments;
road.Shipments = null;
var input = db.Roads.Add(road);
db.SaveChanges();
var getRoad = db.Lanes.Include(p => p.Shipments).FirstOrDefault(t => t.Id == input.Entity.Id);
getRoad.Shipments.AddRange(shipments);
db.SaveChanges();
}
}
}
Alternative solution
public void SaveToDatabase()
{
using (var db = new DbContext())
{
foreach (var road in this.Roads)
{
db.UpdateRange(road.Shipments);
var input = db.Roads.Add(road);
db.SaveChanges();
}
}
}
So I'm using Entity Framework Core to build a database of Guilds (Another name for Discord Servers) and Users, with the Discord.NET Library. Each Guild has many users, and each user can be in many guilds. First time using EF and I'm having some teething issues. The two classes are:
public class Guild
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public ulong Snowflake { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public string Name { get; set; }
public ICollection<User> Users { get; set; }
}
public class User
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public ulong Snowflake { get; set; }
public string Username { get; set; }
public ushort DiscriminatorValue { get; set; }
public string AvatarId { get; set; }
public ICollection<Guild> Guilds { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}
With the goal of having 3 tables: Guild, Users, and GuildUsers. This is my current function for getting the guilds:
using var context = new AutomataContext();
var discordGuilds = this.client.Guilds.ToList();
var dbGuilds = context.Guilds;
List<Guild> internalGuilds = discordGuilds.Select(g => new Guild
{
Snowflake = g.Id,
Name = g.Name,
CreatedAt = g.CreatedAt,
Users = g.Users.Select(gu => new User
{
Id = context.Users.AsNoTracking().FirstOrDefault(u => u.Snowflake == gu.Id)?.Id ?? default(int),
}).ToList(),
}).ToList();
// Upsert Guilds to db set.
foreach (var guild in internalGuilds)
{
var existingDbGuild = dbGuilds.AsNoTracking().FirstOrDefault(g => g.Snowflake == guild.Snowflake);
if (existingDbGuild != null)
{
guild.Id = existingDbGuild.Id;
dbGuilds.Update(guild); // Hits the second Update here and crashes
}
else
{
dbGuilds.Add(guild);
}
}
await context.SaveChangesAsync();
I should note, a 'snowflake' is a unique ID that discord uses, but I wanted to keep my own unique ID for each table.
High level overview, guilds are collected into Discord.NET models. These are then transformed into internalGuilds (my guild class, which includes the list of users). Each of these is looped through and upserted to the database.
The issue arises in the second guild loop, where an error is thrown in the "Update" that a User ID is already being tracked (Inside the guild). So the nested ID is already being tracked? Not sure what's going on here, any help would be appreciated. Thanks.
This exception is most likely occurring because you are loading Users without tracking then looping through and potentially trying to update or insert guilds /w the same user reference, especially using the Update method.
I would suggest removing the use of AsNoTracking. Working with detached entity references via AsNoTracking is more of a performance tweak for when reading large amounts of data. You can pre-fetch all of the User references by their snowflake:
using (var context = new AutomataContext())
{
var discordGuilds = this.client.Guilds.ToList();
// Get the user snowflakes from the guilds, and pre-fetch them.
var userSnowflakes = discordGuilds.SelectMany(g => g.Users.Select(u => u.Id)).ToList();
var users = await context.Users
.Where(x => userSnowflakes.Contains(x.Snowflake))
.ToListAsync();
// We need to add references for any New user snowflakes.
var existingSnowflakes = users.Select(x => x.Snowflake).ToList();
// If more detail is needed for new user records, it will need to be fetched from the passed in Guild.User.
var newUsers = userSnowflakes.Except(existingSnowFlakes)
.Select(x => new User { SnowflakeId = x }).ToList();
if(newUsers.Any())
users.AddRange(newUsers);
List<Guild> internalGuilds = discordGuilds.Select(g => new Guild
{
Snowflake = g.Id,
Name = g.Name,
CreatedAt = g.CreatedAt,
Users = g.Users
.Select(gu => users.Single(u => u.Snowflake == gu.Id))
.ToList(),
}).ToList(),
// Upsert Guilds to db set.
foreach (var guild in internalGuilds)
{
var existingGuildId = context.Guilds
.Where(x => x.Snowflake == guild.Snowflake)
.Select(x => x.Id)
.SingleOrDefault();
if (existingGuildId != 0)
{
guild.Id = existingGuildId;
dbGuilds.Update(guild);
}
else
{
dbGuilds.Add(guild);
}
}
await context.SaveChangesAsync();
This should help ensure that the User references for existing users are pointing at the same instances, whether existing users or new user references that will be associated to the DbContext when first referenced.
Ultimately I don't recommend using Update for "Upsert" scenarios, instead since the Db Record needs to be fetched anyways, updating values on the fetched instance or inserting a new one. Update will want to send all fields from an entity to the database each time, rather than just sending what has changed. It means enforcing a bit more control over what can possibly be changed vs. what should not be.
first of all, sorry for my bad English
I have the following entities
public class User
{
public virtual int Id { get; set; }
public virtual Application Application { get; set; }
public User ()
{
Application = new Application ();
}
}
UserMap
public class UserMap : ClassMap<User>
{
public UserMap ()
{
Table ("Users");
Id (p => p.Id);
References (x => x.Application).Cascade.SaveUpdate ();
}
}
Application
public class Application
{
public virtual int Id { get; set; }
public virtual string ApplicationName { get; set; }
}
ApplicationMap
public ApplicationMap ()
{
Table ("Applications");
Id (x => x.Id);
Map (x => x.ApplicationName);
}
I received this json
{
"Application": 1,
}
and save the object this way
var user = new User();
user.Application.Id = Cast.To<int>(userModel.Application);
userService.Add(user);
userService.Commit();
Correctly records the data in the table "users" but left blank, the "ApplicationName" table field "application"
I think the error is in this line (user.Application.Id = Cast.To (userModel.Application);)
because I did not set "ApplicationName" field
but if I get the id as a parameter, I will need to get the application object by id, and assign the user object?.
thank you very much
You are right, conversion from ID into Entity (Applicaton) will require call to data layer and its operation GetById().
session.Load<Application>(id)
In cases, that we can be sure, that the passed Application ID exists, NHibernate has a dedicated way how to convert ID into its ENTITY. It is a Load() method, which does NOT hit the DB, just creates a proxy with provided ID, and the operation will succeed:
var user = new User();
var applicationId = Cast.To<int>(userModel.Application);
// behind is session.Load<Aplication>(applicaitonId)
var application = applicationService.Load(applicationId);
user.Application = application;
userService.Add(user);
session.Get<Application>(id)
The alternative is a Get() method, which always loads the instance by ID, i.e. hits the DB. If the ID does not match any ID, null is returned. Advantage in this scenario is, that we can even change the referenced ApplicaitonName (if application exists)
var user = new User();
var applicationId = Cast.To<int>(userModel.Application);
// behind is session.Get<Aplication>(applicaitonId)
var application = applicationService.GetById(applicationId);
if(application !== null)
{
application.ApplicationName = ... // here we can even change that;
user.Application = application;
}
cascade for new.
In case we would recieve brand new object
{
"Application": { "ApplicaitonName" : ... }
}
We can create one, and because of Cascading setting above, it will work as well
user.Application = new Appliation
{
ApplicationName = ...,
}
I have the two following models and DbContext:
public class TestDbContext : DbContext
{
public IDbSet<Person> People { get; set; }
public IDbSet<Car> Cars { get; set; }
}
public class Person
{
public Person()
{
ID = Guid.NewGuid();
}
public Guid ID { get; set; }
public string Name { get; set; }
public virtual List<Car> Cars { get; set; }
}
public class Car
{
public Car()
{
ID = Guid.NewGuid();
}
public Guid ID { get; set; }
public string Name { get; set; }
public virtual Person Owner { get; set; }
}
I then declare a list of people and a list of cars, setting the owner of the first car to the first person in the list:
List<Person> People = new List<Person>()
{
new Person() {Name = "bill", ID = new Guid("6F39CC2B-1A09-4E27-B803-1304AFDB23E3")},
new Person() {Name = "ben", ID = new Guid("3EAE0303-39D9-4FD9-AF39-EC6DC73F630B")}
};
List<Car> Cars = new List<Car>() { new Car() { Name = "Ford", Owner = People[0], ID = new Guid("625FAB6B-1D56-4F57-8C98-F9346F1BBBE4") } };
I save this off to the database using the following code and this works fine.
using (TestDbContext context = new TestDbContext())
{
foreach (Person person in People)
{
if (!(context.People.Any(p => p.ID == person.ID)))
context.People.Add(person);
else
{
context.People.Attach(person);
context.Entry<Person>(person).State = System.Data.EntityState.Modified;
}
}
foreach (Car caar in Cars)
{
if (!(context.Cars.Any(c => c.ID == caar.ID)))
context.Cars.Add(caar);
else
{
context.Cars.Attach(caar);
context.Entry<Car>(caar).State = System.Data.EntityState.Modified;
}
}
context.SaveChanges();
}
If I then change the owner of the car to the second person and run the code again, the Car owner property doesn't get updated.
Cars[0].Owner = People[1];
Any ideas to what I'm doing wrong? Thanks for any help.
I believe this is a problem of independent vs foreign key association. You are using independent association at the moment and the relation between car and person is actually managed by separate entry object which has its own state (to access this object you must use ObjectContext API). Setting the car's entity entry to modified state will not change the state of the entry for the relation! The simple solution is to use foreign key association instead which means adding new Guid PersonId property to your car and map it as foreign key property for Person navigation property.
If you insist on using independent associations you should change relations only on attached entities otherwise you will had a strong headache with tracking those changes and setting all required entries with correct state. It should be enough to create objects, attach them to context and only after that set the owner of the car - hopefully it will be tracked as a change.
Try something like this :
using (TestDbContext context = new TestDbContext())
{
foreach (Person person in People)
{
if (!(context.People.Any(p => p.ID == person.ID)))
context.People.Add(person);
else
{
context.People.Attach(person);
context.Entry<Person>(person).State = System.Data.EntityState.Modified;
}
context.SaveChanges();
}
foreach (Car caar in Cars)
{
if (!(context.Cars.Any(c => c.ID == caar.ID)))
context.Cars.Add(caar);
else
{
context.Cars.Attach(caar);
context.Entry<Car>(caar).State = System.Data.EntityState.Modified;
}
context.SaveChanges();
}
}
I think your error is due to the context.SaveChanges placements (and partialy your architecture). Consider using a dedicated method (a basic CRUD for instance) for each operation on your DB via Entity Framework. Hope this helps.
Edit : With a CRUD approch :
public class PersonManager // CRUD
{
public void Create(Person person)
{
using (TestDbContext context = new TestDbContext())
{
context.Person.Add(person);
context.SaveChanges();
}
}
public void Update(Person person)
{
using (TestDbContext context = new TestDbContext())
{
context.Person.Attach(person);
context.Entry(person).State = System.Data.EntityState.Modified;
context.SaveChanges();
}
}
}
You could also make this class static in order to fit to your architecture.
Edit: Updated problem description based on testing - 12 Sep 2011.
I have this query that throws a NotSupportedException ("Specified method is not supported.") whenever I call .ToList().
IQueryable<FileDefinition> query = db
.FileDefinitions
.Include(x => x.DefinitionChangeLogs)
.Include(x => x.FieldDefinitions.Select(y => y.DefinitionChangeLogs)) // bad
.Include(x => x.FieldDefinitions.Select(y => y.FieldValidationTables)) // bad
.Where(x => x.IsActive);
List<FileDefinition> retval = query.ToList();
If I comment out either line that I have commented as "bad", then the query works. I have also tried including different nested entities in my object model with the same effect. Including any 2 will cause a crash. By nested, I mean a navigation property of a navigation property. I also tried using the .Include methods with a string path: same result.
My table structure looks like this:
This is using MySQL 5.1 (InnoDB tables obviously) as the database store with MySQL Connector/NET 6.3.4.
So my question is: Why doesn't this work?
Note: I can get it to work if I explicitly load the related entities like in this link. But I want to know why EF hates my data model.
ANSWER: MySQL Connector is apparently not capable of handling the 2nd nested entity include. It throws the NotSupportedException, not .NET EF. This same error was also present when I tried this using EF4.0, but my research at the time led me to believe it was self-tracking entities causing the issue. I tried upgrading to latest Connector, but it started causing an Out of Sync error. This is yet another reason for me to hate MySQL.
Maybe a little late to the party but i found the following workaround fairly useful in a current project:
IQueryable<FileDefinition> query = db.FileDefinitions
.Include(x => x.FieldDefinitions.Select(y => y.DefinitionChangeLogs.Select(z => z.FieldDefinition.FieldValidationTables)))
Where rather than using a second row of includes, use Select to get back to the original navigation property and another Select to go forwards to the property you need to include.
I have made a little console application to test your scenario and this test application works:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
namespace EFIncludeTest
{
public class Parent
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<ChildLevel1> ChildLevel1s { get; set; }
}
public class ChildLevel1
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<ChildLevel2a> ChildLevel2as { get; set; }
public ICollection<ChildLevel2b> ChildLevel2bs { get; set; }
}
public class ChildLevel2a
{
public int Id { get; set; }
public string Name { get; set; }
}
public class ChildLevel2b
{
public int Id { get; set; }
public string Name { get; set; }
}
public class MyContext : DbContext
{
public DbSet<Parent> Parents { get; set; }
}
class Program
{
static void Main(string[] args)
{
// Create entities to test
using (var ctx = new MyContext())
{
var parent = new Parent
{
Name = "Parent",
ChildLevel1s = new List<ChildLevel1>
{
new ChildLevel1
{
Name = "FirstChildLevel1",
ChildLevel2as = new List<ChildLevel2a>
{
new ChildLevel2a { Name = "FirstChildLevel2a" },
new ChildLevel2a { Name = "SecondChildLevel2a" }
},
ChildLevel2bs = new List<ChildLevel2b>
{
new ChildLevel2b { Name = "FirstChildLevel2b" },
new ChildLevel2b { Name = "SecondChildLevel2b" }
}
},
new ChildLevel1
{
Name = "SecondChildLevel1",
ChildLevel2as = new List<ChildLevel2a>
{
new ChildLevel2a { Name = "ThirdChildLevel2a" },
new ChildLevel2a { Name = "ForthChildLevel2a" }
},
ChildLevel2bs = new List<ChildLevel2b>
{
new ChildLevel2b { Name = "ThirdChildLevel2b" },
new ChildLevel2b { Name = "ForthChildLevel2b" }
}
},
}
};
ctx.Parents.Add(parent);
ctx.SaveChanges();
}
// Retrieve in new context
using (var ctx = new MyContext())
{
var parents = ctx.Parents
.Include(p => p.ChildLevel1s.Select(c => c.ChildLevel2as))
.Include(p => p.ChildLevel1s.Select(c => c.ChildLevel2bs))
.Where(p => p.Name == "Parent")
.ToList();
// No exception occurs
// Check in debugger: all children are loaded
Console.ReadLine();
}
}
}
}
My understanding was that this basically represents your model and the query you are trying (taking also your comments to your question into account). But somewhere must be an important difference which is not visible in the code snippets in your question and which makes your model fail to work.
Edit
I have tested the working console application above with MS SQL provider (SQL Server 2008 R2 Express DB), not MySQL Connector. Apparently this was the "important difference".