Данное руководство устарело. Актуальное руководство: Руководство по Entity Framework Core 7
Для связей между моделями в Entity Framework Core применяются внешние ключи и навигационные свойства. Так, возьмем к примеру следующие модели:
public class Company { public int Id { get; set; } public string Name { get; set; } // название компании public List<User> Users { get; set; } } 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; } public ApplicationContext() { Database.EnsureDeleted(); Database.EnsureCreated(); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=relationsdb;Trusted_Connection=True;"); } }
В данном случае сущность Company является главной сущностью, а класс User - зависимой, так как содержит ссылку на класс Company и зависит от этого класса.
Свойство CompanyId
в классе User является внешним ключом, а свойство Company
- навигационным свойством.
По умолчанию название внешнего ключа должно принимать одно из следующих вариантов имени:
Имя_навигационного_свойства+Имя ключа из связанной сущности - в нашем случае имя навигационного свойства Company, а ключа из модели Company - Id, поэтому в нашем случае нам надо обозвать свойство CompanyId, что собственно и было сделано в вышеприведенном коде.
Имя_класса_связанной_сущности+Имя ключа из связанной сущности - в нашем случае класс Company, а имя ключа из модели Company - Id, поэтому опять же в этом случае получается CompanyId
Свойство Users
, представляющее список пользователей компании, в классе Company также является навигационным свойством.
В итоге после генерации базы данных таблица Users будет иметь следующее определение:
CREATE TABLE [dbo].[Users] ( [Id] INT IDENTITY (1, 1) NOT NULL, [Name] NVARCHAR (MAX) NULL, [CompanyId] INT NOT NULL, CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_Users_Companies_CompanyId] FOREIGN KEY ([CompanyId]) REFERENCES [dbo].[Companies] ([Id]) ON DELETE CASCADE );
Но нам необязательно определять внешний ключ в зависимой сущности. Его можно опустить:
public class User { public int Id { get; set; } public string Name { get; set; } public Company Company { get; set; } // навигационное свойство }
В этом случае Entity Framework сам автоматически сгенерирует столбец для внешнего ключа в таблице Users. Преимущество определения внешнего ключа в качестве свойства состоит в том, что в каких-то ситуациях нам может потребоваться только id связанной сущности. Тем более столбец для внешнего ключа в таблице в любом случае создается.
Более того, мы можем вовсе опустить навигационное свойство в классе User:
public class User { public int Id { get; set; } public string Name { get; set; } }
Но за счет того, что в классе Company также определено навигационное свойство Users все равно будет создаваться внешний ключ и связь таблицы Users и таблицы Companies. В частности, в этом случае определение таблицы Users будет выглядеть следующим образом:
CREATE TABLE [dbo].[Users] ( [Id] INT IDENTITY (1, 1) NOT NULL, [Name] NVARCHAR (MAX) NULL, [CompanyId] INT NULL, CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_Users_Companies_CompanyId] FOREIGN KEY ([CompanyId]) REFERENCES [dbo].[Companies] ([Id]) );
В отличие от первой версии таблицы здесь не добавляется каскадное удаление.
В принципе название свойства - внешнего ключа необязательно должно следовать выше описанным условностям. Чтобы установить свойство в качестве внешнего ключа, применяется атрибут [ForeignKey]:
using System.ComponentModel.DataAnnotations.Schema; public class User { public int Id { get; set; } public string Name { get; set; } public int CompanyInfoKey { get; set; } [ForeignKey("CompanyInfoKey")] public Company Company { get; set; } }
В этом случае на уровне базы данных для этой модели будет генерироваться следующая таблица:
CREATE TABLE [dbo].[Users] ( [Id] INT IDENTITY (1, 1) NOT NULL, [Name] NVARCHAR (MAX) NULL, [CompanyInfoKey] INT NOT NULL, CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_Users_Companies_CompanyInfoKey] FOREIGN KEY ([CompanyInfoKey]) REFERENCES [dbo].[Companies] ([Id]) ON DELETE CASCADE );
Для настройки отношений между моделями с помощью Fluent API применяются специальные методы: HasOne / HasMany / WithOne / WithMany
.
Методы HasOne
и HasMany
устанавливают навигационное свойство для сущности, для которой производится конфигурация. Далее
могут идти вызовы методов WithOne
и WithMany
, который идентифицируют навигационное свойство на стороне связанной сущности.
Методы HasOne/WithOne
применяются для обычного навигационного свойства, представляющего одиночный объект, а методы
HasMany/WithMany
используются для навигационных свойств, представляющих коллекции. Сам же внешний ключ устанавливается с помощью метода
HasForeignKey:
public class ApplicationContext : DbContext { public DbSet<Company> Companies { get; set; } public DbSet<User> Users { get; set; } public ApplicationContext() { Database.EnsureDeleted(); Database.EnsureCreated(); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=relationsdb;Trusted_Connection=True;"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<User>() .HasOne(p => p.Company) .WithMany(t => t.Users) .HasForeignKey(p => p.CompanyInfoKey); } } public class Company { public int Id { get; set; } public string Name { get; set; } public List<User> Users { get; set; } } public class User { public int Id { get; set; } public string Name { get; set; } public int CompanyInfoKey { get; set; } public Company Company { get; set; } }
Кроме того, с помощью Fluent API мы можем связь внешнего ключа не только с первичными ключами связанных сущностей, но и с другими свойствами. Например:
public class ApplicationContext : DbContext { public DbSet<Company> Companies { get; set; } public DbSet<User> Users { get; set; } public ApplicationContext() { Database.EnsureDeleted(); Database.EnsureCreated(); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=relationsdb;Trusted_Connection=True;"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<User>() .HasOne(p => p.Company) .WithMany(t => t.Users) .HasForeignKey(p => p.CompanyName) .HasPrincipalKey(t=>t.Name); } } public class Company { public int Id { get; set; } public string Name { get; set; } public List<User> Users { get; set; } } public class User { public int Id { get; set; } public string Name { get; set; } public string CompanyName { get; set; } public Company Company { get; set; } }
Метод HasPrincipalKey указывает на свойство связанной сущности, на которую будет ссылаться свойство-внешний ключ CompanyName. Кроме того, для
свойства, указанного в HasPrincipalKey()
, будет создавать альтернативный ключ.
Причем при использовании классов нам достаточно установить либо одно навигационное свойство, либо свойство-внешний ключ. Например, укажем значение только для навигационного свойства:
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" }; User user1 = new User { Name = "Tom", CompanyName = company1.Name }; User user2 = new User { Name = "Bob", CompanyName = "Microsoft" }; User user3 = new User { Name = "Sam", CompanyName = company2.Name }; 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}"); } }
Результат работы будет тот же самый.