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

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

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

  • Eager loading (жадная загрузка)

  • Explicit loading (явная загрузка)

  • Lazy loading (ленивая загрузка)

В начале рассмотрим, что предствляет собой eager loading или жадная загрузка. Она позволяет загружать связанные данные с помощью метода Include(), в который передается навигационное свойство.

Например, пусть у нас есть следующие модели:

using Microsoft.EntityFrameworkCore;

public class ApplicationContext : DbContext
{
    public DbSet<User> Users { get; set; } = null!;
    public DbSet<Company> Companies { get; set; } = null!;

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite("Data Source=helloapp.db");
    }
}
public class Company
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public List<User> Users { get; set; } = new();
}

public class User
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public int? CompanyId { get; set; }
    public Company? Company { get; set; }
}

Добавим некоторые начальные данные и загрузим их из базы данных:

using Microsoft.EntityFrameworkCore;

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();
}
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();
}

public class User
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public int? CompanyId { get; set; }
    public Company? Company { get; set; }
}

И пусть есть следующий контекст данных

using Microsoft.EntityFrameworkCore;

public class ApplicationContext : DbContext
{
    public DbSet<User> Users { get; set; } = null!;
    public DbSet<Company> Companies { get; set; } = null!;
    public DbSet<Country> Countries { get; set; } = null!;
	
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite("Data Source=helloapp.db");
    }
}

ThenInclude

Допустим, вместе с пользователями мы хотим загрузить и страны, в которых базируются компании пользователей. То есть получается, что нам нужно спуститься еще на уровень ниже: User - Company - Country. Для этого нам надо применить метод ThenInclude(), который работает похожим образом, что и Include:

using Microsoft.EntityFrameworkCore;

// добавление данных
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 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(), через который затем подгружаются страны компаний.

При загрузке связанных данных EF Core гарантирует, что если связанная сущность не установлена (например, свойство CompanyId в объекте User равно null) то данное навигационное свойство просто будет игнорироваться. Соответственно никакой ошибки в процессе получения данных не произойдет. Но поскольку компилятор не знает об этом, то он выдает предупреждение, например, в следующем случае:

.ThenInclude(c => c.Country)    // 'c' may be null here 

В этом случае мы можем использовать оператор ! (null-forgiving оператор), чтобы указать, что значение null в данной ситуации невоможно.

.ThenInclude(c => c!.Country)    // норм, компилятор доволен 

В итоге на уровне базы данных это выльется в следующий код 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

Include

Также мы можем использовать тот же метод Include для загрузки данных далее по цепочке:

using (ApplicationContext db = new ApplicationContext())
{
    var users = db.Users
        .Include(u => u.Company!.Country)
        .ToList();
    foreach (var user in users)
        Console.WriteLine($"{user.Name} - {user.Company?.Name} - {user.Company?.Country!.Name}");
}

В этом случае будет формироваться такой же sql-запрос, и мы получим аналогичный результат.

Многоуровневая система данных

И в конце рассмотрим более сложную многоуровневую структуру сущностей:

// столица страны
public class City
{
    public int Id { get; set; }
    public string? Name { 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();
}
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();
}
// должность пользователя
public class Position
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public List<User> Users { get; set; } = new();
}
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; }
}

Теперь у каждого пользователя также есть ссылка на должность, представленную классом Position. Компания хранит ссылку на страну Country, которая хранит ссылку на столицу в виде объекта City.

Для взаимодействия с бд определим следующий контекст данных:

using Microsoft.EntityFrameworkCore;

public class ApplicationContext : DbContext
{
    public DbSet<User> Users { get; set; } = null!;
    public DbSet<Company> Companies { get; set; } = null!;
    public DbSet<City> Cities { get; set; } = null!;
    public DbSet<Country> Countries { get; set; } = null!;
    public DbSet<Position> Positions { get; set; } = null!;

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite("Data Source=helloapp.db");
    }
}

Теперь добавим начальные и данные и загрузим пользователей с детальными данными:

using Microsoft.EntityFrameworkCore;

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("----------------------");     // для красоты
    }
}

На уровне базы данных это будет транслироваться в следующий 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