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

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

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

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

Для связей между моделями в 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

Для настройки отношений между моделями с помощью 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}");
	}
}

Результат работы будет тот же самый.

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