Entity Framework 6 Code first:如何使用“循环”关系和存储生成的列来播种数据?

Posted

技术标签:

【中文标题】Entity Framework 6 Code first:如何使用“循环”关系和存储生成的列来播种数据?【英文标题】:Entity Framework 6 Code first: How to seed data with a 'circular' relationship and store-generated columns? 【发布时间】:2014-12-20 12:26:34 【问题描述】:

我是 EF Code First 的新手,我正在尝试使用代码优先迁移为我的数据库添加一些数据。到目前为止,我已经设法解决了几个错误,但现在我被卡住了,无法找到答案。从我的代码更新数据库时,我遇到了两个问题。

我有几个对象,它们具有各种多对多和一对一的关系,有些对象最终会创建一个圆圈。当我尝试为数据库播种时,我不确定这是否是第二个问题的原因。

    我遇到的第一个错误是:A dependent property in a ReferentialConstraint is mapped to a store-generated column. Column: 'LicenseId'.

有没有办法可以使用 db 生成的 id 作为外键?仅仅是我创建/插入对象的顺序吗? (见下面的种子代码)

如果我不在许可证中使用[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)],我会收到一个错误,提示无法隐式插入不是由数据库生成的 id。

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

    如果我将 License-Customer 更改为多对多关系(我不希望这样做),我会收到以下错误:Cannot insert duplicate key row in object 'dbo.Users' with unique index 'IX_Username'.

这些都是我的关系:

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

这是我在protected override void Seed(DAL.Models.Context context) 中一直使用的代码,在代码中创建对象,然后使用 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);

我还尝试将//Default Customers - create 之后的第一部分更改为

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

非常感谢您对此的帮助,以便我可以为我的数据库添加适当的数据。

非常感谢您的帮助,很抱歉发了这么长的帖子。

--- 更新 ---

在以下 Chris Pratt 的出色回答之后,我现在收到以下错误: Conflicting changes detected. This may happen when trying to insert multiple entities with the same key.

这是我用于播种和所有相关模型的新代码:https://gist.github.com/jacquibo/c19deb492ec3fff0b5a7

有人可以帮忙吗?

【问题讨论】:

【参考方案1】:

播种可能有点复杂,但您只需要记住以下几点:

    使用AddOrUpdate 添加的任何内容都将被添加或更新。但其他任何内容都会添加,而不是更新。在您的代码中,您唯一使用的 AddOrUpdateCustomer

    EF 将在添加AddOrUpdate 项时添加相关项,但在更新该项时忽略这些关系。

基于此,如果您要像这样添加对象层次结构,则必须格外小心。首先,您需要在层次结构的级别之间调用SaveChanges,其次,您需要使用ids,而不是关系。例如:

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

现在您的所有客户都得到了照顾,他们每个人的 id 都会以一种或另一种方式获得价值。然后,开始深入了解您的层次结构:

// 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]);

如果您正在处理层次结构中同一级别的对象,则无需调用SaveChanges,直到您全部定义它们。但是,如果您有一个引用 License 之类的实体,那么您需要在添加之前再次调用 SaveChanges

另外,请注意我正在切换到设置 id 而不是关系。这样做将允许您稍后通过更改 id 来更新关系。例如:

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

而以下不会起作用:

// 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] ,

当然,您实际上在 CustomerId 上没有 CustomerId 属性,但您应该这样做。虽然 EF 将自动生成一个列来保存没有它的关系,但如果没有显式属性,您永远无法真正获得外键值,而且除了此之外,能够使用实际的外键是非常有益的。只需为所有参考属性遵循此约定:

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

ForeignKey 属性并不总是必需的,这取决于您的外键和引用属性名称是否符合 EF 对此类事物的约定,但我发现明确说明我的意图更容易且不易出错。

【讨论】:

嗨@Chris,感谢您的出色回答。我已经更新了我的代码,但现在出现了一个新错误。我已将新错误和代码作为对原始问题的更新。我不得不稍微调整你的答案,使用外键数据注释,因为当它们像你上面所说的那样时我遇到了另一个错误(我认为它是'无法确定类型'权限'和'ProductLicense'。必须明确配置此关联的主体端......')。使用流利的方式来指定这些关系会更好吗?非常感谢:)【参考方案2】:

外键必须引用另一个表的候选键(通常是 PK,可以由 DB 生成)。 FK 依赖于另一个表中的值,显然不能由拥有它的表生成。

您要执行的操作称为“共享主键”。看起来您已经很接近了 - 您只是向后生成了 ID,并且在所需实体上缺少 ForeignKeyAttribute

如果License 是依赖实体,您希望它看起来像这样:

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; 

如果您愿意,这可用于Table Splitting,您可以在两个或多个实体之间拆分单个数据库表的字段(您可以通过为每个实体指定相同的表名来执行上述操作)通过TableAttribute 或 FluentAPI 调用)。例如,如果您希望能够选择性地加载一个大字段,这将非常有用。

【讨论】:

【参考方案3】:

在生成要应用的新迁移脚本时,代码优先迁移不会考虑为您当前的数据库记录提供完整性更新。您可能必须通过使用 Sql("...") 函数在迁移文件的 Up() 方法中添加额外的 SQL 命令,该函数将在尝试应用迁移之前更新当前数据库行。

假设您想将新的引用约束应用到数据库中。即使您的参考文件不包含相应的行,您在生成迁移文件时也不会收到错误消息。但是您在包管理器控制台上执行脚本时收到错误 1(您的第一条消息)。

为此,您应该在 Up() 方法的顶部添加 Sql("INSERT INTO .... ") 和/或 Sql("UPDATE .....") 命令,以修改您当前的具有正确值的数据库行。

同样,您可能需要从数据库中删除一些记录,具体取决于您的结构更改。

【讨论】:

以上是关于Entity Framework 6 Code first:如何使用“循环”关系和存储生成的列来播种数据?的主要内容,如果未能解决你的问题,请参考以下文章

Entity Framework 6 Code First +MVC5+MySql/Oracle使用过程中的两个问题

Entity Framework 6 + Code First + Oracle 12c 的示例

Code First Entity Framework 6化被动为主动之explicit loading模式实战分析( 附源码)

如何使用 Entity Framework 6 Code First 设置默认值约束?

Entity Framework 6 Code First 方法 - 唯一约束不起作用

Entity Framework 6 Code first:如何使用“循环”关系和存储生成的列来播种数据?