我正在Metait.com上学习“分层数据”课程。例如,采用一个递归实体,它可以具有到父实体及其子实体的链接:MenuItem
public class MenuItem
{
public int Id { get; set; }
public string? Title { get; set; }
public int? ParentId { get; set; }
public MenuItem? Parent { get; set; }
public List<MenuItem> Children { get; set; } = new();
}
现在,如果我们MenuItem
按如下方式初始化几个:
using (ApplicationContext db = new ApplicationContext())
{
// пересоздаем бд
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
// добавляем начальные данные
MenuItem file = new MenuItem { Title = "File" };
MenuItem edit = new MenuItem { Title = "Edit" };
MenuItem open = new MenuItem { Title = "Open", Parent = file };
MenuItem save = new MenuItem { Title = "Save", Parent = file };
MenuItem copy = new MenuItem { Title = "Copy", Parent = edit };
MenuItem paste = new MenuItem { Title = "Paste", Parent = edit };
db.MenuItems.AddRange(file, edit, open, save, copy, paste);
db.SaveChanges();
}
那么,例如,Menuitem
对于标题,File
将会有MenuItem
带有标题Open
和的子项Save
:
using (ApplicationContext db = new ApplicationContext())
{
// получаем все пункты меню из бд
var menuItems = db.MenuItems.ToList();
Console.WriteLine("All Menu:");
foreach (MenuItem m in menuItems)
{
Console.WriteLine(m.Title);
}
Console.WriteLine();
// получаем определенный пункт меню с подменю
var fileMenu = db.MenuItems.FirstOrDefault(m => m.Title == "File");
if(fileMenu != null)
{
Console.WriteLine(fileMenu.Title);
foreach(var m in fileMenu.Children)
{
Console.WriteLine($"---{m.Title}");
}
}
}
File
---Open
---Save
但是该字段是如何初始化的Children
呢?我们没有在任何地方显式设置它的值,并且在生成的 SQL 查询中完全不存在这样的字段:
CREATE TABLE "MenuItems" (
"Id" INTEGER NOT NULL,
"Title" TEXT,
"ParentId" INTEGER,
CONSTRAINT "FK_MenuItems_MenuItems_ParentId" FOREIGN KEY("ParentId") REFERENCES "MenuItems"("Id"),
CONSTRAINT "PK_MenuItems" PRIMARY KEY("Id" AUTOINCREMENT)
);
在您的示例中,ParentId 字段存储一个外键来引用父菜单项,并且 Parent 和 Children 之间的关系通过一对多关系表示。 ParentId 字段指向父元素。父级导航属性建立到父级的链接。 Children 属性是对将当前元素作为其父元素(通过其 ParentId 字段)的子元素的反向引用。当您执行以下操作时:
在底层,EF 向数据库发送查询以获取 ParentId 指向 fileMenu 元素的所有元素。 Children 字段是导航属性,不直接存储在数据库中。数据库仅存储指向父级的外键(ParentId)。当需要检索子元素时,EF会自动使用外键来查询数据库并加载关联数据。
这都是关于工作原理的
Entity Framework
。导航属性是
Entity Framework
实体类中的属性,允许您导航到相关实体。它用于定义实体之间的关系(例如一对一、一对多和多对多)。导航属性允许您加载和使用与当前实体关联的数据。这是一个更清晰的示例:假设存在实体(模型)
Company
和User
,其中每个用户属于一家公司。然后您可以按如下方式定义导航属性:
在此示例中,
Company
它有一个导航属性Users
,表示与该公司关联的用户集合。User
有一个导航属性Company
,指向用户所属的公司。当您从数据库加载数据时,如果您使用按类型加载方法,则会
Entity Framework
自动Include
填充这些导航属性:同样重要的是要记住,如果数据库中的数据未正确链接,导航属性将无法按预期工作。导航属性取决于建立实体之间关系的有效外键。
从原来的例子来看:
如果您有
MenuItem
带有 field 的实体ParentId
,但该字段为空或填充不正确,则它将Entity Framework
无法确定哪些元素是该元素的子元素。因此,导航属性Children
将保持为空。为了使导航属性正常工作,在向数据库添加数据时必须正确设置所有外键。
PS 一定的荣耀属于用户@Alexander Petrov,他为我的问题提供了正确的想法,这间接影响了这个问题,为此我感谢他。
PPS 在数据库表中,将关键字段设置
ID
为自增,否则以后不会出现异常 - 我测试了它。Database First
该方法的示例:但最好使用
Code First
以下方法(可以避免不必要的错误):阅读文章加载链接数据。包括方法。
要获得子元素的第一层嵌套,您需要在代码中添加对 Include 方法的调用:
简而言之:这是变更跟踪机制的结果。
如果运行以下代码:
那么它只会被显示
File
。该集合Children
将为空。如果在开头添加行:
然后——突然! - 值
Open
, , 也会显示Save
。该行加载了数据库中的所有记录,并且更改跟踪器跟踪所有连接并设置适当的链接。他之所以能够做到这一点,是因为所有数据都已下载并可供他使用。如果你紧接着写:
然后它会输出:
它清楚地表明Change Tracker已经建立了实体之间的所有连接。
有关更多详细信息,请参阅更改外键和导航文档。
但是,如果数据库中有很多记录,则加载整个表的成本太高。然后我们仅加载必要的数据,并使用以下命令指定所需的导航属性
Include
:这会生成以下请求:
在这里您可以清楚地看到如何按字段
Id
和进行合并ParentId
。可以通过其他方式加载相关数据:加载相关数据。
要了解整个厨房,您应该阅读整个文档Change Tracking in EF Core。
您可以在左下角将语言切换为俄语(或其他语言)。
要查看生成的请求,请将日志记录添加到
OnConfiguring
上下文方法,例如,如下所示: