Отношения между сущностями

Внешние ключи и навигационные свойства

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

Для связей между сущностями в Entity Framework Core применяются внешние ключи и навигационные свойства. Так, возьмем к примеру следующие сущности:

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;

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

В данном случае сущность Company является главной сущностью, а класс User - зависимой, так как содержит ссылку на класс Company и зависит от этого класса.

Свойство CompanyId в классе User является внешним ключом, а свойство Company - навигационным свойством. По умолчанию название внешнего ключа должно принимать одно из следующих вариантов имени:

  • Имя_навигационного_свойства+Имя ключа из связанной сущности - в нашем случае имя навигационного свойства Company, а ключа из модели Company - Id, поэтому в нашем случае нам надо обозвать свойство CompanyId, что собственно и было сделано в вышеприведенном коде.

  • Имя_класса_связанной_сущности+Имя ключа из связанной сущности - в нашем случае класс Company, а имя ключа из модели Company - Id, поэтому опять же в этом случае получается CompanyId

Свойство Users, представляющее список пользователей компании, в классе Company также является навигационным свойством.

В итоге после генерации базы данных в случае с SQLite таблицы будут иметь следующее определение:

CREATE TABLE "Users" (
	"Id"	INTEGER NOT NULL,
	"Name"	TEXT,
	"CompanyId"	INTEGER NOT NULL,
	CONSTRAINT "FK_Users_Companies_CompanyId" FOREIGN KEY("CompanyId") REFERENCES "Companies"("Id") ON DELETE CASCADE,
	CONSTRAINT "PK_Users" PRIMARY KEY("Id" AUTOINCREMENT)
);

CREATE TABLE "Companies" (
	"Id"	INTEGER NOT NULL,
	"Name"	TEXT,
	CONSTRAINT "PK_Companies" PRIMARY KEY("Id" AUTOINCREMENT)
);

Установка главной сущности по навигационному свойству зависимой сущности

Причем при использовании классов нам достаточно установить либо одно навигационное свойство, либо свойство-внешний ключ. Например, укажем значение только для навигационного свойства:

using (ApplicationContext db = new ApplicationContext())
{
	Company company1 = new Company { Name = "Google" };
	Company company2 = new Company { Name = "Microsoft" };
	User user1 = new User { Name = "Tom", Company = company1 };
	User user2 = new User { Name = "Bob", Company = company2 };
	User user3 = new User { Name = "Sam", Company = company2 };

	db.Companies.AddRange(company1, company2);	// добавление компаний
	db.Users.AddRange(user1, user2, user3);		// добавление пользователей
	db.SaveChanges();

	foreach (var user in db.Users.ToList())
	{
		Console.WriteLine($"{user.Name} работает в {user.Company?.Name}");
	}
}

Консольный вывод программы:

Tom работает в Google
Bob работает в Microsoft
Sam работает в Microsoft

Установка главной сущности по свойству-внешнему ключу зависимой сущности

Также можно использовать свойство-внешний ключ для установки связи:

using (ApplicationContext db = new ApplicationContext())
{
    Company company1 = new Company { Name = "Google" };
    Company company2 = new Company { Name = "Microsoft" };
    db.Companies.AddRange(company1, company2);  // добавление компаний
    db.SaveChanges();

    User user1 = new User { Name = "Tom", CompanyId = company1.Id };
    User user2 = new User { Name = "Bob", CompanyId = company1.Id };
    User user3 = new User { Name = "Sam", CompanyId = company2.Id };

    db.Users.AddRange(user1, user2, user3);     // добавление пользователей
    db.SaveChanges();

    foreach (var user in db.Users.ToList())
    {
        Console.WriteLine($"{user.Name} работает в {user.Company?.Name}");
    }
}

Здесь надо отметить один момент: для устновки свойства внешнего ключа CompanyId нам необходимо знать его значение. Однако посколько оно связано со свойством Id класса Company, значение которого генерируется при добавление объекта в БД, соответственно в данном случае необходимо сначала добавить объект Company в базу данных.

Установка зависимой сущности через навигационное свойство главной сущности

Выше для установки связи применялась зависимая сущность - User. Но мы также можем зайти с другой стороны и установить набор зависимых сущностей через навигационное свойство главной сущности:

using (ApplicationContext db = new ApplicationContext())
{
    User user1 = new User { Name = "Tom"};
    User user2 = new User { Name = "Bob" };
    User user3 = new User { Name = "Sam"};

    Company company1 = new Company { Name = "Google", Users = { user1, user2} };
    Company company2 = new Company { Name = "Microsoft", Users = { user3 } };

    db.Companies.AddRange(company1, company2);  // добавление компаний
    db.Users.AddRange(user1, user2, user3);     // добавление пользователей
    db.SaveChanges();

    foreach (var user in db.Users.ToList())
    {
        Console.WriteLine($"{user.Name} работает в {user.Company?.Name}");
    }
}

Отсутствие свойства внешнего ключа и навигационного свойства

Нам необязательно определять внешний ключ в зависимой сущности. Его можно опустить:

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

    public Company? Company { get; set; }	// навигационное свойство
}

В этом случае Entity Framework сам автоматически сгенерирует столбец для внешнего ключа в таблице Users.

CREATE TABLE "Users" (
	"Id"	INTEGER NOT NULL,
	"Name"	TEXT,
	"CompanyId"	INTEGER,
	CONSTRAINT "PK_Users" PRIMARY KEY("Id" AUTOINCREMENT),
	CONSTRAINT "FK_Users_Companies_CompanyId" FOREIGN KEY("CompanyId") REFERENCES "Companies"("Id")
);

Преимущество определения внешнего ключа в качестве свойства состоит в том, что в каких-то ситуациях нам может потребоваться только id связанной сущности. Тем более столбец для внешнего ключа в таблице в любом случае создается.

Более того, мы можем вовсе опустить навигационное свойство в классе User:

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

Но за счет того, что в классе Company также определено навигационное свойство Users все равно будет создаваться внешний ключ и связь таблицы Users и таблицы Companies. В частности, тогда в случае БД SQLite определение таблицы Users будет выглядеть следующим образом:

CREATE TABLE "Users" (
	"Id"	INTEGER NOT NULL,
	"Name"	TEXT,
	"CompanyId"	INTEGER,
	CONSTRAINT "FK_Users_Companies_CompanyId" FOREIGN KEY("CompanyId") REFERENCES "Companies"("Id"),
	CONSTRAINT "PK_Users" PRIMARY KEY("Id" AUTOINCREMENT)
);

В отличие от первой версии таблицы здесь не добавляется каскадное удаление.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850