Данное руководство устарело. Актуальное руководство: Руководство по Entity Framework Core 7
Отношение один к одному предполагает, что главная сущность может ссылаться только на один объект зависимой сущности. В свою очередь, зависимая сущность может ссылаться только на один объект главной сущности.
Рассмотрим стандартный пример подобных отношений: есть класс пользователя User, который хранит логин и пароль, то есть данные учетных записей. А все данные профиля, такие как имя, возраст и так далее, выделяются в класс профиля UserProfile.
public class User { public int Id { get; set; } public string Login { get; set; } public string Password { get; set; } public UserProfile Profile { get; set; } } public class UserProfile { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } public int UserId { get; set; } public User User { get; set; } } public class ApplicationContext : DbContext { public DbSet<User> Users { get; set; } public DbSet<UserProfile> UserProfiles { get; set; } public ApplicationContext() { Database.EnsureDeleted(); Database.EnsureCreated(); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=relationsdb;Trusted_Connection=True;"); } }
В этой связи между классами сущность UserProfile является зависимой по отношению к сущности User.
И чтобы установить связь один к одному, у зависимой сущности устанавливается свойство внешний ключ: public int UserId { get; set; }
.
Благодаря этому Entity Framework узнает, что UserProfile является зависимой сущностью. К примеру, в классе User также есть навигационное свойство - ссылка на объект
UserProfile, но при этом внешний ключ отсутствует.
В итоге для сущности UserProfile будет создана следующая таблица в базе данных:
CREATE TABLE [dbo].[UserProfiles] ( [Id] INT IDENTITY (1, 1) NOT NULL, [Age] INT NOT NULL, [Name] NVARCHAR (MAX) NULL, [UserId] INT NOT NULL, CONSTRAINT [PK_UserProfiles] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_UserProfiles_Users_UserId] FOREIGN KEY ([UserId]) REFERENCES [dbo].[Users] ([Id]) ON DELETE CASCADE );
Внешне эта таблица не отличается от таблицы, которая создается для зависимой сущности при связи один-ко-многим. Однако также стоит добавить, что для этой таблицы для столбца, который представляет внешний ключ (в данном случае UserId), создается уникальный индекс. И этот индекс гарантирует, что только одна зависимая сущность (здесь UserProfile) может быть связана с одной главной сущностью (здесь сущность User):
CREATE UNIQUE NONCLUSTERED INDEX [IX_UserProfiles_UserId] ON [dbo].[UserProfiles]([UserId] ASC);
Посмотрим, как работать с моделями с такой связью. Добавление:
using (ApplicationContext db = new ApplicationContext()) { // пересоздадим базу данных db.Database.EnsureDeleted(); db.Database.EnsureCreated(); User user1 = new User { Login = "login1", Password = "pass1234" }; User user2 = new User { Login = "login2", Password = "5678word2" }; db.Users.AddRange(user1, user2); UserProfile profile1 = new UserProfile { Age = 22, Name = "Tom", User = user1 }; UserProfile profile2 = new UserProfile { Age = 27, Name = "Alice", User = user2 }; db.UserProfiles.AddRange(profile1, profile2); db.SaveChanges(); }
Получение данных:
using (ApplicationContext db = new ApplicationContext()) { foreach (User user in db.Users.Include(u=>u.Profile).ToList()) { Console.WriteLine($"Name: {user.Profile?.Name} Age: {user.Profile?.Age}"); Console.WriteLine($"Login: { user.Login} Password: { user.Password} \n"); } }
Редактирование:
using (ApplicationContext db = new ApplicationContext()) { User user = db.Users.FirstOrDefault(); // получаем первый объект User if (user != null) { user.Password = "dsfvbggg"; db.SaveChanges(); } // получаем объект UserProfile для пользователя с логином "login2" UserProfile profile = db.UserProfiles.FirstOrDefault(p => p.User.Login == "login2"); if (profile != null) { profile.Name = "Alice II"; db.SaveChanges(); } }
При удалении надо учитывать следующее: так как объект UserProfile требует наличие объекта User и зависит от этого объекта, то при удалении связанного объекта User также будет удален и связанный с ним объект UserProfile. Если же будет удален объект UserProfile, на объект User это никак не повлияет:
using (ApplicationContext db = new ApplicationContext()) { // удаляем первый объект User User user = db.Users.FirstOrDefault(); if (user != null) { db.Users.Remove(user); db.SaveChanges(); } // удаляем объект UserProfile c логином login2 UserProfile profile = db.UserProfiles.FirstOrDefault(p => p.User.Login == "login2"); if (profile != null) { db.UserProfiles.Remove(profile); db.SaveChanges(); } }
Для настройки подобного отношения с помощью Fluent API применяются методы HasOne() и WithOne():
public class User { public int Id { get; set; } public string Login { get; set; } public string Password { get; set; } public UserProfile Profile { get; set; } } public class UserProfile { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } public int UserKey { get; set; } public User User { get; set; } } public class ApplicationContext : DbContext { public DbSet<User> Users { get; set; } public DbSet<UserProfile> UserProfiles { 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(u => u.Profile) .WithOne(p => p.User) .HasForeignKey<UserProfile>(p => p.UserKey); } }
В Entity Framework Core можно хранить данные моделей, которые связаны отношением один-к-одному, в одной таблице. Например, возьмем те же модели User и UserProfile и определим для них одну таблицу Users:
public class ApplicationContext : DbContext { public DbSet<User> Users { get; set; } public DbSet<UserProfile> UserProfiles { get; set; } 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(u => u.Profile).WithOne(p => p.User) .HasForeignKey<UserProfile>(up => up.Id); modelBuilder.Entity<User>().ToTable("Users"); modelBuilder.Entity<UserProfile>().ToTable("Users"); } }
В этом случае будет создаваться одна таблица Users:
CREATE TABLE [dbo].[Users] ( [Id] INT IDENTITY (1, 1) NOT NULL, [Login] NVARCHAR (MAX) NULL, [Password] NVARCHAR (MAX) NULL, [Name] NVARCHAR (MAX) NULL, [Age] INT NULL, [UserId] INT NULL, CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED ([Id] ASC) );
Например, добавление и получение обеих моделей:
using (ApplicationContext db = new ApplicationContext()) { // пересоздадим базу данных db.Database.EnsureDeleted(); db.Database.EnsureCreated(); User user1 = new User { Login = "login1", Password = "pass1234" }; User user2 = new User { Login = "login2", Password = "5678word2" }; db.Users.AddRange(user1, user2); UserProfile profile1 = new UserProfile { Age = 22, Name = "Tom", User = user1 }; UserProfile profile2 = new UserProfile { Age = 27, Name = "Alice", User = user2 }; db.UserProfiles.AddRange(profile1, profile2); db.SaveChanges(); } using (ApplicationContext db = new ApplicationContext()) { // получим данные foreach (var u in db.Users.Include(u => u.Profile).ToList()) { Console.WriteLine($"Name: {u.Profile?.Name} Age: {u.Profile?.Age}"); Console.WriteLine($"Login: { u.Login} Password: { u.Password} \n"); } }
Однако несмотря на то, что данные хранятся в одной таблице, мы по прежнему с ними можем работать по отдельности через db.UserProfiles и db.Users.