EFCore - 为什么我必须使子对象为空才能阻止它们插入?一对多

EFCore - Why do i have to null child objects to stop them inserting? One-to-many

少量上下文,我已经使用代码映射 NHibernate 几年了,最近几个月我开始使用 Entity Framework Core.

我想了解为什么我必须使子对象为空以阻止它们插入新记录。我不确定这是否是我的理解问题,或者这是否是 Entity Framework 的工作方式。

我有两个 类,Command 和 CommandCategory。 Command 有一个 CommandCategory,而 CommandCategory 可以有很多命令。例如,命令 "set timeout" 将归入 "Configuration" 类别。同样,"set URL" 命令也属于 "Configuration" 类别。

class Command
{
    public Guid Id { get; set; }
    public string Name { get; set; }

    public string CommandString { get; set; }
    public Guid CommandCategoryId { get; set; }

    public CommandCategory CommandCategory { get; set; }
}

class CommandCategory
{
     public CommandCategory(string id, string name)
    {
        Id = Guid.Parse(id);
        Name = name;
        Commands = new List<Command>();
    }

    public Guid Id { get; set; }
    public string Name { get; set; }

    public ICollection<Command> Commands { get; set; }
}

我的 DbContext 设置如下:

class EfContext : DbContext
{
    private const string DefaultConnection = "XXXXX";

    public virtual DbSet<Command> Command { get; set; }
    public virtual DbSet<CommandCategory> CommandCategory { get; set; }


    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            optionsBuilder.UseSqlServer(DefaultConnection);
            optionsBuilder.EnableSensitiveDataLogging();
        }
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Command>()
            .HasOne(x => x.CommandCategory)
            .WithMany(x => x.Commands);
    }
}

那么这里是实际运行它的代码。首先我调用 Add()。添加创建一个新命令并将其添加到数据库中。它还会创建一个名为 "Configuration" 的 CommandCategory 并正确插入两者。

接下来我调用 AddWithExisting()。这将创建一个新命令,但使用现有的 CommandCategory。当它尝试添加到数据库时,它首先插入 Command,然后尝试插入 CommandCategory。因为 CommandCategory.Id 已经存在,并且它被设置为主键,所以它会失败,因为它是一个重复键。为了解决这个问题,我必须确保 Command 对象上的 CommandCategory 属性 设置为 null。这只会将 Command 插入数据库,而不是 CommandCategory 对象。

我知道通常您不会创建新的 CommandCategory 对象,但在本例中,我模拟了通过 ApiController 从客户端发出的对象。我的应用程序通过 WebApi 来回发送数据,因此基本上是在发出请求时创建新对象。

取消 属性 似乎是一件奇怪的事情,我认为对象关系映射的要点是不必像这样处理单个属性。

这是它应该如何运作还是我做错了什么?

class Program
{
    static void Main(string[] args)
    {
        var dbContext = new EfContext();

        Add(dbContext);
        AddWithExisting(dbContext);
        Console.WriteLine("Hello World!");
    }

    private static void Add(EfContext dbContext)
    {
        var newCommand = new Command();
        newCommand.Id = Guid.NewGuid();
        newCommand.Name = "set timeout";
        newCommand.CommandString = "timeout:500;";

        var newCommandCategory = new CommandCategory("8C0D0E31-950E-4062-B783-6817404417D4", "Configuration");
        newCommandCategory.Commands.Add(newCommand);

        newCommand.CommandCategory = newCommandCategory;


        dbContext.Command.Add(newCommand);

        dbContext.SaveChanges();
    }
    private static void AddWithExisting(EfContext dbContext)
    {
        var newCommand = new Command();
        newCommand.Id = Guid.NewGuid();
        newCommand.Name = "set URL";
        newCommand.CommandString = "url:www.whosebug.com";

        // this uses the same Id and Name as the existing command, this is to simulate a rest call coming up with all the data.
        var newCommandCategory = new CommandCategory("8C0D0E31-950E-4062-B783-6817404417D4", "Configuration");
        newCommandCategory.Commands.Add(newCommand);


        // If i don't null the below line, it will insert to the database a second time
        newCommand.CommandCategory = newCommandCategory;
        newCommand.CommandCategoryId = newCommandCategory.Id;

        dbContext.Command.Add(newCommand);

        dbContext.SaveChanges();
    }

这是设计使然,您可以在这里做两件事:

  1. 您可以从数据库中查找现有的命令类别并将其设置为 属性(因为此对象是 'attached' 到数据库上下文,它不会创建一个新的)。

  2. 在命令上设置命令类别的ID即可

例如

newCommand.CommandCategory = dbContext.CommandCategories.Find("8C0D0E31-950E-4062-B783-6817404417D4");

newCommand.CommandCategoryId = new Guid("8C0D0E31-950E-4062-B783-6817404417D4");

此刻,它看到一个新的命令类别(未附加),因此正在尝试创建它。

EF 不执行 InsertOrUpdate 检查。实体由 DbContext 跟踪为已添加或已更新。如果您与跟踪的实体或 "Add" 实体交互到 DbContext,所有未跟踪的相关实体将被识别为已添加,从而导致插入。

我能给出的最简单的建议是,当涉及到实体时,让 EF 免于怀疑,不要尝试过早优化。它可以减轻头痛。

using (var dbContext = new EfContext())
{
    var newCommand = Add(dbContext);
    AddWithExisting(newCommand, dbContext);

    dbContext.SaveChanges();
    Console.WriteLine("Hello World!");
}

private static command Add(EfContext dbContext)
{
    var newCommand = new Command
    {
        Id = Guid.NewGuid(), // Should either let DB set this by default, or use a Sequential ID implementation.
        Name = "set timeout",
        CommandString = "timeout:500;"
    };
    Guid commandCategoryId = new Guid("8C0D0E31-950E-4062-B783-6817404417D4");
    var commandCategory = dbContext.CommandCategory.Where(x => x.CommandCategoryId == commandCategoryId);
    if(commandCategory == null)
        commandCategory = new CommandCategory 
        { 
            Id = commandCategoryId,
            Name = "Configuration"
        };

    newCommand.CommandCategory = commandCategory;
    dbContext.Command.Add(command);
    return command;
}
private static Command AddWithExisting(Command command, EfContext dbContext)
{
    var newCommand = new Command 
    {
        Id = Guid.NewGuid(),
        Name = "set URL",
        CommandString = "url:www.whosebug.com",
        CommandCategory = command.CommandCategory
    };
    dbContext.Commands.Add(newCommand);
    return newCommand;
}

那么这里发生了什么变化?

首先,DbContext 引用是一次性的,因此它应该始终用 using 块包裹。接下来,我们创建初始命令,作为避免假设的安全措施,我们通过 ID 搜索现有 CommandCategory 的上下文并将其关联,否则我们创建命令类别并将其关联到命令。一对多关系不需要是双向的,即使您确实想要双向关系,如果映射设置正确,您通常也不需要将两个引用设置为彼此。如果加载 CommandCategory 并导航到使用该类别的所有命令是有意义的,那么保留它,但即使查询特定类别的所有命令,从命令级别查询也很容易。双向引用会导致烦人的问题,所以我不建议使用它们,除非确实有必要。 我们 return 从第一次调用中返回新的命令对象,并将其传递给第二次调用。我们实际上只需要在第一次调用中传递对命令类别 loaded/created 的引用,但为了防止第一个命令对 check/copy 信息有意义,我使用了这个示例。我们创建新的附加命令实例并将其命令类别引用设置为与第一个实例相同的实例。然后我 return 新命令也是如此。我们不使用对第二个命令的引用。这与您尝试过的重要区别在于,此处的 CommandCategory 指向相同的引用,而不是具有相同 ID 的两个引用。 EF 将跟踪此实例,因为它是 associated/added,并连接适当的 SQL。

最后请注意,SaveChanges 调用已移到两个调用之外。上下文通常应该在其生命周期内只保存一次更改。一切都会一起承诺。当开发人员希望在数据库自动生成键时手动连接关联时,拥有多个 SaveChanges 通常是一种味道。 (标识或默认值)提供的关系与导航属性及其 FK 正确映射,EF 完全有能力自动管理这些。这意味着,如果您将数据库设置为默认命令 ID,例如 newsequentialid() 并告诉 EF 将 PK 视为标识列,EF 将自动处理这一切。这也适用于将这些新 PK 作为 FK 关联到相关实体。无需保存父记录,因此可以在子实体中设置父 ID,将其映射、关联并让 EF 处理。