实体框架 6 代码优先:如何为具有“循环”关系的数据和存储生成的列设定种子
本文关键字:数据 关系 存储 种子 循环 代码 框架 实体 | 更新日期: 2023-09-27 18:27:40
我是 EF Code First 的新手,我正在尝试使用代码优先迁移为我的数据库提供一些数据种子。到目前为止,我已经设法解决了几个错误,但现在我被困住了,无法找到答案。从我的代码更新数据库时有两个问题。
我有几个对象,它们具有各种多对多和一对一的关系,有些最终会创建一个圆。当我尝试为数据库播种时,我不确定这是否是第二个问题的原因。
- 我遇到的第一个错误是:
A dependent property in a ReferentialConstraint is mapped to a store-generated column. Column: 'LicenseId'.
有没有办法将数据库生成的 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
}
- 如果我将许可证客户更改为多对多关系(我不想要(,则会出现以下错误:
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
谁能帮忙?
播种可能有点复杂,但你只需要记住几点:
-
添加
AddOrUpdate
的任何内容都将被添加或更新。但是其他任何内容都将添加,而不是更新。在您的代码中,您唯一使用AddOrUpdate
的是Customer
。 -
EF 将在添加
AddOrUpdate
项时添加相关项,但在更新该项时会忽略这些关系。
基于此,如果您要添加这样的对象层次结构,则必须格外小心。首先,您需要在层次结构的级别之间调用SaveChanges
,其次,您需要使用 id,而不是关系。例如:
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] },
当然,你实际上没有License
CustomerId
属性,但你应该这样做。虽然 EF 将自动生成一个列来保存没有它的关系,但如果没有显式属性,您永远无法实际获取外键值,并且出于更多原因,能够使用实际外键是非常有益的。只需对所有引用属性遵循以下约定:
[ForeignKey("Customer")]
public int CustomerId { get; set;}
public virtual Customer Customer { get; set; }
ForeignKey
属性并不总是必需的,具体取决于外键和引用属性名称是否与 EF 的约定一致,但我发现明确我的意图更容易且更不容易出错。
外键必须引用另一个表的候选键(通常是 PK,可以由数据库生成(。 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调用为每个实体指定相同的表名来执行此操作(。 例如,如果您希望能够有选择地加载大型字段,这将非常有用。
在生成要应用的新迁移脚本时,代码优先迁移不考虑为当前数据库记录提供完整性更新。您可能必须在迁移文件的 Up(( 方法中添加额外的 SQL 命令,方法是使用 Sql("..."( 函数,这些函数将在尝试应用迁移之前更新当前数据库行。
假定您要将新的引用约束应用于数据库。生成迁移文件时,即使参考文件不包含相应的行,也不会收到错误消息。但是在包管理器控制台上执行脚本时收到错误 1(您的第一条消息(。
为此,您应该添加 Sql("插入到...."(和/或 Up(( 方法顶部的 Sql("UPDATE ....."( 命令,这些命令将使用正确的值修改当前数据库行。
同样,您可能必须从数据库中删除某些记录,具体取决于结构更改。