实体框架自动外键填充

5
有没有办法强制Entity Framework在将实体添加到上下文时立即填充外键,而不是延迟到上下文中发生其他事情?默认行为在使用数据绑定显示引用实体时并不是很有帮助。
仅从上下文引用任何DbSet就足以强制EF填充已添加的Children的Parent和Parent_Name。但是,除非调用SaveChanges,否则没有任何东西可以强制EF填充Reference或Reference_Name。
我真的想使用[Required]属性标记Reference_Name,以便它在数据库中为Not Null,但如果我这样做,当我尝试调用SaveChanges时,除非我明确设置了Reference_Name,否则我会收到验证错误,即使SaveChanges本身将正确填充Reference_Name如果Reference已设置。
我真的想能够设置Reference或Reference_Name并立即使用另一个。同样,我想在添加Child对象后立即使用Parent或Parent_Name,而无需使用一些来自上下文的其他元素的恶作剧。
有人能帮助我理解为什么EF会延迟这些内容,以及如何强制它填充外键属性或外键列,最好是立即,但至少不必调用SaveChanges吗?我真的不想在EF将正确填充它们时显式地填充所有属性。
public class OracleContext : DbContext
{
    public virtual DbSet<Parent> Parents { get; set; }
    public virtual DbSet<Child> Children { get; set; }
    public virtual DbSet<Reference> References { get; set; }
    public virtual DbSet<SomethingElse> SomethingElses { get; set; }
}

public class Parent
{
    [Key, MaxLength(30)]
    public string Name { get; set; }

    [InverseProperty("Parent")]
    public virtual List<Child> Children { get; set; } = new List<Child>();
}

public class Child
{
    [Key, Column(Order = 1), MaxLength(30)]
    public string Parent_Name { get; set; }

    [Key, Column(Order = 2), MaxLength(30)]
    public string Name { get; set; }

    public string Reference_Name { get; set; }

    [ForeignKey("Parent_Name")]
    public virtual Parent Parent { get; set; }

    [ForeignKey("Reference_Name")]
    public virtual Reference Reference { get; set; }

    public Child Clone()
    {
        return new Child
        {
            Parent_Name = this.Parent_Name,
            Name = this.Name,
            Reference_Name = this.Reference_Name,
            Parent = this.Parent,
            Reference = this.Reference
        };
    }
}

public class Reference
{
    [Key, MaxLength(30)]
    public string Name { get; set; }
}

public class SomethingElse
{
    [Key, MaxLength(30)]
    public string Name { get; set; }
}

private void button1_Click(object sender, EventArgs e)
{
    OracleContext context = new OracleContext();

    Reference reference = context.References.Add(new Reference { Name = "Reference" });

    Parent alpha = context.Parents.Add(new Parent { Name = "Alpha" });

    Child alphaOne = new Child { Name = "AlphaOne" };
    Child alphatwo = new Child { Name = "AlphaTwo", Reference_Name = "Reference" };
    alpha.Children.AddRange(new List<Child> { alphaOne, alphatwo });
    alphaOne.Reference = reference;

    var list = (
            from child in alpha.Children
            select new
            {
                Time = "Before referencing SomethingElses.Local",
                Child = child.Clone()
            }
        ).ToList();

    var x = context.SomethingElses.Local;

    list.AddRange(
            from child in alpha.Children
            select new
            {
                Time = "After referencing SomethingElses.Local",
                Child = child.Clone()
            }
        );

    list.AddRange(
            from parent in context.Parents.Local
            from child in parent.Children
            select new
            {
                Time = "Before SaveChanges",
                Child = child.Clone()
            }
        );

    context.SaveChanges();

    list.AddRange(
            from parent in context.Parents.Local
            from child in parent.Children
            select new
            {
                Time = "After SaveChanges",
                Child = child.Clone()
            }
        );

    foreach (var item in list)
    {
        Console.WriteLine("{0}:\r\n\tName = '{1}'\r\n\tParent = '{2}' ({3})\r\n\tReference = '{4}' ({5})",
            item.Time, item.Child.Name, item.Child.Parent_Name, item.Child.Parent, item.Child.Reference_Name, item.Child.Reference);
    }
}

Before referencing SomethingElses.Local:
    Name = 'AlphaOne'
    Parent = '' ()
    Reference = '' (WindowsFormsApplication2.Reference)
Before referencing SomethingElses.Local:
    Name = 'AlphaTwo'
    Parent = '' ()
    Reference = 'Reference' ()
After referencing SomethingElses.Local:
    Name = 'AlphaOne'
    Parent = 'Alpha' (WindowsFormsApplication2.Parent)
    Reference = '' (WindowsFormsApplication2.Reference)
After referencing SomethingElses.Local:
    Name = 'AlphaTwo'
    Parent = 'Alpha' (WindowsFormsApplication2.Parent)
    Reference = 'Reference' ()
Before SaveChanges:
    Name = 'AlphaOne'
    Parent = 'Alpha' (WindowsFormsApplication2.Parent)
    Reference = '' (WindowsFormsApplication2.Reference)
Before SaveChanges:
    Name = 'AlphaTwo'
    Parent = 'Alpha' (WindowsFormsApplication2.Parent)
    Reference = 'Reference' ()
After SaveChanges:
    Name = 'AlphaOne'
    Parent = 'Alpha' (WindowsFormsApplication2.Parent)
    Reference = 'Reference' (WindowsFormsApplication2.Reference)
After SaveChanges:
    Name = 'AlphaTwo'
    Parent = 'Alpha' (WindowsFormsApplication2.Parent)
    Reference = 'Reference' (WindowsFormsApplication2.Reference)

我不确定你希望ID在何时被填充,因为它们需要由数据库生成,并且只有在添加到后端之后才能生成,如果这是你的问题。 - Ron Beyer
正如Ron所说,数据库会生成身份标识,因此记录被插入后,ID被提取,您的对象将更新该ID。如果您想在实际提交数据到数据库之前存在这些身份标识的方法,则需要使用事务,并在准备好ID持久保存到数据库时进行提交。 - MutantNinjaCodeMonkey
Ron Beyer和MutantNinjaCodeMonkey,我根本没有谈论DatabaseGenerated ids。如果您查看我提供的模型,所有主键都是字符串。 - Matt Knowles
1个回答

4
有没有一种方法可以强制Entity Framework在将实体添加到上下文中时立即填充外键,而不是延迟到上下文中发生其他事情时再进行?
选项1: 如果您只想修复实体之间的关系而不保存它们到数据库中,因此调用DbContext.SaveChanges(),则只需调用DbContext.ChangeTracker.DetectChanges()。
选项2: EF可以自动修复实体之间的关系,而无需调用DbContext.SaveChanges()或DbContext.ChangeTracker.DetectChanges()。这些实体称为“代理类”。代理类是一个动态生成的派生类型,充当实体的代理。此代理覆盖实体的某些虚拟属性,以插入钩子,以便在访问该属性时自动执行操作。默认情况下,代理创建已启用您的DbContext,除非您通过调用DbContext.Configuration.ProxyEnbled = false; 禁用了它。您不需要添加那行代码,因为您需要启用代理创建。
无论如何,在利用此功能之前,您需要修改类的一些内容: - 所有属性(标量、导航、集合)都必须标记为virtual。 - 所有导航集合必须声明为ICollection。 - 所有实体实例化都必须使用DbContext.DbSet.Create()方法完成。 - 所有集合实例化不能在构造函数中初始化。代理类将负责实例化并在不遵循此点时引发异常。
按照这些步骤,您的实体类必须看起来像这样:
public class Parent
{
    [Key, MaxLength(30)]
    public virtual string Name { get; set; }

    [InverseProperty("Parent")]
    public virtual ICollection<Child> Children { get; set; }
}

public class Child
{
    [Key, Column(Order = 1), MaxLength(30)]
    public virtual string Parent_Name { get; set; }

    [Key, Column(Order = 2), MaxLength(30)]
    public virtual string Name { get; set; }

    public virtual string Reference_Name { get; set; }

    [ForeignKey("Parent_Name")]
    public virtual Parent Parent { get; set; }

    [ForeignKey("Reference_Name")]
    public virtual Reference Reference { get; set; }

    public Child Clone()
    {
        return new Child
        {
            Parent_Name = this.Parent_Name,
            Name = this.Name,
            Reference_Name = this.Reference_Name,
            Parent = this.Parent,
            Reference = this.Reference
        };
    }
}

public class Reference
{
    [Key, MaxLength(30)]
    public virtual string Name { get; set; }
}

public class SomethingElse
{
    [Key, MaxLength(30)]
    public virtual string Name { get; set; }
}

您的点击事件处理程序实现应该如下所示:

Reference reference = context.References.Create();
reference.Name = "Reference";
context.References.Add(reference);

Parent alpha = context.Parents.Create();
alpha.Name = "Alpha"; 
context.Parents.Add(alpha);

Child alphaOne = context.Children.Create();
alphaOne.Name = "AlphaOne";

Child alphatwo = context.Children.Create();
alphatwo.Name = "AlphaTwo";
alphatwo.Reference = reference; // Notice we use the navigational property.

alpha.Children.Add(alphaOne);
alpha.Children.Add(alphatwo);
alphaOne.Reference = reference;

var list = (
        from child in alpha.Children
        select new
        {
            Time = "Before referencing SomethingElses.Local",
            Child = child.Clone()
        }
    ).ToList();

var x = context.SomethingElses.Local;

list.AddRange(
        from child in alpha.Children
        select new
        {
            Time = "After referencing SomethingElses.Local",
            Child = child.Clone()
        }
    );

list.AddRange(
        from parent in context.Parents.Local
        from child in parent.Children
        select new
        {
            Time = "Before SaveChanges",
            Child = child.Clone()
        }
    );

context.SaveChanges();

list.AddRange(
        from parent in context.Parents.Local
        from child in parent.Children
        select new
        {
            Time = "After SaveChanges",
            Child = child.Clone()
        }
    );

foreach (var item in list)
{
    Console.WriteLine("{0}:\r\n\tName = '{1}'\r\n\tParent = '{2}' ({3})\r\n\tReference = '{4}' ({5})",
        item.Time, item.Child.Name, item.Child.Parent_Name, item.Child.Parent, item.Child.Reference_Name, item.Child.Reference);
}

这个实现有两个明显的变化:

  • 如上所述,您必须使用DbContext.DbSet.Create来获取T的生成代理实例,而不是使用默认构造函数。
  • 关于延迟加载需要知道的一件事是它会检查是否已加载导航属性。如果没有加载,则会检查数据库以加载实体。在您的情况下,所有实体都处于Added状态,因此执行Reference_Name =“Reference”将无法帮助上下文来懒惰地加载导航属性Refererence。这就是为什么我执行alphatwo.Reference = reference;而不是alphatwo.Reference_Name = “Reference”;的原因,因为reference处于Added状态,懒惰加载将在数据库中找不到任何东西。

非常感谢您如此详尽的回答,直接解决了我的问题。看起来非常有前途。我会进行一些测试,并回来将其标记为答案,如果我能让它工作的话。 - Matt Knowles
好的,还有一件事我不是很明白:如果我只将Reference_Name设置为“Reference”,然后调用SaveChanges,Reference属性将自动正确设置。然而,除了SaveChanges(甚至不是DetectChanges)之外,似乎没有什么能够强制它发生。在我的看来,代理类应该能够检测到我已经填充了所有ForeignKey列,并自动填充适当的ForeignKey对象如果它已经在上下文中被加载 - Matt Knowles
除此之外,这个程序完美运作。我将Children改为ICollection<Child>,现在在添加后立刻看到Child的Parent和Parent_Name属性。再次感谢你的出色回答! - Matt Knowles

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接