Отношение один к одному предполагает, что главная сущность может ссылаться только на один объект зависимой сущности. В свою очередь, зависимая сущность может ссылаться только на один объект главной сущности.
Рассмотрим стандартный пример подобных отношений: есть класс пользователя User, который хранит логин и пароль, то есть данные учетных записей. А все данные профиля, такие как имя, возраст и так далее, выделяются в класс профиля UserProfile.
using Microsoft.EntityFrameworkCore; 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; } = null!; public DbSet<UserProfile> UserProfiles { get; set; } = null!; protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlite("Data Source=helloapp.db"); } }
В этой связи между классами сущность UserProfile является зависимой по отношению к сущности User.
И чтобы установить связь один к одному, у зависимой сущности устанавливается свойство внешний ключ: public int UserId { get; set; }
.
Благодаря этому Entity Framework узнает, что UserProfile является зависимой сущностью. К примеру, в классе User также есть навигационное свойство - ссылка на объект
UserProfile, но при этом внешний ключ отсутствует.
В итоге для сущности UserProfile в случае с SQLite будет создана следующая таблица в базе данных:
CREATE TABLE "UserProfiles" ( "Id" INTEGER NOT NULL, "Name" TEXT, "Age" INTEGER NOT NULL, "UserId" INTEGER NOT NULL, CONSTRAINT "PK_UserProfiles" PRIMARY KEY("Id" AUTOINCREMENT), CONSTRAINT "FK_UserProfiles_Users_UserId" FOREIGN KEY("UserId") REFERENCES "Users"("Id") ON DELETE CASCADE );
Внешне эта таблица не отличается от таблицы, которая создается для зависимой сущности при связи один-ко-многим. Однако также стоит добавить, что для этой таблицы для столбца, который представляет внешний ключ (в данном случае UserId), создается уникальный индекс. И этот индекс гарантирует, что только одна зависимая сущность (здесь UserProfile) может быть связана с одной главной сущностью (здесь сущность User):
CREATE UNIQUE INDEX "IX_UserProfiles_UserId" ON "UserProfiles" ("UserId")
Посмотрим, как работать с моделями с такой связью. Добавление:
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():
using Microsoft.EntityFrameworkCore; 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; } = null!; public DbSet<UserProfile> UserProfiles { get; set; } = null!; protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlite("Data Source=helloapp.db"); } 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:
using Microsoft.EntityFrameworkCore; 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 User? User { get; set; } } public class ApplicationContext : DbContext { public DbSet<User> Users { get; set; } = null!; public DbSet<UserProfile> UserProfiles { get; set; } = null!; protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlite("Data Source=helloapp.db"); } 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"); } }
В этом случае в БД SQLite будет создаваться одна таблица Users:
CREATE TABLE "Users" ( "Id" INTEGER NOT NULL, "Login" TEXT, "Password" TEXT, "Name" TEXT, "Age" INTEGER, CONSTRAINT "PK_Users" PRIMARY KEY("Id" AUTOINCREMENT) );
Например, добавление и получение обеих моделей:
using Microsoft.EntityFrameworkCore; 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.