Загрузка связанных данных. Метод Include

Данное руководство устарело. Актуальное руководство: Руководство по Entity Framework Core 7

Последнее обновление: 16.11.2020

Через навигационные свойства мы можем загружать связанные данные. И здесь у нас три стратегии загрузки:

  • 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

ThenInclude и загрузка моделей со сложной структурой

В примере выше структура моделей довольна простая - главная сущность связана с другой простой сущностью. Рассмотрим более сложную структуру моделей. Допустим, у каждой компании есть связанная сущность - страна, где находится компания:

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
------------------------------
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850