Для связей между сущностями в 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) );
В отличие от первой версии таблицы здесь не добавляется каскадное удаление.