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
添加的任何内容都将被添加或更新。但其他任何内容都会添加,而不是更新。在您的代码中,您唯一使用的 AddOrUpdate
是 Customer
。
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 设置默认值约束?