Данное руководство устарело. Актуальное руководство: Руководство по Entity Framework Core 7
Как правило, каждый тип сопоставляется с отдельной таблицей. Однако бывают случаи, когда определенный класс существует не сам по себе, а несет некоторую дополнительную информацию по отношению к другой главной модели. Данная функциональность предполагает, что у нас есть несколько типов, но один из них является владельцем (owner). Другие же зависимые или собственные типы (owned entity types) являются частью типа-владельца и без него существовать не могут.
Для создания связки "главный тип - зависимые типы" можно использовать либо атрибут OwnedAttribute, либо настройки Fluent API.
Атрибут OwnedAttribute позволяет установить зависимый тип. Например:
using Microsoft.EntityFrameworkCore; namespace HelloApp { public class User { public int Id { get; set; } public string Login { get; set; } public string Password { get; set; } public UserProfile Profile { get; set; } } [Owned] public class UserProfile { public string Name { get; set; } public int Age { get; set; } } public class ApplicationContext : DbContext { public DbSet<User> Users { get; set; } public ApplicationContext() { Database.EnsureDeleted(); Database.EnsureCreated(); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=relationsdb;Trusted_Connection=True;"); } } }
В данном случае есть тип-владелец User и есть владеемый им тип UserProfile. Класс User определяет основные настройки учетной записи - логин и пароль, а UserProfile - всторостепенные данные - имя, возраст и т.д. Чтобы указать, что User владеет типом UserProfile, над UserProfile указывается атрибут OwnedAttribute. Кроме того, UserProfile не содержит ключа.
В итоге при создании базы данных данные обоих типов будут содержаться в одной таблице:
CREATE TABLE [dbo].[Users] ( [Id] INT IDENTITY (1, 1) NOT NULL, [Login] NVARCHAR (MAX) NULL, [Password] NVARCHAR (MAX) NULL, [Profile_Name] NVARCHAR (MAX) NULL, [Profile_Age] INT NULL, CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED ([Id] ASC) );
Использование типов:
public static void Main(string[] args) { // добавление данных using (ApplicationContext db = new ApplicationContext()) { User user1 = new User { Login = "login1", Password = "pass1234", Profile = new UserProfile { Age = 23, Name = "Tom" } }; User user2 = new User { Login = "login2", Password = "5678word2", Profile = new UserProfile { Age = 27, Name = "Alice" } }; db.Users.AddRange(user1, user2); db.SaveChanges(); // получение данных var users = db.Users.ToList(); foreach (User u in users) Console.WriteLine($"Name: {u.Profile?.Name} Age: {u.Profile?.Age} Login: { u.Login} Password: { u.Password} "); } }
Также для настройки связи можно использовать Fluent API, в частности, метод OwnsOne():
using Microsoft.EntityFrameworkCore; namespace HelloApp { 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 string Name { get; set; } public int Age { get; set; } } public class ApplicationContext : DbContext { public DbSet<User> Users { 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>().OwnsOne(u => u.Profile); } } }
В методе OwnsOne()
указывается навигационное свойство, которое представляет зависимый тип.
Вполне возможно, что мы захотим сделать навигационное свойство Profile приватным. В этом случае понадобится дополнительная настройка:
using Microsoft.EntityFrameworkCore; namespace HelloApp { public class User { public User() { } public User(string login, string password, UserProfile profile) { Login = login; Password = password; Profile = profile; } public int Id { get; set; } public string Login { get; set; } public string Password { get; set; } private UserProfile Profile { get; set; } public override string ToString() { return $"Name: {Profile?.Name} Age: {Profile?.Age} Login: {Login} Password: {Password}"; } } public class UserProfile { public string Name { get; set; } public int Age { get; set; } } public class ApplicationContext : DbContext { public DbSet<User> Users { 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>().OwnsOne(typeof(UserProfile), "Profile"); } } }
В этом случае в метод OwnsOne
передается тип навигационного свойства и само имя навигационного свойства. Применение:
using (ApplicationContext db = new ApplicationContext()) { User user1 = new User("login1", "pass1234", new UserProfile { Age = 23, Name = "Tom" }); User user2 = new User("login2","5678word2", new UserProfile { Age = 27, Name = "Alice" }); db.Users.AddRange(user1, user2); db.SaveChanges(); var users = db.Users.ToList(); foreach (User user in users) Console.WriteLine(user.ToString()); }
Одни собственные типы могут иметь другие собственные типы, уввеличия сложность структуры классов. Например, определим следующие классы:
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 Claim Name { get; set; } public Claim Age { get; set; } } public class Claim { public string Key { get; set; } public string Value { get; set; } }
Главной сущностью здесь является класс User, который содержит объект класса UserProfile. Класс UserProfile, в свою очередь, также содержит объекты еще одного класса - Claim, который представляет отдельные данные по принципу ключ-значение.
Теперь определим контекст данных:
public class ApplicationContext : DbContext { public DbSet<User> Users { 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>().OwnsOne(u => u.Profile, p => { p.OwnsOne(c => c.Name); p.OwnsOne(c => c.Age); }); } }
С помощью метода OwnsOne() устанавливается включение через свойства собственных типов. И после создания базы данных мы получим следующую таблицу User, которая будет содержать данные, представленные собственными типами.
CREATE TABLE [dbo].[Users] ( [Id] INT IDENTITY (1, 1) NOT NULL, [Login] NVARCHAR (MAX) NULL, [Password] NVARCHAR (MAX) NULL, [Profile_Name_Key] NVARCHAR (MAX) NULL, [Profile_Name_Value] NVARCHAR (MAX) NULL, [Profile_Age_Key] NVARCHAR (MAX) NULL, [Profile_Age_Value] NVARCHAR (MAX) NULL, CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED ([Id] ASC) );
Для работы с собственными типами определим следующий код:
using (ApplicationContext db = new ApplicationContext()) { User user1 = new User { Login = "login1", Password = "pass1234", Profile = new UserProfile { Age =new Claim { Key = "Age", Value = "23" }, Name = new Claim { Key="Name", Value="Tom"} } }; User user2 = new User { Login = "login2", Password = "5678word2", Profile = new UserProfile { Age = new Claim { Key = "Age", Value = "27" }, Name = new Claim { Key = "Name", Value = "Alice" } } }; db.Users.AddRange(user1, user2); db.SaveChanges(); var users = db.Users.ToList(); foreach (User user in users) Console.WriteLine($"Name: {user.Profile?.Name?.Value} Age: {user.Profile?.Age?.Value} Login: {user.Login} Password: {user.Password}"); }