Entity Framework 6 Code One: How to seed data using "circular" relationships and store-generated columns?

I am new to EF Code First and I am trying to seed my database with some data using code first migrations. So far I've managed to solve a few errors, but now I'm stuck and couldn't find an answer. I have two problems while updating the database from my code.

I have several objects that have different many-to-many and one-to-one relationships, and some end up creating a circle. I'm not sure if this is the reason for the second problem or not when I try to seed the database.

  • First error: A dependent property in a ReferentialConstraint is mapped to a store-generated column. Column: 'LicenseId'.

Is there a way to use the db generated id as a foreign key? Is it just the order in which I create / insert objects? (see seeding code below)

If I do not use [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]

in the License, I get an error that I am unable to implicitly insert an identifier that is not generated by the database.

public class License : Entity
{
    [Key, ForeignKey("Customer")]
    [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
    public int LicenseId { get; set; }

    [Required(ErrorMessage = "Date Created name is required")]
    public DateTime DateCreated { get; set; }

    public virtual ICollection<ProductLicense> ProductLicenses { get; set; } // one License has many ProductLicenses
    public virtual Customer Customer { get; set; } // one Customer has one License
}

public class Customer : Entity
{
    [Key]
    public int CustomerId { get; set;}

    [Required(ErrorMessage = "Username is required")]
    [Column(TypeName = "nvarchar")]
    [MaxLength(500)]
    public string Name { get; set; }

    public virtual ICollection<User> Users { get; set; } // one Customer has many users
    public virtual License License { get; set; } // one Customer has one License
    //public virtual ICollection<License> License { get; set; } // one Customer has many Licenses
}

      

  1. If I change License-Customer to be a many-to-many relationship (which I don't want), I get the following error: Cannot insert duplicate key row in object 'dbo.Users' with unique index 'IX_Username'.

This is all my relationship:

Customer -many-> User -many-> Role -many-> Permission -one- ProductLicense <-many- License -one- Customer

      

Here is the code I used in protected override void Seed(DAL.Models.Context context)

creating objects in code and then using AddOrUpdate:

// Default Customers - create
var customers = new[]{
    new Customer { Name = "cust_a" },
    new Customer { Name = "cust_b" },
    new Customer { Name = "cust_c" }
};

// Default Licenses - create
var licenses = new[]{
    new License { DateCreated = DateTime.Now.AddDays(-3), Customer = customers[0] },
    new License { DateCreated = DateTime.Now.AddDays(-2), Customer = customers[1] },
    new License { DateCreated = DateTime.Now.AddDays(-1), Customer = customers[2] }
};

// Default ProductLicenses - create, and add default licenses
var productLicenses = new[]{
    new ProductLicense { LicenceType = LicenseType.Annual, StartDate = DateTime.Now, License = licenses[0] },
    new ProductLicense { LicenceType = LicenseType.Monthly, StartDate = DateTime.Now, License = licenses[1] },
    new ProductLicense { LicenceType = LicenseType.PAYG, StartDate = DateTime.Now, License = licenses[2] }
    };

// Default Permissions - create, and add default product licenses
var permissions = new[]{
    new Permission { Name = "Super_a", ProductLicense = productLicenses[0] },
    new Permission { Name = "Access_b", ProductLicense = productLicenses[1] },
    new Permission { Name = "Access_c", ProductLicense = productLicenses[2] }
};

// Default Roles - create, and add default permissions
var roles = new[]{
    new Role { Name = "Super_a", Permissions = permissions.Where(x => x.Name.Contains("_a")).ToList() },
    new Role { Name = "User_b", Permissions = permissions.Where(x => x.Name.Contains("_b")).ToList() },
    new Role { Name = "User_c", Permissions = permissions.Where(x => x.Name.Contains("_c")).ToList() }
};

// Default Users - create, and add default roles
var users = new[]{
    new User { Username = "user@_a.com", Password = GenerateDefaultPasswordHash(), Salt = _defaultSalt, Roles = roles.Where(x => x.Name.Contains("_a")).ToList() },
    new User { Username = "user@_b.co.uk", Password = GenerateDefaultPasswordHash(), Salt = _defaultSalt, Roles = roles.Where(x => x.Name.Contains("_b")).ToList() },
    new User { Username = "user@_c.com", Password = GenerateDefaultPasswordHash(), Salt = _defaultSalt, Roles = roles.Where(x => x.Name.Contains("_c")).ToList() }
        };

// Default Customers - insert, with default users

foreach (var c in customers)
{
    c.Users = users.Where(x => x.Username.Contains(c.Name.ToLower())).ToList();
    context.Customers.AddOrUpdate(c);
}

      

I also tried changing the first part after //Default Customers - create

to

// Default Customers - create and insert
context.Customers.AddOrUpdate(
    u => u.Name,
    new Customer { Name = "C6" },
    new Customer { Name = "RAC" },
    new Customer { Name = "HSBC" }
);

context.SaveChanges();

var customers = context.Customers.ToList();

      

I would really appreciate help on this so that I can seed my database with the appropriate data.

Thanks a lot for your help and sorry for the long post.

--- UPDATE ---

After following Chris Pratt's excellent answer below, I get the following error: Conflicting changes detected. This may happen when trying to insert multiple entities with the same key.

Here is my new code for seeding and all models used: https://gist.github.com/jacquibo/c19deb492ec3fff0b5a7

Can anyone help with this?

+3


source to share


3 answers


Seeding can be a little tricky, but you just need to keep a few things in mind:

  • Anything added with AddOrUpdate

    will be added or updated. But everything else will be added, not updated. Only in your code that you use AddOrUpdate

    to: Customer

    .

  • EF will add related items when an item is AddOrUpdate

    added, but it ignores those relationships when that item is updated.

Based on this, if you are adding object hierarchies like this, then you need a little care. First, you need to call SaveChanges

between levels of the hierarchy, and second, you need to work with IDs, not relationships. For example:

var customers = new[]{
    new Customer { Name = "cust_a" },
    new Customer { Name = "cust_b" },
    new Customer { Name = "cust_c" }
};
context.Customers.AddOrUpdate(r => r.Name, customers[0], customers[1], customers[2]);
context.SaveChanges()

      

Now you have everything your customers care about, and each of them will make a difference to their ID, one way or another. Then start digging into your hierarchy:

// Default Licenses - create
var licenses = new[]{
    new License { DateCreated = DateTime.Now.AddDays(-3), CustomerId = customers[0].CustomerId },
    new License { DateCreated = DateTime.Now.AddDays(-2), CustomerId = customers[1].CustomerId },
    new License { DateCreated = DateTime.Now.AddDays(-1), CustomerId = customers[2].CustomerId }
};
context.Licenses.AddOrUpdatE(r => r.DateCreated, licenses[0], licenses[1], licenses[2]);

      

You do not need to call SaveChanges

if you are dealing with objects at the same level in the hierarchy until you have defined all of them. However, if you have an object that refers to something like License

, then you want to call again SaveChanges

before adding them.



Also note that I am switching to setting id instead of relationship. Doing it this way will allow you to update the relationship later by changing the identifier. For example:

// Changed customer id from customer[1] to customer[0]
new License { DateCreated = DateTime.Now.AddDays(-2), CustomerId = customers[0].CustomerId },

      

Whereas the following won't work:

// Attempted to change customer[1] to customer[0], but EF ignores this in the update.
new License { DateCreated = DateTime.Now.AddDays(-2), Customer = customers[0] },

      

Of course, you don't actually have the property CustomerId

on License

, but you should. While EF will automatically generate a column to preserve the relationship without it, you can never get a foreign key value without an explicit property, and for several reasons but one, being able to work with the actual foreign key is extremely useful. Just follow this convention for all your reference properties:

[ForeignKey("Customer")]
public int CustomerId { get; set;}
public virtual Customer Customer { get; set; }

      

The attribute is ForeignKey

not always required depending on whether your foreign keys and property reference names match the EF conventions for things like this, but I find it easier and less error prone to just be explicit about my intentions.

+12


source


The foreign key must refer to the candidate key of another table (usually a PK that the DB can generate). The FK depends on values ​​in another table and clearly cannot be generated by the table that owns it.

What you want to do is called "Shared Primary Key". It looks like you were close - you just have ids generation back and are missing ForeignKeyAttribute

on the required object.

If License

is a dependent object, you want it to look like this:



public class License : Entity
{
    [Key, ForeignKey("Customer")]
    public int LicenseId { get; set; } // consider renaming to CustomerId

    // other properties here
    ...

    public virtual Customer Customer { get; set; }
}

public class Customer : Entity
{
    [Key, ForeignKey( "License" )]
    [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
    public int CustomerId { get; set;}

    // other properties here
    ...

    public virtual License License { get; set; }
}

      

If you so desire, this can be used for Table Splitting

where you share the fields of one DB table between two or more objects (you can do this with the above by specifying the same table name for each object either through calls TableAttribute

or FluentAPI). This is useful if you have a large field that you want to selectively load, for example.

0


source


The first code migrations do not consider consistency updates to your current database records when creating a new migration script. You may need to add additional SQL commands to the Up () method of the migration file using Sql ("...") functions that will update the current database rows before attempting to apply your migration.

Suppose you want to apply a new reference constraint in the database. You will not receive an error message when generating the migration file, even if your referenced file does not contain the appropriate lines. But you are getting error 1 (your first message) when you execute the script in the Package Manager Console.

For this purpose, you must add Sql ("INSERT INTO ....") and / or Sql ("UPDATE .....") commands at the top of your Up () method, which will change your current database rows with the correct values.

Likewise, you may have to delete some records from the database depending on your structural changes.

0


source







All Articles