Начиная с версии 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, в частности, метод 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()
указывается навигационное свойство, которое представляет комплексный тип.