Отношение один к одному

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

Отношение один к одному предполагает, что главная сущность может ссылаться только на один объект зависимой сущности. В свою очередь, зависимая сущность может ссылаться только на один объект главной сущности.

Рассмотрим стандартный пример подобных отношений: есть класс пользователя 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

Для настройки подобного отношения с помощью 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.

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