RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 1592703
Accepted
Боков Глеб
Боков Глеб
Asked:2024-09-03 13:40:11 +0000 UTC2024-09-03 13:40:11 +0000 UTC 2024-09-03 13:40:11 +0000 UTC

如何在实体框架的分层数据中初始化子实体?

  • 772

我正在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)
);
c#
  • 4 4 个回答
  • 164 Views

4 个回答

  • Voted
  1. PoSha
    2024-09-09T01:29:58Z2024-09-09T01:29:58Z

    在您的示例中,ParentId 字段存储一个外键来引用父菜单项,并且 Parent 和 Children 之间的关系通过一对多关系表示。 ParentId 字段指向父元素。父级导航属性建立到父级的链接。 Children 属性是对将当前元素作为其父元素(通过其 ParentId 字段)的子元素的反向引用。当您执行以下操作时:

        foreach(var m in fileMenu.Children)
    {
        Console.WriteLine($"---{m.Title}");
    }
    

    在底层,EF 向数据库发送查询以获取 ParentId 指向 fileMenu 元素的所有元素。 Children 字段是导航属性,不直接存储在数据库中。数据库仅存储指向父级的外键(ParentId)。当需要检索子元素时,EF会自动使用外键来查询数据库并加载关联数据。

    • 2
  2. Chaos_Sower
    2024-09-10T03:15:25Z2024-09-10T03:15:25Z

    这都是关于工作原理的Entity Framework。

    导航属性是Entity Framework实体类中的属性,允许您导航到相关实体。它用于定义实体之间的关系(例如一对一、一对多和多对多)。导航属性允许您加载和使用与当前实体关联的数据。

    这是一个更清晰的示例:假设存在实体(模型)Company和User,其中每个用户属于一家公司。

    然后您可以按如下方式定义导航属性:

    public class Company
    {
        public int Id { get; private set; }
        public string Name { get; set; }
        public List<User> Users { get; set; } = new();
    }
    
    public class User
    {
        public int Id { get; private set; }
        public string Name { get; set; }
        public int CompanyId { get; set; } // внешний ключ
        public Company Company { get; set; } // навигационное свойство
    }
    

    在此示例中,Company它有一个导航属性Users,表示与该公司关联的用户集合。User有一个导航属性Company,指向用户所属的公司。

    当您从数据库加载数据时,如果您使用按类型加载方法,则会Entity Framework 自动Include填充这些导航属性:

    using (ApplicationContext db = new ApplicationContext())
    {
        var company = db.Companies
            .Include(c => c.Users)
            .FirstOrDefault(c => c.Name == "Google");
        
        if (company != null)
        {
            Console.WriteLine(company.Name);
            
            foreach (var user in company.Users)
            {
                Console.WriteLine($"---{user.Name}");
            }
        }
    }
    

    同样重要的是要记住,如果数据库中的数据未正确链接,导航属性将无法按预期工作。导航属性取决于建立实体之间关系的有效外键。

    从原来的例子来看:

    如果您有MenuItem带有 field 的实体ParentId,但该字段为空或填充不正确,则它将Entity Framework无法确定哪些元素是该元素的子元素。因此,导航属性Children 将保持为空。

    为了使导航属性正常工作,在向数据库添加数据时必须正确设置所有外键。

    PS 一定的荣耀属于用户@Alexander Petrov,他为我的问题提供了正确的想法,这间接影响了这个问题,为此我感谢他。

    PPS 在数据库表中,将关键字段设置ID为自增,否则以后不会出现异常 - 我测试了它。

    Database First该方法的示例:

    -- Создание базы данных
    CREATE DATABASE ContactsNotebook;
    GO
    
    -- Использование базы данных
    USE ContactsNotebook;
    GO
    
    -- Создание таблицы ContactsInfo
    CREATE TABLE ContactsInfo
    (
        ID INT IDENTITY(1,1) PRIMARY KEY,
        Name VARCHAR(50) NOT NULL,
        Surname VARCHAR(50) NOT NULL,
        Patronymic VARCHAR(50),
        Sex CHAR(1) NOT NULL
    );
    GO
    
    -- Вставка данных в таблицу ContactsInfo
    INSERT INTO ContactsInfo (Name, Surname, Patronymic, Sex) VALUES
    ('Анна', 'Ильюшина', 'Васильевна', 'Ж'),
    ('Кирилл', 'Андреев', NULL, 'М')
    GO
    
    -- Создание таблицы PhoneTypes
    CREATE TABLE PhoneTypes
    (
        ID INT IDENTITY(1,1) PRIMARY KEY,
        PhoneType VARCHAR(50) NOT NULL
    );
    GO
    
    -- Вставка данных в таблицу PhoneTypes
    INSERT INTO PhoneTypes (PhoneType) VALUES
    ('Рабочий'),
    ('Мобильный'),
    ('Домашний'),
    ('Стационарный'),
    ('Офисный');
    GO
    
    -- Создание таблицы Contacts
    CREATE TABLE Contacts
    (
        ID INT IDENTITY(1,1) PRIMARY KEY,
        PhoneNumber NVARCHAR(18) NOT NULL,
        PhoneTypeID INT,
        ContactInfoID INT NOT NULL,
        FOREIGN KEY (PhoneTypeID) REFERENCES PhoneTypes(ID),
        FOREIGN KEY (ContactInfoID) REFERENCES ContactsInfo(ID)
    );
    GO
    
    -- Вставка данных в таблицу Contacts
    INSERT INTO Contacts (PhoneNumber, PhoneTypeID, ContactInfoID) VALUES
    ('+7 (917) 363-53-08', 1, 1),
    ('+7 (915) 223-33-63', 2, 2)
    GO
    

    但最好使用Code First以下方法(可以避免不必要的错误):

    /// <summary>
    /// Асинхронная задача создания БД с данными
    /// </summary>
    /// <returns></returns>
    private async Task InitializeDatabaseAsync()
    {
        Database.Create();
        await SeedAsync(this);
    }
    
    /// <summary>
    /// Метод создания таблиц по заданным параметрам
    /// </summary>
    /// <param name="DBModelBuilder"></param>
    protected override void OnModelCreating(DbModelBuilder DBModelBuilder)
    {
        DBModelBuilder.Entity<ContactInfo>()
            .HasKey(c => c.ID)
            .Property(c => c.ID)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
        
        DBModelBuilder.Entity<PhoneType>()
            .HasKey(p => p.ID)
            .Property(p => p.ID)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
        
        DBModelBuilder.Entity<Contact>()
            .HasKey(c => c.ID)
            .Property(c => c.ID)
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
        
        DBModelBuilder.Entity<Contact>()
            .HasRequired(c => c.ContactInfo)
            .WithMany(ci => ci.Contacts)
            .HasForeignKey(c => c.ContactInfoID);
        
        DBModelBuilder.Entity<Contact>()
            .HasOptional(c => c.PhoneType)
            .WithMany(pt => pt.Contacts)
            .HasForeignKey(c => c.PhoneTypeID);
    }
    
    /// <summary>
    /// Асинхронная задача внесения данных в таблицы БД
    /// </summary>
    /// <param name="ApplicationContext"></param>
    /// <returns></returns>
    private static async Task SeedAsync(ApplicationContext ApplicationContext)
    {
        List<PhoneType> PhoneTypes =
        [
            new() { PhoneTypeText = "Рабочий" },
            new() { PhoneTypeText = "Мобильный" },
            new() { PhoneTypeText = "Домашний" },
            new() { PhoneTypeText = "Стационарный" },
            new() { PhoneTypeText = "Офисный" }
        ];
        
        List<ContactInfo> ContactsInfo =
        [
            new() { Name = "Анна", Surname = "Ильюшина", Patronymic = "Васильевна", Sex = "Ж" },
            new() { Name = "Кирилл", Surname = "Андреев", Patronymic = null, Sex = "М" }
        ];
        
        List<Contact> Contacts =
        [
            new() { PhoneNumber = "+7 (917) 363-53-08", PhoneTypeID = 1, ContactInfoID = 1 },
            new() { PhoneNumber = "+7 (915) 223-33-63", PhoneTypeID = 2, ContactInfoID = 2 }
        ];
        
        ApplicationContext.PhoneTypes?.AddRange(PhoneTypes);
        ApplicationContext.ContactsInfo?.AddRange(ContactsInfo);
        await ApplicationContext.SaveChangesAsync(); // сначала нужно сохранить таблицу, которую мы свяжем по внешнему ключу! Одновременное сохранение вызывает исключение
        
        ApplicationContext.Contacts?.AddRange(Contacts);
        await ApplicationContext.SaveChangesAsync();
    }
    
    • 1
  3. DmitriySidyakin
    2024-09-10T05:00:04Z2024-09-10T05:00:04Z

    阅读文章加载链接数据。包括方法。

    要获得子元素的第一层嵌套,您需要在代码中添加对 Include 方法的调用:

    using (ApplicationContext db = new ApplicationContext())
    {
    
    ...
    
        // получаем все пункты меню из бд
        var menuItems = db.MenuItems.Include((m) => m.Children).ToList();
    
    ...
    
    }
    
    • 1
  4. Best Answer
    Alexander Petrov
    2024-09-12T03:43:47Z2024-09-12T03:43:47Z

    简而言之:这是变更跟踪机制的结果。

    如果运行以下代码:

    using var db = new ApplicationContext();
    
    var fileMenu = db.MenuItems.First(m => m.Title == "File");
    
    Console.WriteLine(fileMenu.Title);
    
    foreach (var m in fileMenu.Children)
        Console.WriteLine($"---{m.Title}");
    

    那么它只会被显示File。该集合Children将为空。
    如果在开头添加行:

    var menuItems = db.MenuItems.ToList();
    

    然后——突然! - 值Open, , 也会显示Save。
    该行加载了数据库中的所有记录,并且更改跟踪器跟踪所有连接并设置适当的链接。他之所以能够做到这一点,是因为所有数据都已下载并可供他使用。如果你紧接着写:

    Console.WriteLine(db.ChangeTracker.DebugView.LongView);
    

    然后它会输出:

    MenuItem {Id: 1} Unchanged
        Id: 1 PK
        ParentId: <null> FK
        Title: 'File'
      Children: [{Id: 3}, {Id: 4}]
      Parent: <null>
    MenuItem {Id: 2} Unchanged
        Id: 2 PK
        ParentId: <null> FK
        Title: 'Edit'
      Children: [{Id: 5}, {Id: 6}]
      Parent: <null>
    MenuItem {Id: 3} Unchanged
        Id: 3 PK
        ParentId: 1 FK
        Title: 'Open'
      Children: []
      Parent: {Id: 1}
    MenuItem {Id: 4} Unchanged
        Id: 4 PK
        ParentId: 1 FK
        Title: 'Save'
      Children: []
      Parent: {Id: 1}
    MenuItem {Id: 5} Unchanged
        Id: 5 PK
        ParentId: 2 FK
        Title: 'Copy'
      Children: []
      Parent: {Id: 2}
    MenuItem {Id: 6} Unchanged
        Id: 6 PK
        ParentId: 2 FK
        Title: 'Paste'
      Children: []
      Parent: {Id: 2}
    

    它清楚地表明Change Tracker已经建立了实体之间的所有连接。

    有关更多详细信息,请参阅更改外键和导航文档。


    但是,如果数据库中有很多记录,则加载整个表的成本太高。然后我们仅加载必要的数据,并使用以下命令指定所需的导航属性Include:

    var fileMenu = db.MenuItems.Include(m => m.Children).First(m => m.Title == "File");
    

    这会生成以下请求:

    SELECT [t].[Id], [t].[ParentId], [t].[Title], [m0].[Id], [m0].[ParentId], [m0].[Title]
    FROM (
        SELECT TOP(1) [m].[Id], [m].[ParentId], [m].[Title]
        FROM [MenuItems] AS [m]
        WHERE [m].[Title] = N'File'
    ) AS [t]
    LEFT JOIN [MenuItems] AS [m0] ON [t].[Id] = [m0].[ParentId]
    ORDER BY [t].[Id]
    

    在这里您可以清楚地看到如何按字段Id和进行合并ParentId。

    可以通过其他方式加载相关数据:加载相关数据。


    要了解整个厨房,您应该阅读整个文档Change Tracking in EF Core。
    您可以在左下角将语言切换为俄语(或其他语言)。

    要查看生成的请求,请将日志记录添加到OnConfiguring上下文方法,例如,如下所示:

    optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);
    
    • 1

相关问题

  • 使用嵌套类导出 xml 文件

  • 分层数据模板 [WPF]

  • 如何在 WPF 中为 ListView 手动创建列?

  • 在 2D 空间中,Collider 2D 挂在玩家身上,它对敌人的重量相同,我需要它这样当它们碰撞时,它们不会飞向不同的方向。统一

  • 如何在 c# 中使用 python 神经网络来创建语音合成?

  • 如何知道类中的方法是否属于接口?

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    我看不懂措辞

    • 1 个回答
  • Marko Smith

    请求的模块“del”不提供名为“default”的导出

    • 3 个回答
  • Marko Smith

    "!+tab" 在 HTML 的 vs 代码中不起作用

    • 5 个回答
  • Marko Smith

    我正在尝试解决“猜词”的问题。Python

    • 2 个回答
  • Marko Smith

    可以使用哪些命令将当前指针移动到指定的提交而不更改工作目录中的文件?

    • 1 个回答
  • Marko Smith

    Python解析野莓

    • 1 个回答
  • Marko Smith

    问题:“警告:检查最新版本的 pip 时出错。”

    • 2 个回答
  • Marko Smith

    帮助编写一个用值填充变量的循环。解决这个问题

    • 2 个回答
  • Marko Smith

    尽管依赖数组为空,但在渲染上调用了 2 次 useEffect

    • 2 个回答
  • Marko Smith

    数据不通过 Telegram.WebApp.sendData 发送

    • 1 个回答
  • Martin Hope
    Alexandr_TT 2020年新年大赛! 2020-12-20 18:20:21 +0000 UTC
  • Martin Hope
    Alexandr_TT 圣诞树动画 2020-12-23 00:38:08 +0000 UTC
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5