Комплексные типы

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

Начиная с версии EF Core 8 в Entity Framework была добавленна концепция комплексных типов. Комплексные типы характеризуются следующими признаками:

  • Они не имеют свойства-ключа, который бы их однозначно идентифицировал

  • Они существуют только как часть другой сущности и для них не определяется в контексте отдельное свойство DbSet

  • Они могут представлять как значимый, так и ссылочный тип

  • На один объект комплексного типа могут указывать свойства нескольких объектов

Рассмотрим ситуацию. У нас есть программисты, которые используют определенный язык программирования. И также есть компании, которые тоже используют определенный язык программирования. Под эту схему определим следующие классы сущностей и контекст данных:

using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
 
public class User
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public required Language Language { get; set; }
}
public class Company
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public required Language Language { get; set; }
}
[ComplexType]
public class Language
{
    public required string Name { get; set; }
}
public class ApplicationContext : DbContext
{
    public DbSet<Company> Companies { get; set; } = null!;
    public DbSet<User> Users { get; set; } = null!;
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite("Data Source=helloapp.db");
    }
}

В данном случае язык программирования определен в виде класса Language, который может состоять из различного набора свойств (в данном случае только название языка). Но который в данном случае определен именно как комплексный тип. Для этого применяется атрибут [ComplexType]. А классы User и Company, которые представляют соответственно программиста и компанию, определяют свойство Language для хранения используемого языка программирования. Причем класс Language не имеет ключа и как отдельная сущность в базе данных храниться не будет. Допустим, нам это не надо, нам надо только отразить, что определенные программисты и компании использует некоторый язык программирования.

В итоге в базе данных SQLite будут создаваться следующие таблицы:

CREATE TABLE "Users" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY AUTOINCREMENT,
    "Name" TEXT NOT NULL,
    "Language_Name" TEXT NOT NULL
)
CREATE TABLE "Companies" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Companies" PRIMARY KEY AUTOINCREMENT,
    "Name" TEXT NOT NULL,
    "Language_Name" TEXT NOT NULL
)

Комплесная сущность в подобных таблицах представленна в виде столбцов, которые называются по шаблону Класс_Свойство, например, "Language_Name".

Для теста определим следующий код:

// добавление данных
using (ApplicationContext db = new ApplicationContext())
{
    db.Database.EnsureDeleted();
    db.Database.EnsureCreated();

    Language csharp = new Language{Name ="C#"};
    User tom = new User
    {
        Name = "Tom",
        Language = csharp
    };
    User bob = new User
    {
        Name = "Bob",
        Language = tom.Language // использует язык Тома
    };
    Company microsoft = new Company 
    {
        Name = "Microsoft",
        Language = csharp
    };
    // добавление данных
    db.Users.AddRange(tom, bob);
    db.Companies.Add(microsoft);
    db.SaveChanges();

    // получаем данные
    var users = db.Users.ToList();
    foreach (User u in users)
        Console.WriteLine($"User: {u.Name}  Language: {u.Language.Name}");
    var companies = db.Companies.ToList();
    foreach (Company comp in companies)
        Console.WriteLine($"Company: {comp.Name}  Language: {comp.Language.Name}");

}

В данном случае определяем объект Language:

Language csharp = new Language{Name ="C#"};

Объект tom устанавливает этот объект в качестве значения Language:

User tom = new User {Name = "Tom", Language = csharp};

Затем создается объект bob, который использует язык программирования объекта tom (по сути тот же объект csharp).

User bob = new User{ Name = "Bob", Language = tom.Language // использует язык Тома};

И далее создаем компанию microsoft, которая также использует язык csharp:

Company microsoft = new Company { Name = "Microsoft", Language = csharp };

Причем здесь важно подчеркнуть разделяемость - один объект комплексного типа может использоваться разными объектами. В этом комплексные типы отличаются от собственных типов, которые были рассмотрены в прошлой статье и которые определялись либо с помощью атрибута [Owned], либо с помощью метода OwnsOne().

Консольный вывод программы:

User: Tom  Language: C#
User: Bob  Language: C#
Company: Microsoft  Language: C#

Причем поскольку все три объекта разделяют одну и ту же сущность Language, то при изменении значения в одном объекте затронут и другие объекты:

// добавление данных
using (ApplicationContext db = new ApplicationContext())
{
    db.Database.EnsureDeleted();
    db.Database.EnsureCreated();

    Language csharp = new Language{Name ="C#"};
    User tom = new User
    {
        Name = "Tom",
        Language = csharp
    };
    User bob = new User
    {
        Name = "Bob",
        Language = tom.Language // использует язык Тома
    };
    Company microsoft = new Company 
    {
        Name = "Microsoft",
        Language = csharp
    };
    // добавление данных
    db.Users.AddRange(tom, bob);
    db.Companies.Add(microsoft);
    db.SaveChanges();

    var firstUser = db.Users.FirstOrDefault();
    // меняем имя языка у одного объекта User
    if(firstUser != null) firstUser.Language.Name = "Шарп";
    // можно и так поменять
    // csharp.Name = "Шарп";

    // получаем данные
    var users = db.Users.ToList();
    foreach (User u in users)
        Console.WriteLine($"User: {u.Name}  Language: {u.Language.Name}");
    var companies = db.Companies.ToList();
    foreach (Company comp in companies)
        Console.WriteLine($"Company: {comp.Name}  Language: {comp.Language.Name}");

}

В данном случае меняем названия языка у одного объекта User, это приведет к тому, что изменится значение для всех объектов. Консольный вывод:

User: Tom  Language: Шарп
User: Bob  Language: Шарп
Company: Microsoft  Language: Шарп

Однако свойства будт изменяться, если создание и изменение отслеживаются одним объектом контекста. Например, в следующей ситуации изменение комплесного типа затронет только один объект User:

using (ApplicationContext db = new ApplicationContext())
{
    db.Database.EnsureDeleted();
    db.Database.EnsureCreated();

    Language csharp = new Language{Name ="C#"};
    User tom = new User
    {
        Name = "Tom",
        Language = csharp
    };
    User bob = new User
    {
        Name = "Bob",
        Language = tom.Language // использует язык Тома
    };
    Company microsoft = new Company 
    {
        Name = "Microsoft",
        Language = csharp
    };
    // добавление данных
    db.Users.AddRange(tom, bob);
    db.Companies.Add(microsoft);
    db.SaveChanges();
}
using (ApplicationContext db = new ApplicationContext())
{
    var firstUser = db.Users.FirstOrDefault();
    if(firstUser != null) firstUser.Language.Name = "Шарп";

    // получаем данные
    var users = db.Users.ToList();
    foreach (User u in users)
        Console.WriteLine($"User: {u.Name}  Language: {u.Language.Name}");
    var companies = db.Companies.ToList();
    foreach (Company comp in companies)
        Console.WriteLine($"Company: {comp.Name}  Language: {comp.Language.Name}");

}

Консольный вывод:

User: Tom  Language: Шарп
User: Bob  Language: C#
Company: Microsoft  Language: C#

При этом свойства комплесных типов также можно использовать в методах LINQ, например, для фильтрации:

using (ApplicationContext db = new ApplicationContext())
{
    db.Database.EnsureDeleted();
    db.Database.EnsureCreated();

    Language csharp = new Language{Name ="C#"};
    Language java = new Language{Name ="Java"};
    User tom = new User { Name = "Tom", Language = csharp };
    User bob = new User { Name = "Bob", Language = java };
    User sam = new User { Name = "Sam", Language = csharp }; 
    db.Users.AddRange(tom, bob, sam); 
    db.SaveChanges();

    // получаем разработчиков, у которых язык C#
    var users = db.Users.Where(u=>u.Language.Name == "C#").ToList();
    foreach (User u in users)
        Console.WriteLine(u.Name);

}

Fluent API

Также для настройки связи можно использовать Fluent API, в частности, метод ComplexProperty():

using Microsoft.EntityFrameworkCore;
 
public class User
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public required Language Language { get; set; }
}
public class Company
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public required Language Language { get; set; }
}
public class Language
{
    public required string Name { get; set; }
}
public class ApplicationContext : DbContext
{
    public DbSet<Company> Companies { get; set; } = null!;
    public DbSet<User> Users { get; set; } = null!;
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite("Data Source=helloapp.db");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>(
            builder =>
            {
                builder.ComplexProperty(user => user.Language);
            }
        );
        modelBuilder.Entity<Company>(
            builder =>
            {
                builder.ComplexProperty(comp => comp.Language);
            }
        );
    }
}

В методе ComplexProperty() указывается навигационное свойство, которое представляет комплексный тип.

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