实体框架 6 代码优先:如何为具有“循环”关系的数据和存储生成的列设定种子

本文关键字:数据 关系 存储 种子 循环 代码 框架 实体 | 更新日期: 2023-09-27 18:27:40

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

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

  1. 我遇到的第一个错误是: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
}
  1. 如果我将许可证客户更改为多对多关系(我不想要(,则会出现以下错误: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

谁能帮忙?

实体框架 6 代码优先:如何为具有“循环”关系的数据和存储生成的列设定种子

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

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

  2. 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 ....."( 命令,这些命令将使用正确的值修改当前数据库行。

同样,您可能必须从数据库中删除某些记录,具体取决于结构更改。