我有一个实体
public class ExpenseCategory
{
public int Id { get; set; }
public string Label { get; set; } = string.Empty;
public int? ParentId { get; set; }
public ExpenseCategory? Parent { get; set; }
public int? PreviousSiblingId { get; set; }
public ExpenseCategory? PreviousSibling { get; set; }
public int? NextSiblingId { get; set; }
public ExpenseCategory? NextSibling { get; set; }
public List<ExpenseCategory> Children { get; set; } = [];
public List<Expense> Expenses { get; set; } = [];
}
在我的 DbContext 中
modelBuilder.Entity<ExpenseCategory>().ToTable("ExpenseCategory")
.HasOne(e => e.Parent)
.WithMany(e => e.Children)
.HasForeignKey(e => e.ParentId)
.OnDelete(DeleteBehavior.Restrict); // Prevents cascading deletes
// Configuring the ExpenseCategory entity
modelBuilder.Entity<ExpenseCategory>()
.HasKey(ec => ec.Id);
modelBuilder.Entity<ExpenseCategory>()
.Property(ec => ec.Label)
.IsRequired(); // Ensure Label is required
modelBuilder.Entity<ExpenseCategory>()
.HasOne(e => e.PreviousSibling)
.WithOne()
.HasForeignKey<ExpenseCategory>(e => e.PreviousSiblingId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<ExpenseCategory>()
.HasOne(e => e.NextSibling)
.WithOne()
.HasForeignKey<ExpenseCategory>(e => e.NextSiblingId)
.OnDelete(DeleteBehavior.Restrict);
在表单代码中我有一个
private void AddRootButton_Click(object sender, EventArgs e)
{
var label = GetNewCategoryLabel();
if (label.IsNullOrEmpty()) return;
ExpenseCategory? prevCategory = null;
TreeNode? prevNode = null;
var rootNodes = TV.Nodes;
if (rootNodes.Count > 0)
{
prevNode = rootNodes[rootNodes.Count - 1];
if (prevNode != null)
{
prevCategory = (ExpenseCategory)prevNode.Tag;
}
}
// Create new category
var newCategory = new ExpenseCategory()
{
Label = label,
Parent = null,
PreviousSibling = prevCategory,
NextSibling = null, // Initially null
Children = [],
Expenses = []
};
// Add new category to the context
var entry = _context.ExpenseCategories.Add(newCategory);
// Setup sibling references conditionally
if (prevCategory != null)
{
// Set the NextSibling of the previous category to the new one
prevCategory.NextSibling = entry.Entity;
entry.Entity.PreviousSibling = prevCategory; // Establish mutual reference
}
else
{
// If there's no previous category, we can explicitly set null to avoid confusion
entry.Entity.PreviousSibling = null;
}
// Add the new node to the TreeView
TreeNode newNode = new TreeNode(newCategory.Label)
{
Tag = newCategory
};
var index = TV.Nodes.Add(newNode);
TV.SelectedNode = TV.Nodes[index];
ActiveControl = TV;
// Note: _context.SaveChanges() will be called later
}
因为我希望 _context 在表单关闭时而不是在输入新条目时保存更改 我打电话给这个
private async void ExpenseCategoriesDialog_FormClosing(object sender, FormClosingEventArgs e)
{
await SaveChangesWithTrackingChecksAsync();
}
private async Task SaveChangesWithTrackingChecksAsync()
{
try
{
foreach (var entry in _context.ChangeTracker.Entries<ExpenseCategory>())
{
var category = entry.Entity;
Debug.WriteLine($"Entity: {category.Label}, State: {entry.State}");
// Check PreviousSibling and NextSibling presence and tracking status
if (category.PreviousSibling != null)
{
var previousSiblingEntry = _context.ChangeTracker.Entries().FirstOrDefault(e => e.Entity == category.PreviousSibling);
if (previousSiblingEntry == null)
{
Debug.WriteLine($" PreviousSibling (Label: {category.PreviousSibling.Label}, Id: {category.PreviousSibling.Id}) is NOT being tracked. Attaching it now.");
_context.Attach(category.PreviousSibling);
}
else
{
Debug.WriteLine($" PreviousSibling (Label: {category.PreviousSibling.Label}, Id: {category.PreviousSibling.Id}) is being tracked.");
}
}
else
{
Debug.WriteLine(" PreviousSibling is null.");
}
if (category.NextSibling != null)
{
var nextSiblingEntry = _context.ChangeTracker.Entries().FirstOrDefault(e => e.Entity == category.NextSibling);
if (nextSiblingEntry == null)
{
Debug.WriteLine($" NextSibling (Label: {category.NextSibling.Label}, Id: {category.NextSibling.Id}) is NOT being tracked. Attaching it now.");
_context.Attach(category.NextSibling);
}
else
{
Debug.WriteLine($" NextSibling (Label: {category.NextSibling.Label}, Id: {category.NextSibling.Id}) is being tracked.");
}
}
else
{
Debug.WriteLine(" NextSibling is null.");
}
// Additional check to verify sibling chain integrity
if (category.NextSibling?.PreviousSibling != category)
{
Debug.WriteLine($"Warning: {category.Label}'s NextSibling ({category.NextSibling?.Label}) does not reference {category.Label} as PreviousSibling.");
}
if (category.PreviousSibling?.NextSibling != category)
{
Debug.WriteLine($"Warning: {category.Label}'s PreviousSibling ({category.PreviousSibling?.Label}) does not reference {category.Label} as NextSibling.");
}
}
await _context.SaveChangesAsync();
}
catch (NullReferenceException ex)
{
Debug.WriteLine("NullReferenceException encountered: " + ex.Message);
throw;
}
}
当我在 SaveChangesAsync 被点击时添加 1 和 2 作为输入条目时,我得到了
System.NullReferenceException: 'Object reference not set to an instance of an object.'
在输出窗口中我得到
Entity: 1, State: Added
PreviousSibling is null.
NextSibling (Label: 2, Id: 0) is being tracked.
Warning: 1's PreviousSibling () does not reference 1 as NextSibling.
Entity: 2, State: Added
PreviousSibling (Label: 1, Id: 0) is being tracked.
NextSibling is null.
Warning: 2's NextSibling () does not reference 2 as PreviousSibling.
Exception thrown: 'System.NullReferenceException' in System.Private.CoreLib.dll
Object reference not set to an instance of an object.
正如您所见,尽管引用已正确分配,但我不断收到此异常,而且我无法弄清楚这是否是 EFCore 问题,还是我遗漏了某些内容! 还有
Warning: 1's PreviousSibling () does not reference 1 as NextSibling.
和
Warning: 2's NextSibling () does not reference 2 as PreviousSibling.
无效,因为 1 的 PreviousSibling 为 null,因此 1 的 PreviousSibling 没有 NextSibling 并且 2 的 NextSibling 也为 null,因此 2 的 NextSibling 也没有 PreviousSibling
如果我在每次输入条目后调用 SaveChanges,条目就会正确保存! 你能帮忙吗,因为我花了几个小时试图弄清楚为什么 EFCore 无法保存如此简单的关系数据! 提前谢谢
我已经在上面向您展示了我迄今为止所尝试的内容 仅当添加新条目后立即调用 savechanges 时才能正常工作 否则,当表单关闭时,我总是会收到异常“对象引用未设置为对象的实例”,并且我无法弄清楚哪个对象引用未设置为对象的实例
尝试和管理可能隐藏问题的跟踪和未跟踪实体需要进行大量工作。您可能缺少的是处理对特定实体的另一个引用恰好被跟踪的情况。 对跟踪缓存的检查可能找不到您正在查找的特定实例,但如果在尝试附加之前找到具有相同 ID 的不同实例,它应该进行处理。这不会导致 NullRef,而是导致不同的异常。
我还发现了一些其他问题。首先,构建您的收藏以保护设置者。当使用实体时,您永远不希望出现集合被重新初始化的情况。 EF 通过跟踪引用来工作,因此任何代码都不应该重新初始化它们,否则您将完全破坏更改跟踪。
public List<ExpenseCategory> Children { get; } = [];
public List<Expense> Expenses { get; } = [];
代码中出现问题的地方就是问题区域。
接下来,我会诚实地完全删除 PreviousSibling/NextSibling FK/关系。这是父母与孩子之间关系结构的非规范化。虽然拥有方便的双链表关系似乎很有用,但在关系模型中引入类似的东西的问题是它不切实际甚至不可能执行。基本规则是“兄弟姐妹”应该始终是同一父母的另一个孩子,但没有办法强制执行。上一个/下一个同级 ID 可以设置为 any ExpenseId。
如果您希望在结构中使用双链表的便利,我建议使用未映射的链表。 ExpenseCategory 需要一个 SortOrder,以便有孩子的父母可以按照可预测的排名顺序排列“兄弟姐妹”,然后 ExpenseCategory 可以从父母内部找到他们的兄弟姐妹:
[NotMapped]
public ExpenseCategory? PreviousSibling => Parent.Children.Where(c => c.SortOrder < SortOrder).OrderByDescending(c => c.SortOrder).FirstOrDefault();
[NotMapped]
public ExpenseCategory? NextSibling => Parent.Children.Where(c => c.SortOrder > SortOrder).OrderBy(c => c.SortOrder).FirstOrDefault();
这意味着您不能在查询表达式中使用上一个/下一个同级,并且在查看/编辑给定级别的费用时,您需要确保它是父级和父级。子级是急切加载的。
移动兄弟姐妹将涉及交换 SortOrder 值。插入将涉及增加插入点之后的所有排序顺序。同样,删除同级应该在删除点之后减少排序顺序以缩小间隙。 通过 Parent.Children 简化这种关系可能会消除试图兼顾双链表关系和父子关系的逻辑所引发的任何问题。