Данное руководство устарело. Актуальное руководство: Руководство по Entity Framework Core 7
Через навигационные свойства мы можем загружать связанные данные. И здесь у нас три стратегии загрузки:
Eager loading (жадная загрузка)
Explicit loading (явная загрузка)
Lazy loading (ленивая загрузка)
В начале рассмотрим, что предствляет собой eager loading или жадная загрузка. Она позволяет загружать связанные данные с помощью метода Include(), в который передается навигационное свойство.
Например, пусть у нас есть следующие модели:
public class Company { public int Id { get; set; } public string Name { get; set; } public List<User> Users { get; set; } = new List<User>(); } public class User { public int Id { get; set; } public string Name { get; set; } public int? CompanyId { get; set; } public Company Company { get; set; } } public class ApplicationContext : DbContext { public DbSet<Company> Companies { get; set; } public DbSet<User> Users { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=relationsdb;Trusted_Connection=True;"); } }
Добавим некоторые начальные данные и загрузим их из базы данных:
using System; using System.Linq; using Microsoft.EntityFrameworkCore; namespace HelloApp { public class Program { public static void Main(string[] args) { using (ApplicationContext db = new ApplicationContext()) { // пересоздадим базу данных db.Database.EnsureDeleted(); db.Database.EnsureCreated(); // добавляем начальные данные Company microsoft = new Company { Name = "Microsoft" }; Company google = new Company { Name = "Google" }; db.Companies.AddRange(microsoft, google); User tom = new User { Name = "Tom", Company = microsoft }; User bob = new User { Name = "Bob", Company = google }; User alice = new User { Name = "Alice", Company = microsoft }; User kate = new User { Name = "Kate", Company = google }; db.Users.AddRange(tom, bob, alice, kate); db.SaveChanges(); // получаем пользователей var users = db.Users .Include(u=>u.Company) // подгружаем данные по компаниям .ToList(); foreach (var user in users) Console.WriteLine($"{user.Name} - {user.Company?.Name}"); } } } }
Для загрузки связанных данных используется метод Include:
var users = db.Users.Include(u=>u.Company).ToList();
Поскольку свойство Company в классе User является навигационным свойством, через которое мы можем получить связанную с пользователем компанию, то
мы можем использовать это свойство в методе Include
. На уровне базы данных это выражение будет транслироваться в следующий
SQL-запрос:
SELECT [u].[Id], [u].[CompanyId], [u].[Name], [c].[Id], [c].[Name] FROM [Users] AS [u] LEFT JOIN [Companies] AS [c] ON [u].[CompanyId] = [c].[Id]
То есть на уровне базы данных это будет означать использование выражения LEFT JOIN
, который присоединяет данные из другой таблицы.
Консольный вывод программы:
Bob - Google Tom - Microsoft Alice - Microsoft Kate - Google
Стоит отметить, что если данные уже ранее были загружены в контекст данных или просто ранее были в него добавлены, то можно не использовать метод Include для их получения, так как они уже в контексте. Например, возьмем выше приведенный пример:
using (ApplicationContext db = new ApplicationContext()) { db.Database.EnsureDeleted(); db.Database.EnsureCreated(); Company microsoft = new Company { Name = "Microsoft" }; Company google = new Company { Name = "Google" }; db.Companies.AddRange(microsoft, google); User tom = new User { Name = "Tom", Company = microsoft }; User bob = new User { Name = "Bob", Company = google }; User alice = new User { Name = "Alice", Company = microsoft }; User kate = new User { Name = "Kate", Company = google }; db.Users.AddRange(tom, bob, alice, kate); db.SaveChanges(); var users = db.Users.ToList(); // метод Include не используется foreach (var user in users) Console.WriteLine($"{user.Name} - {user.Company?.Name}"); }
Здесь не использован метод Include, но в итоге мы получим тот же самый результат. Почему? Потому что мы уже добавили все объекты в
контекст при их создании с помощью методов db.Users.AddRange()
и
db.Companies.AddRange()
и последующего сохранения с помощью вызова db.SaveChanges()
. Объекты уже в контексте, нет смысла их притягивать с
помощью метода Include. То же самое относится к ситуации, если ранее данные уже были загружены:
using (ApplicationContext db = new ApplicationContext()) { var companies = db.Companies.ToList(); // получаем пользователей var users = db.Users //.Include(u => u.Company) // подгружаем данные по компаниям .ToList(); foreach (var user in users) Console.WriteLine($"{user.Name} - {user.Company?.Name}"); }
Здесь к моменту получения пользователей компании уже загружены в констекст, поэтому нет смысла использоваться метод Include.
Теперь рассмотрим другую ситуацию:
public static void Main(string[] args) { using (ApplicationContext db = new ApplicationContext()) { // пересоздадим базу данных db.Database.EnsureDeleted(); db.Database.EnsureCreated(); Company microsoft = new Company { Name = "Microsoft" }; Company google = new Company { Name = "Google" }; db.Companies.AddRange(microsoft, google); User tom = new User { Name = "Tom", Company = microsoft }; User bob = new User { Name = "Bob", Company = google }; User alice = new User { Name = "Alice", Company = microsoft }; User kate = new User { Name = "Kate", Company = google }; db.Users.AddRange(tom, bob, alice, kate); db.SaveChanges(); } using (ApplicationContext db = new ApplicationContext()) { var users = db.Users .Include(u => u.Company) // добавляем данные по компаниям .ToList(); foreach (var user in users) Console.WriteLine($"{user.Name} - {user.Company?.Name}"); } }
Здесь программа логически разделена на две части: добавление объектов и их получение. Для каждой части создается свой объект ApplicationContext. В итоге при получении объект ApplicationContext не будет ничего знать об объектах, которые были добавлены в области действия другого объекта ApplicationContext. Поэтому в этом случае, если мы хотим получить связанные данные, нам необходимо использовать метод Include.
Подобным образом мы можем получить компании и подгрузить к ним связанных с ними пользователей через навигационное свойство Users в классе Company:
var companies = db.Companies .Include(c => c.Users) // добавляем данные по пользователям .ToList(); foreach (var company in companies) { Console.WriteLine(company.Name); // выводим сотрудников компании foreach (var user in company.Users) Console.WriteLine(user.Name); Console.WriteLine("----------------------"); // для красоты }
Консольный вывод:
Microsoft Tom Alice ------------------------------- Google Bob Kate
В примере выше структура моделей довольна простая - главная сущность связана с другой простой сущностью. Рассмотрим более сложную структуру моделей. Допустим, у каждой компании есть связанная сущность - страна, где находится компания:
public class Country { public int Id { get; set; } public string Name { get; set; } public List<Company> Companies { get; set; } = new List<Company>(); } public class Company { public int Id { get; set; } public string Name { get; set; } public int CountryId { get; set; } public Country Country { get; set; } public List<User> Users { get; set; } = new List<User>(); } public class User { public int Id { get; set; } public string Name { get; set; } public int? CompanyId { get; set; } public Company Company { get; set; } } public class ApplicationContext : DbContext { public DbSet<Country> Countries { get; set; } public DbSet<Company> Companies { get; set; } public DbSet<User> Users { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=relationsdb;Trusted_Connection=True;"); } }
Допустим, вместе с пользователями мы хотим загрузить и страны, в которых базируются компании пользователей. То есть получается, что нам нужно спуститься еще на уровень ниже: User - Company - Country. Для этого нам надо применить метод ThenInclude(), который работает похожим образом, что и Include:
using System; using System.Linq; using Microsoft.EntityFrameworkCore; namespace HelloApp { public class Program { public static void Main(string[] args) { // добавление данных using (ApplicationContext db = new ApplicationContext()) { // пересоздадим базу данных db.Database.EnsureDeleted(); db.Database.EnsureCreated(); Country usa = new Country { Name = "USA" }; Country japan = new Country { Name = "Japan" }; db.Countries.AddRange(usa, japan); // добавляем начальные данные Company microsoft = new Company { Name = "Microsoft", Country = usa }; Company sony = new Company { Name = "Sony", Country = japan }; db.Companies.AddRange(microsoft, sony); User tom = new User { Name = "Tom", Company = microsoft }; User bob = new User { Name = "Bob", Company = sony }; User alice = new User { Name = "Alice", Company = microsoft }; User kate = new User { Name = "Kate", Company = sony }; db.Users.AddRange(tom, bob, alice, kate); db.SaveChanges(); } // получение данных using (ApplicationContext db = new ApplicationContext()) { var companies = db.Companies.ToList(); // получаем пользователей var users = db.Users .Include(u => u.Company) // подгружаем данные по компаниям .ThenInclude(c => c.Country) // к компаниям подгружаем данные по странам .ToList(); foreach (var user in users) Console.WriteLine($"{user.Name} - {user.Company?.Name} - {user.Company?.Country?.Name}"); } } } }
Вначале загружаются данные пользователям. Затем загружаются связанные данные по компании. И чтобы пойти дальше по цепочке навигационных свойств, надо использовать метод ThenInclude(), через который затем подгружаются страны компаний. На уровне базы данных это выльется в следующий код SQL:
SELECT [u].[Id], [u].[CompanyId], [u].[Name], [c].[Id], [c].[CountryId], [c].[Name], [c0].[Id], [c0].[Name] FROM [Users] AS [u] LEFT JOIN [Companies] AS [c] ON [u].[CompanyId] = [c].[Id] LEFT JOIN [Countries] AS [c0] ON [c].[CountryId] = [c0].[Id]
В итоге мы получим следующий консольный вывод:
Tom - Microsoft - USA Alice - Microsoft - USA Bob - Sony - Japan Kate - Sony - Japan
И в конце рассмотрим более сложную многоуровневую структуру моделей:
using Microsoft.EntityFrameworkCore; using System.Collections.Generic; namespace HelloApp { public class ApplicationContext : DbContext { public DbSet<Company> Companies { get; set; } public DbSet<User> Users { get; set; } public DbSet<City> Cities { get; set; } public DbSet<Country> Countries { get; set; } public DbSet<Position> Positions { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=relationsdb;Trusted_Connection=True;"); } } public class Company { public int Id { get; set; } public string Name { get; set; } public int CountryId { get; set; } public Country Country { get; set; } public List<User> Users { get; set; } = new List<User>(); } // должность пользователя public class Position { public int Id { get; set; } public string Name { get; set; } public List<User> Users { get; set; } = new List<User>(); } public class User { public int Id { get; set; } public string Name { get; set; } public int? CompanyId { get; set; } public Company Company { get; set; } public int? PositionId { get; set; } public Position Position { get; set; } } // страна компании public class Country { public int Id { get; set; } public string Name { get; set; } public int CapitalId { get; set; } public City Capital { get; set; } // столица страны public List<Company> Companies { get; set; } = new List<Company>(); } // столица страны public class City { public int Id { get; set; } public string Name { get; set; } } }
Теперь у каждого пользователя также есть ссылка на должность, представленную классом Position. Компания хранит ссылку на страну Country, которая хранит ссылку на столицу в виде объекта City. Теперь добавим начальные и данные и загрузим пользователей с детальными данными:
using System; using System.Linq; using Microsoft.EntityFrameworkCore; namespace HelloApp { public class Program { public static void Main(string[] args) { using (ApplicationContext db = new ApplicationContext()) { // пересоздадим базу данных db.Database.EnsureDeleted(); db.Database.EnsureCreated(); Position manager = new Position { Name = "Manager" }; Position developer = new Position { Name = "Developer" }; db.Positions.AddRange(manager, developer); City washington = new City { Name = "Washington" }; db.Cities.Add(washington); Country usa = new Country { Name = "USA", Capital = washington }; db.Countries.Add(usa); Company microsoft = new Company { Name = "Microsoft", Country = usa }; Company google = new Company { Name = "Google", Country = usa }; db.Companies.AddRange(microsoft, google); User tom = new User { Name = "Tom", Company = microsoft, Position = manager }; User bob = new User { Name = "Bob", Company = google, Position = developer }; User alice = new User { Name = "Alice", Company = microsoft, Position = developer }; User kate = new User { Name = "Kate", Company = google, Position = manager }; db.Users.AddRange(tom, bob, alice, kate); db.SaveChanges(); } using (ApplicationContext db = new ApplicationContext()) { // получаем пользователей var users = db.Users .Include(u => u.Company) // добавляем данные по компаниям .ThenInclude(comp => comp.Country) // к компании добавляем страну .ThenInclude(count => count.Capital) // к стране добавляем столицу .Include(u => u.Position) // добавляем данные по должностям .ToList(); foreach (var user in users) { Console.WriteLine($"{user.Name} - {user.Position.Name}"); Console.WriteLine($"{user.Company?.Name} - {user.Company?.Country.Name} - {user.Company?.Country.Capital.Name}"); Console.WriteLine("----------------------"); // для красоты } } Console.Read(); } } }
На уровне базы данных это будет транслироваться в следующий SQL-запрос:
SELECT u.Id, u.CompanyId, u.Name, u.PositionId, c.Id, c.CountryId, c.Name, c0.Id, c0.CapitalId, c0.Name, c1.Id, c1.Name, p.Id, p.Name FROM Users AS u LEFT JOIN Companies AS c ON u.CompanyId == c.Id LEFT JOIN Countries AS c0 ON c.CountryId == c0.Id LEFT JOIN Cities AS c1 ON c0.CapitalId == c1.Id LEFT JOIN Positions AS p ON u.PositionId == p.Id)
В итоге мы получим следующий консольный вывод:
Tom - Manager Microsoft - USA - Washington ------------------------------ Alice - Developer Microsoft - USA - Washington ------------------------------ Bob - Developer Google - USA - Washington ------------------------------ Kate - Manager Google - USA - Washington ------------------------------