【问题标题】:EFCore - Why do i have to null child objects to stop them inserting? One-to-manyEFCore - 为什么我必须使子对象为空才能阻止它们插入?一对多
【发布时间】:2020-01-07 06:04:43
【问题描述】:

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

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

我有两个类,Command 和 CommandCategory。 Command 有一个 CommandCategory,而 CommandCategory 可以有许多命令。例如,“设置超时”命令将位于“配置”类别下。同样,“设置 URL”命令也将位于“配置”类别下。

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()。 Add 创建一个新命令并将其添加到数据库中。它还会创建一个名为“Configuration”的 CommandCategory 并正确插入两者。

接下来我调用 AddWithExisting()。这将创建一个新命令,但使用现有的 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.stackoverflow.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();
    }

【问题讨论】:

    标签: entity-framework entity-framework-core relationship one-to-many ef-core-2.2


    【解决方案1】:

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

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

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

    例如

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

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

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

    【讨论】:

    • 这是有道理的,但确实看起来很尴尬。在具有多个一对多关系和多对多关系的较大对象上,管理起来确实非常复杂。绝对不是做不到,只是不是我习惯的少量代码。
    • 如果只是设置 ID,那还不错,但如果 FK 错误不存在,您将面临保存时出现 FK 错误的风险。
    【解决方案2】:

    EF 不执行 InsertOrUpdate 检查。实体由 DbContext 跟踪为已添加或已更新。如果您与跟踪的实体交互或将实体“添加”到 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.stackoverflow.com",
            CommandCategory = command.CommandCategory
        };
        dbContext.Commands.Add(newCommand);
        return newCommand;
    }
    

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

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

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

    【讨论】:

      猜你喜欢
      • 2017-03-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-09-05
      相关资源
      最近更新 更多