Настройка привязки модели к базе данных
В предыдущих примерах, когда мы каждый раз добавляли новый класс модели, мы добавляли ссылку на него в виде свойства в классе контекста, унаследованного от DbContext. Это свойство имело тип DbSet, типизированный классом модели данных. DbSet выполняет две функции. Во-первых при вызове свойства контекста данных он возвращает коллекцию объектов, полученных из базы данных. Во-вторых он указывает классу DbModelBuilder, выполняющему привязку модели к базе данных, что этот класс модели должен отображаться на таблицу базы данных.
Использование DbSet является не единственным способом, с помощью которого связывателю модели можно указать на сущностный класс. Вообще существует три разных способа:
- Использовать типизированное свойство DbSet в классе контекста.
- Можно указать ссылку через навигационное свойство в классе модели на другой класс, который должен отобразиться на таблицу, при этом первый класс уже должен быть указан с помощью DbSet в контексте данных. Например, для нашей модели, когда мы использовали взаимосвязанные классы Customer и Order в классе контекста достаточно оставить ссылку на Customer, чтобы тип Order был автоматически учтен связывателем модели.
- Можно использовать настройки Fluent API.
Допустим у нас имеется следующая простая модель:
public class Customer < public int CustomerId < get; set; >public string FirstName < get; set; >// Ссылка на заказы public virtual List Orders < get; set; >> public class Order < public int OrderId < get; set; >public string ProductName < get; set; >// Ссылка на покупателя public Customer Customer < get; set; >>
Если вы использовали эту модель ранее, удалите ссылки на сущностные объекты DbSet в классе контекста. Чтобы указать связывателю модели, что класс Customer должен отображаться на таблицу в базе данных, можно использовать свойство Configurations объекта DbModelBuilder, как показано в примере ниже:
protected override void OnModelCreating(DbModelBuilder modelBuilder) < modelBuilder.Configurations.Add( new EntityTypeConfiguration()); >
Методу Add() передается объект EntityTypeConfiguration, типизированный нужным классом модели. Если вы помните, при рассмотрении подхода Code-First мы использовали унаследованные классы конфигурации от EntityTypeConfiguration. Экземпляры этих классов также можно передавать методу Add() объекта конфигурации.
Установка классов и свойств модели без привязки к таблице
Ваша модель данных может содержать классы, которые не требуется отражать в виде таблиц в базе данных. Даже если вы явно не укажите эти классы в DbSet класса контекста, все равно возможны ситуации, когда Code-First попытается создать из них таблицы. Например, как было сказано выше указывать класс Order в DbSet не обязательно, т.к. класс Customer автоматически ссылается на него через навигационное свойство.
Что делать, если нам нужно указать Code-First, что класс Order не должен отображаться в базе данных? Для этих целей в аннотациях данных используется специальный атрибут NotMapped, который применяется к классу модели. В Fluent API это реализовано с помощью метода Ignore() класса DbModelBuilder. В примере ниже показано, как использовать эти средства:
[NotMapped] public class Order < // . >// то же самое с помощью Fluent API protected override void OnModelCreating(DbModelBuilder modelBuilder) < modelBuilder.Ignore(); >
Если вы запустите этот пример, то в базе данных будет создана только таблица Customer. Свойство Customer.Orders фактически больше не является навигационным, т.к. класс Order больше не является объектом базы данных.
Entity Framework также позволяет не отображать конкретные свойства класса модели – зачастую, в классах требуется использовать дополнительные свойства, которые не должны быть отражены на столбцы таблицы базы данных. Для этого используются также, атрибут NotMapped и метод Ignore(), только они указываются на уровне свойства, а не на уровне класса:
public class Customer < public int CustomerId < get; set; >public string FirstName < get; set; >[NotMapped] public bool HelperProperty < get; set; >// . > // то же самое с помощью Fluent API protected override void OnModelCreating(DbModelBuilder modelBuilder) < modelBuilder.Entity().Ignore(c => c.HelperProperty); >
Привязка свойств и их доступность
В Code-First есть ряд соглашений, которые определяют как должны привязываться различные свойства модели к столбцам таблицы. Ранее мы подробно на этом моменте не останавливались, так что давайте более подробно рассмотрим эти соглашения. В разделах ниже приводится список правил по отображению свойств модели и описываются способы их конфигурирования.
Скалярные свойства
Скалярные свойства будут отображаться на столбцы только в том случае, если их тип поддерживается в модели EDM (Entity Data Model), иначе говоря, если этот тип можно будет преобразовать в SQL-совместимый тип данных. Допустимыми типами EDM являются: Binary, Boolean, Byte, DateTime, DateTimeOffset, Time, Decimal, Double, Guid, Int16, Int32, Int64, SByte, Single и String. Скалярные свойства, которые не могут быть отображены на тип EDM игнорируются (например, перечисления и целые числа без знака – ulong, long и т.д.)
Модификаторы доступа и поддержка чтения/записи для свойств
Напомню, в C# поддержка к записи для свойств определяется оператором set, к чтению – оператором get. Ранее в каждой модели данных мы использовали автоматически определяемые свойства. Также, каждое свойство помечается модификатором доступа: public, protected, internal или private. Code-First использует следующие соглашения в плане доступности свойств и поддержки чтения/записи:
- Все открытые свойства (модификатор public) будут автоматически отображены на столбец таблицы в базе данных.
- Доступ к записи для свойства можно указать явно, используя оператор set, но при этом доступ к чтению свойства должен быть открытым, чтобы Code-First автоматически отобразил его в таблице.
- Закрытые свойства (помеченные модификаторами private, internal или protected) автоматически не отображаются на таблицу, но это поведение можно изменить используя Fluent API.
Для конфигурации закрытых свойств вы должны быть в состоянии получить доступ к свойству в том месте, где вы выполняете конфигурацию. Например, если у вас есть класс Customer со свойством LastName, помеченным модификатором internal и он находится в той же сборке что и класс контекста данных, у вас будет доступ к этому свойству из метода OnModelCreating():
protected override void OnModelCreating(DbModelBuilder modelBuilder) < modelBuilder.Entity().Property(c => c.LastName); >
Однако, если классы Customer и SampleContext (наш класс контекста из примеров) находятся в разных сборках, настраивать закрытое свойство таким образом нельзя. Эту проблему можно решить, если вы определите класс конфигурации (унаследованный от EntityTypeConfiguration) в той же сборке, где находится класс Customer, добавите ссылку на эту сборку в сборке, где находится класс контекста и в методе OnModelCreating() укажите объекту DbModelBuilder ссылку на эту конфигурацию.
Таким же способом можно определить закрытые и защищенные свойства (модификаторы private и protected). Важно запомнить, чтобы этот подход работал, класс конфигурации должен быть вложен в класс модели, к которому он применяется. Ниже показан пример для класса Customer, в котором мы определяем закрытое свойство LastName и класс конфигурации, вложенный в класс Customer:
public class Customer < public int CustomerId < get; set; >public string FirstName < get; set; >private string LastName < get; set; >public class CustomerConfig : EntityTypeConfiguration < public CustomerConfig() < this.Property(c =>c.LastName); > > >
Обратите внимание, для того чтобы указать Code-First, что свойство является частью таблицы, достаточно просто вызвать метод Property() в объекте конфигурации без использования дополнительных методов.
Теперь сослаться на эту конфигурации можно в классе контекста, используя Fluent API:
// Следующий метод указывается в классе контекста protected override void OnModelCreating(DbModelBuilder modelBuilder)
Наследование в классах модели
Entity Framework поддерживает различные иерархии наследования в модели. Ранее, при обсуждении возможностей Entity Framework мы не обращали внимание на возможность наследования классов модели, поэтому у вас может возникнуть вопрос о том, как Code-First работает с такими моделями.
Отображение Table Per Hierarchy (TPH)
используется в Code-First по умолчанию и означает, что иерархия унаследованных классов отображается на одну таблицу в базе данных. Чтобы увидеть это отображение в действие, давайте изменим модель и укажем класс User, унаследованный от Customer, добавляющий два новых свойства:
public class Customer < public int CustomerId < get; set; >public string FirstName < get; set; >public string LastName < get; set; >> public class User : Customer < public int Age < get; set; >[Column(TypeName="image")] public byte[] Photo < get; set; >>
Для такой модели Entity Framework сгенерирует одну таблицу Customers, имеющую следующую структуру:
Теперь данные из этих двух классов хранятся в одной общей таблице. Обратите внимание, что Code-First создаст дополнительный столбец Discriminator имеющий тип NVARCHAR(128). По умолчанию, Code-First будет использовать имя класса каждого типа в иерархии в качестве значения, хранящегося в столбце дискриминатора. Например, если вы попытаетесь выполнить следующий код, в котором в таблицу вставляются данные заказчика с помощью класса Customer, столбец Discriminator будет хранить имя “Customer”:
protected void Page_Load(object sender, EventArgs e) < Customer customer = new Customer < FirstName = "Вася", LastName = "Пупкин" >; SampleContext context = new SampleContext(); context.Customers.Add(customer); context.SaveChanges(); >
Если вы используете для вставки данных класс User, как показано в примере ниже, то Code-First вставит в столбец Discriminator значение “User”:
protected void Page_Load(object sender, EventArgs e) < User user = new User < FirstName = "Вася", LastName = "Пупкин", Age = 20, Photo = new byte[100] >; SampleContext context = new SampleContext(); context.Customers.Add(user); context.SaveChanges(); >
Фактически, с помощью этого столбца Entity Framework отслеживает, какой класс в иерархии наследования использовался для изменения/вставки данных в таблицу.
У вас есть возможность настроить имя и тип специального столбца Discriminator, а также возможные значения, которые будут использоваться для разграничения типов. Эти настройки не поддерживаются в аннотациях данных (т.к. столбец Discriminator генерируется автоматически), поэтому нужно использовать Fluent API:
protected override void OnModelCreating(DbModelBuilder modelBuilder) < modelBuilder.Entity() .Map(m => < m.Requires("CustomerType").HasValue("Добавлено из Customer"); >) .Map(m => < m.Requires("CustomerType").HasValue("Добавлено из User"); >); >
В этой настройки для указания имени столбца дискриминатора используется метод Requires() объекта конфигурации EntityMappingConfiguration. Мы явно указали имя CustomerType для столбца дискриминатора. С помощью метода HasValue() можно установить значение, которое будет генерироваться для каждого класса. Теперь, если вы вставите запись в таблицу Customers с помощью класса User, столбец CustomerType будет хранить значение “Добавлено из User”.
Можно также изменить тип столбца дискриминатора. Например, если вам известно, что в иерархии классов модели используются всего два класса (как, в нашем примере), то можно определить столбец IsUser типа bool (в SQL тип BIT), который будет указывать, добавлена ли строка с помощью Customer, либо с помощью User:
protected override void OnModelCreating(DbModelBuilder modelBuilder) < modelBuilder.Entity() .Map(m => < m.Requires("IsUser").HasValue(false); >) .Map(m => < m.Requires("IsUser").HasValue(true); >); >
Структура таблицы Customers с использованием нового столбца IsUser показана на рисунке ниже:
Отображение Table Per Type (TPT)
В то время, как отображение TPH хранит одну таблицу для иерархии наследованных типов, позволяет создать таблицу для каждого типа, при этом в таблицах, отображающих наследуемые классы хранится внешний ключ, ссылающийся на их родительский класс (таблицу). Если ваша схема базы данных должна использовать отдельные таблицы для иерархии классов, вы должны явно сконфигурировать производные. Для этого вам нужно просто указать имя таблицы для производного типа. Вы можете сделать это с помощью аннотаций данных или Fluent API:
[Table("User")] public class User : Customer < // . >// то же самое с помощью Fluent API protected override void OnModelCreating(DbModelBuilder modelBuilder) < modelBuilder.Entity() .Map(m => < m.ToTable("User"); >); >
В результате запуска этого примера, Code-First создаст две таблицы: Customers и User. На рисунке ниже показана их структура:
В таблице User есть столбец CustomerId, который является первичным ключом этой таблицы и внешним ключом, ссылающимся на таблицу Customers, т.е. между таблицами было создано отношение ноль-или-один-к-одному. Если вам интересно, EF не включил каскадное удаление для этого внешнего ключа, при удалении записи из таблицы Customers, Code-First автоматически позаботится об удалении связанной записи из таблицы User, если она существует.
Когда вы добавляете новую запись с помощью класса User, Code-First сначала создаст новую запись в таблице Customers, а затем вставит новую запись в таблицу User, ссылающуюся на созданную запись в таблице Customers.
Отображение Table Per Concrete Type (TPC)
похоже на отображение TPT тем, что при таком отображении для каждого класса в иерархии наследования создается таблица. Но при этом таблицы, созданные на основе этой иерархии никак не связаны, и в каждой таблице отображается весь набор свойств из родительских классов. Такое отображение бывает полезно когда вы хотите расширить старую таблицу при этом не изменяя ее и не удаляя, а просто создав новую таблицу со старыми и новыми столбцами.
Вы можете настроить отображение TPC с использованием Fluent API (аннотации данных для этого отображения не поддерживаются). Давайте изменим отображение для наших классов Customer и User. Для настройки TPC используется вспомогательный метод MapInheritedProperties(), который доступен только на объекте EntityMappingConfiguration, т.е. в вызове метода Map(). Ниже показан соответствующий пример:
protected override void OnModelCreating(DbModelBuilder modelBuilder) < modelBuilder.Entity() .ToTable("Customer") .Map(m => < m.ToTable("User"); m.MapInheritedProperties(); >); >
Обратите внимание, что вы должны явно включить отображение имени таблицы с помощью метода ToTable() для базовой таблицы. С отображением TPT это не требуется, но с TPC это является обязательным. Метод MapInheritedProperties() указывает Code-First, что он должен отобразить все свойства, унаследованные от базового класса к новым колонкам в таблице, созданной на основе производного типа. Структура таблиц Customer и User выглядит следующим образом:
Абстрактные классы модели
Все создаваемые ранее классы модели не были абстрактными, это означает, что в коде мы могли создавать объекты этих классов. В C# поддерживается возможность создания абстрактных классов, экземпляры которых нельзя создавать в коде. Возникает вопрос, как работает Entity Framework с такими классами модели?
Давайте изменим нашу модель классов Customer-User, и укажем, что класс Customer должен являться абстрактным, а также добавим новый класс Client, который наследуется от Customer:
public abstract class Customer < public int CustomerId < get; set; >public string FirstName < get; set; >public string LastName < get; set; >> public class User : Customer < public int? Age < get; set; >[Column(TypeName="image")] public byte[] Photo < get; set; >> public class Client : Customer < public string AddressCity < get; set; >public string AddressStreet < get; set; >>
Теперь вы не можете создавать экземпляры класса Customer, поэтому удалите создание этих объектов, если вы использовали их ранее где-то в приложении. Также удалите настройки TPC в методе OnModelCreating(), в результате чего Code-First будет использовать по умолчанию подход для отображения TPH – все поля из производных классов будут содержаться в таблице Customers.
Если вы запустите этот пример, то увидите, что структура таблицы Customers идентична той, что у нас получалась при рассмотрении TPH, за тем лишь исключением, что были добавлены поля из класса Client.
Фактически поведение Code-First при использовании абстрактных классов идентично поведению для обычных классов, но при этом меняется способ взаимодействия с таблицей Customers. Для вставки данных в эту таблицу вы можете использовать объекты унаследованных классов Client и User. Если вы реализуете отображение TPC, очевидно, что при использовании абстрактного класса вы потеряете доступ к вставке/изменению данных в таблице Customers.
Ускоряемся в Entity Framework Core
При выборке данных выбирать нужно ровно столько сколько нужно за один раз. Никогда не извлекайте все данные из таблицы!
using var ctx = new EFCoreTestContext(optionsBuilder.Options); // Мы возвращаем колонку ID с сервера, но никогда не используем и это неправильно! ctx.FederalDistricts.Select(x=> new < x.ID, x.Name, x.ShortName >).ToList();
using var ctx = new EFCoreTestContext(optionsBuilder.Options); // Мы не возвращаем колонку ID с сервера и это правильно! ctx.FederalDistricts.Select(x=> new < x.Name, x.ShortName >).ToList(); ctx.FederalDistricts.Select(x => new MyClass < Name = x.Name, ShortName = x.ShortName >).ToList();
var blogs = context.Blog.ToList(); // Тут вы скопировали ВСЮ таблицу в память. Зачем? // Чтобы выбрать лишь некоторые записи? var somePost = blogs.FirstOrDefault(x=>x.Title.StartWidth(“Hello world!”));
var somePost = context.Blog.FirstOrDefault(x=>x.Title.StartWidth(“Hello world!”));
Встроенная проверка данных может быть выполнена, когда запрос вернул какие-то записи.
var blogs = context.Blogs.Where(blog => StandardizeUrl(blog.Url).Contains("dotnet")).ToList(); public static string StandardizeUrl(string url) < url = url.ToLower(); if (!url.StartsWith("http://")) < url = string.Concat("http://", url); >return url; >
var blogs = context.Blogs.AsEnumerable().Where(blog => StandardizeUrl(blog.Url).Contains("dotnet")).ToList(); //Еще правильней так var blogs = context.Blogs.Where(blog => blog.Contains("dotnet")) .OrderByDescending(blog => blog.Rating) .Select(blog => new < Url = StandardizeUrl(blog.Url) >) .ToList();
Вау, вау, вау, разогнался.
Самое время немного освежить знания по методам LINQ.
Давайте рассмотрим отличия между ToList AsEnumerable AsQueryable
Итак, ToList
- Выполняет запрос немедленно.
- Используйте .ToList() для форсирования получения данных и выхода из режима поздней загрузки (lazy loading), так что этот метод полезен перед тем как вы пройдетесь по данным.
- Выполнение с задержкой (lazy loading)
- Принимает параметр: Func
- Загружает каждую запись в память приложения и управляет фильтрует его (в том числе Where/Take/Skip приведут к тому, что, например запрос select * from Table1,
- загрузит результирующий набор в память, затем выберет первые N элементов)
- В этом случает отрабатывает схема: Linq-to-SQL + Linq-to-Object.
- Используйте IEnumerable для получения списка из базы данных в режиме поздней загрузки (lazy loading).
- Выполнение с задержкой (lazy loading)
- Может быть перезагружен:
AsQueryable(IEnumerable) или AsQueryable(IEnumerable)
Пример использования AsQueryable в простейшеем случае:
public IEnumerable GetEmails(out int totalRecords, Guid? deviceWorkGroupID, DateTime? timeStart, DateTime? timeEnd, string search, int? confirmStateID, int? stateTypeID, int? limitOffset, int? limitRowCount, string orderBy, bool desc) < var r = new List(); using (var db = new GJobEntities()) < var query = db.Emails.AsQueryable(); if (timeStart != null && timeEnd != null) < query = query.Where(p =>p.Created >= timeStart && p.Created if (stateTypeID != null && stateTypeID > -1) < query = query.Where(p =>p.EmailStates.OrderByDescending(x => x.AtTime).FirstOrDefault().EmailStateTypeID == stateTypeID); > if (confirmStateID != null && confirmStateID > -1) < var boolValue = confirmStateID == 1 ? true : false; query = query.Where(p =>p.IsConfirmed == boolValue); > if (!string.IsNullOrEmpty(search)) < search = search.ToLower(); query = query.Where(p =>(p.Subject + " " + p.CopiesEmails + " " + p.ToEmails + " " + p.FromEmail + " " + p.Body) .ToLower().Contains(search)); > if (deviceWorkGroupID != Guid.Empty) < query = query.Where(x =>x.SCEmails.FirstOrDefault().SupportCall.Device.DeviceWorkGroupDevices.FirstOrDefault(p => p.DeviceWorkGroupID == deviceWorkGroupID) != null); > totalRecords = query.Count(); query = query.OrderByDescending(p => p.Created); if (limitOffset.HasValue) < query = query.Skip(limitOffset.Value).Take(limitRowCount.Value); >var items = query.ToList(); // Получаем все отфильтрованные записи foreach (var item in items) < var n = new EmailView < SentTime = item.SentTime, IsConfirmed = item.IsConfirmed, Number = item.Number, Subject = item.Subject, IsDeleted = item.IsDeleted, ToEmails = item.ToEmails, Created = item.Created, CopiesEmails = item.CopiesEmails, FromEmail = item.FromEmail, >; // Другой код для заполнения класса-представления r.Add(n); > > return r; >
Волшебство простого чтения
Если вам не нужно менять данные, только отобразить используйте .AsNoTracking() метод.
Медленная выборка
var blogs = context.Blogs.ToList();
Быстрая выборка (только на чтение)
var blogs = context.Blogs.AsNoTracking().ToList();
Чувствую, вы немного уже размялись?
Типы загрузки связанных данных
Для тех, кто забыл, что такое lazy loading.
Ленивая загрузка (Lazy loading) означает, что связанные данные прозрачно загружаются из базы данных при обращении к свойству навигации. Подробнее читаем тут .
И заодно, напомню о других типах загрузки связанных данных.
Активная загрузка (Eager loading) означает, что связанные данные загружаются из базы данных как часть первоначального запроса.
using (var context = new BloggingContext()) < var blogs = context.Blogs .Include(blog =>blog.Posts) .ThenInclude(post => post.Author) .ThenInclude(author => author.Photo) .Include(blog => blog.Owner) .ThenInclude(owner => owner.Photo) .ToList(); >
Внимание! Начиная с версии EF Core 3.0.0, каждое Include будет вызывать добавление дополнительного JOIN к запросам SQL, создаваемым реляционными поставщиками, тогда как предыдущие версии генерировали дополнительные запросы SQL. Это может значительно изменить производительность ваших запросов, в лучшую или в худшую сторону. В частности, запросы LINQ с чрезвычайно большим числом операторов включения могут быть разбиты на несколько отдельных запросов LINQ.
Явная загрузка (Explicit loading) означает, что связанные данные явно загружаются из базы данных позднее.
using (var context = new BloggingContext()) < var blog = context.Blogs .Single(b =>b.BlogId == 1); var goodPosts = context.Entry(blog) .Collection(b => b.Posts) .Query() .Where(p => p.Rating > 3) .ToList(); >
Рывок и прорыв! Двигаемся дальше?
Готовы ускориться еще больше?
Чтобы резко ускориться при выборке сложно структурированных и даже ненормализованных данных из реляционной базы данных есть два способа сделать это: используйте индексированные представления (1) или что еще лучше – предварительно подготовленные(вычисленные) данные в простой плоской форме для отображения (2).
(1) Индексированное представление в контексте MS SQL Server
Индексированное представление имеет уникальный кластеризованный индекс. Уникальный кластерный индекс хранится в SQL Server и обновляется, как и любой другой кластерный индекс. Индексированное представление является более значительным по сравнению со стандартными представлениями, которые включают сложную обработку большого количества строк, например, агрегирование большого количества данных или объединение множества строк.
Если на такие представления часто ссылаются в запросах, мы можем повысить производительность, создав уникальный кластеризованный индекс для представления. Для стандартного представления набор результатов не сохраняется в базе данных, вместо этого набор результатов вычисляется для каждого запроса, но в случае кластеризованного индекса набор результатов сохраняется в базе данных точно так же, как таблица с кластеризованным индексом. Запросы, которые специально не используют индексированное представление, могут даже выиграть от существования кластеризованного индекса из представления.
Представление индекса имеет определенную стоимость в виде производительности. Если мы создаем индексированное представление, каждый раз, когда мы изменяем данные в базовых таблицах, SQL Server должен поддерживать не только записи индекса в этих таблицах, но также и записи индекса в представлении. В редакциях SQL Server для разработчиков и предприятий оптимизатор может использовать индексы представлений для оптимизации запросов, которые не указывают индексированное представление. Однако в других выпусках SQL Server запрос должен включать индексированное представление и указывать подсказку NOEXPAND, чтобы получить преимущество от индекса в представлении.
(2) Если нужно сделать запрос, требующий отображения более трех уровней связанных таблиц в количестве три и более c повышенной CRUD нагрузкой, лучшим способом будет задуматься о том, чтобы периодически вычислять результирующий набор, сохранять его в таблице и использовать для отображения. Результирующая таблица, в которой будут сохраняться данные должна иметь Primary Key и индексы по полям поиска в LINQ.
Что насчет асинхронности?
Да! Используем ее где только можно! Вот пример:
public void Do() < var myTask = GetFederalDistrictsAsync (); foreach (var item in myTask.Result) < //Ваш код >> public async Task> GetFederalDistrictsAsync()
И да, ничего не забыли для повышения производительности? Бууум!
return await context.FederalDistricts.AsNoTracking().ToListAsync();
Внимание: метод Do() добавлен для демонстрационных целей только, с целью указать работоспособность метода GetFederalDistrictsAsync(). Как правильно заметили мои коллеги тутнужен другой пример чистой асинхронности.
И давайте я его приведу на основе понятия компонента представления в ASP .NET Core:
// Класс компонента public class PopularPosts : ViewComponent < private readonly IStatsRepository _statsRepository; public PopularPosts(IStatsRepository statsRepository) < _statsRepository = statsRepository; >public async Task InvokeAsync() < // Вызов нашего метода без изменений из выделенного репозитория бизнес-логики var federalDistricts = await _statsRepository.GetFederalDistrictsAsync(); var model = new TablePageModel() < FederalDistricts = federalDistricts, >; return View(model); > > // Далее /// /// Интерфейс бизнес-логики для получения хммм. чего-либо /// public interface IStatsRepository < /// /// Получение списка федеральных округов и их субъектов федерации /// /// IEnumerable FederalDistricts(); /// /// Получение списка федеральных округов и их субъектов федерации /// Асинхронно. /// /// Task GetFederalDistrictsAsync(); > /// /// Бизнес-логика для получения хммм. чего-либо /// public class StatsRepository : IStatsRepository < private readonly DbContextOptionsBuilderoptionsBuilder = new DbContextOptionsBuilder(); private readonly IConfigurationRoot configurationRoot; public StatsRepository() < IConfigurationBuilder configurationBuilder = new ConfigurationBuilder() .SetBasePath(Environment.CurrentDirectory) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); configurationRoot = configurationBuilder.Build(); >public async Task GetFederalDistrictsAsync() < var conn = configurationRoot.GetConnectionString("EFCoreTestContext"); optionsBuilder.UseSqlServer(conn); using var context = new EFCoreTestContext(optionsBuilder.Options); return await context.FederalDistricts.Include(x =>x.FederalSubjects).ToListAsync(); > public IEnumerable FederalDistricts() < var conn = configurationRoot.GetConnectionString("EFCoreTestContext"); optionsBuilder.UseSqlServer(conn); using var ctx = new EFCoreTestContext(optionsBuilder.Options); return ctx.FederalDistricts.Include(x =>x.FederalSubjects).ToList(); > > // Вызов компонента происходит в данном примере на странице Home\Index @await Component.InvokeAsync("PopularPosts") // А собственно HTML с моделю по пути Shared\Components\PopularPosts\Default.cshtml
Напомню, когда выполняются запросы в Entity Framework Core.
При вызове операторов LINQ вы просто создаете представление запроса в памяти. Запрос отправляется в базу данных только после обработки результатов.
Ниже приведены наиболее распространенные операции, которые приводят к отправке запроса в базу данных.
- Итерация результатов в цикле for.
- Использование оператора, например ToList, ToArray, Single, Count.
- Привязка данных результатов запроса к пользовательскому интерфейсу.
Как же организовать код EF Core с точки зрения архитектуры приложения?
(1) C точки зрения архитектуры приложения, нужно обеспечить чтобы код доступа к вашей базе данных был изолирован / отделен в четко определенном месте (в изоляции). Это позволяет найти код базы данных, который влияет на производительность.
(2) Не смешивать код доступа к вашей базе данных с другими частями приложения, такими как пользовательский интерфейс или API. Таким образом, код доступа к базе данных можно изменить, не беспокоясь о других проблемах, не связанных с базой данных.
Как правильно и быстро сохранять данные с помощью SaveChanges?
Если вставляемые записи одинаковые имеет смысл использовать одну операцию сохранения на все записи.
Неправильно
using(var db = new NorthwindEntities()) < var transaction = db.Database.BeginTransaction(); try < // Вставка записи 1 var obj1 = new Customer(); obj1.CustomerID = "ABCDE"; obj1.CompanyName = "Company 1"; obj1.Country = "USA"; db.Customers.Add(obj1); //Сохраняем первую запись db.SaveChanges(); // Вставка записи 2 var obj2 = new Customer(); obj2.CustomerID = "PQRST"; obj2.CompanyName = "Company 2"; obj2.Country = "USA"; db.Customers.Add(obj2); // Сохраняем вторую запись db.SaveChanges(); transaction.Commit(); >catch < transaction.Rollback(); >>
using(var db = new NorthwindEntities()) < var transaction = db.Database.BeginTransaction(); try < //Вставка записи 1 var obj1 = new Customer(); obj1.CustomerID = "ABCDE"; obj1.CompanyName = "Company 1"; obj1.Country = "USA"; db.Customers.Add(obj1); // Вставка записи 2 var obj2 = new Customer(); obj2.CustomerID = "PQRST"; obj2.CompanyName = "Company 2"; obj2.Country = "USA"; db.Customers.Add(obj2); // Сохранение двух или N записей db.SaveChanges(); transaction.Commit(); >catch < transaction.Rollback(); >>
Всегда есть исключения из правила. Если контекст транзакции сложный, то есть состоит из нескольких независимых операций, то можно выполнять сохранение после выполнения каждой операции. А еще правильней использовать асинхронное сохранение в транзакции.
// Увеличение депозита его владельца public async Task AddDepositToHousehold(int householdId, DepositRequestModel model) < using (var transaction = await Context.Database.BeginTransactionAsync(IsolationLevel.Snapshot)) < try < // Добавить депозит в БД var deposit = this.Mapper.Map(model); await this.Context.Deposits.AddAsync(deposit); await this.Context.SaveChangesAsync(); // Оплатить задолжности с депозита var debtsToPay = await this.Context.Debts.Where(d => d.HouseholdId == householdId && !d.IsPaid).OrderBy(d => d.DateMade).ToListAsync(); debtsToPay.ForEach(d => d.IsPaid = true); await this.Context.SaveChangesAsync(); // Увеличение баланса владельца var household = this.Context.Households.FirstOrDefaultAsync(h => h.Id == householdId); household.Balance += model.DepositAmount; await this.Context.SaveChangesAsync(); transaction.Commit(); return this.Ok(); > catch < transaction.Rollback(); return this.BadRequest(); >> >
Триггеры, вычисляемые поля, пользовательские функции и EF Core
Для снижения нагрузки на приложения содержащим EF Core имеет смысл применять простые вычисляемые поля и триггеры баз данных, но лучше этим не увлекаться, так как приложение может оказаться очень запутанным. А вот пользовательские функции могут быть очень полезны особенно при операциях выборки!
Параллелизм в EF Core
Если ты хочешь все запараллелить чтобы ускориться, то обломись: EF Core не поддерживает выполнение нескольких параллельных операций в одном экземпляре контекста. Следует подождать завершения одной операции, прежде чем запускать следующую. Для этого обычно нужно указать ключевое слово await в каждой асинхронной операции.
EF Core использует асинхронные запросы, которые позволяют избежать блокирования потока при выполнении запроса в базе данных. Асинхронные запросы важны для обеспечения быстрого отклика пользовательского интерфейса в толстых клиентах. Они могут также увеличить пропускную способность в веб-приложении, где можно высвободить поток для обработки других запросов. Вот пример:
public async Task> GetBlogsAsync() < using (var context = new BloggingContext()) < return await context.Blogs.ToListAsync(); >>
А что вы знаете про компилированные запросы LINQ?
Если у вас есть приложение, которое многократно выполняет структурно похожие запросы в Entity Framework, вы часто можете повысить производительность, компилируя запрос один раз и выполняя его несколько раз с различными параметрами. Например, приложению может потребоваться получить всех клиентов в определенном городе; город указывается во время выполнения пользователем в форме. LINQ to Entities поддерживает использование для этой цели скомпилированных запросов.
Начиная с .NET Framework 4.5, запросы LINQ кэшируются автоматически. Тем не менее, вы все равно можете использовать скомпилированные запросы LINQ, чтобы снизить эту стоимость в последующих выполнениях, и скомпилированные запросы могут быть более эффективными, чем запросы LINQ, которые автоматически кэшируются. Обратите внимание, что запросы LINQ to Entities, которые применяют оператор Enumerable.Contains к коллекциям в памяти, не кэшируются автоматически. Также не допускается параметризация коллекций в памяти в скомпилированных запросах LINQ.
Много примеров можно посмотреть тут.
Не делайте больших контекстов DbContext!
В общем так, я знаю многие из вас, если не почти все — lazy f_u__c_k__e_r__s и всю базу данных вы размещаете в один контекст, особенно это свойственно для подхода Database-First. И зря вы это делаете! Ниже приведен пример как можно разделить контекст. Конечно, таблицы соединения между контекстами придется дублировать, это минус. Так или иначе если у вас в контексте более 50 таблиц лучше подумать о его разделении.
Использование группировки контекста (pooling DdContext)
Смысл пула DbContext состоит в том, чтобы разрешить повторное использование экземпляров DbContext из пула, что в некоторых случаях может привести к повышению производительности по сравнению с созданием нового экземпляра каждый раз. Это также является основной причиной создания пула соединений в ADO.NET, хотя прирост производительности для соединений будет более значительным, поскольку соединения, как правило, являются более тяжелым ресурсом.
using System; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace Demos < public class Blog < public int BlogId < get; set; >public string Name < get; set; >public string Url < get; set; >> public class BloggingContext : DbContext < public static long InstanceCount; public BloggingContext(DbContextOptions options) : base(options) =>Interlocked.Increment(ref InstanceCount); public DbSet Blogs < get; set; >> public class BlogController < private readonly BloggingContext _context; public BlogController(BloggingContext context) =>_context = context; public async Task ActionAsync() => await _context.Blogs.FirstAsync(); > public class Startup < private const string ConnectionString = @"Server=(localdb)\mssqllocaldb;Database=Demo.ContextPooling;Integrated Security=True;ConnectRetryCount=0"; public void ConfigureServices(IServiceCollection services) < services.AddDbContext(c => c.UseSqlServer(ConnectionString)); > > public class Program < private const int Threads = 32; private const int Seconds = 10; private static long _requestsProcessed; private static async Task Main() < var serviceCollection = new ServiceCollection(); new Startup().ConfigureServices(serviceCollection); var serviceProvider = serviceCollection.BuildServiceProvider(); SetupDatabase(serviceProvider); var stopwatch = new Stopwatch(); MonitorResults(TimeSpan.FromSeconds(Seconds), stopwatch); await Task.WhenAll( Enumerable .Range(0, Threads) .Select(_ =>SimulateRequestsAsync(serviceProvider, stopwatch))); > private static void SetupDatabase(IServiceProvider serviceProvider) < using (var serviceScope = serviceProvider.CreateScope()) < var context = serviceScope.ServiceProvider.GetService(); if (context.Database.EnsureCreated()) < context.Blogs.Add(new Blog < Name = "The Dog Blog", Url = "http://sample.com/dogs" >); context.Blogs.Add(new Blog < Name = "The Cat Blog", Url = "http://sample.com/cats" >); context.SaveChanges(); > > > private static async Task SimulateRequestsAsync(IServiceProvider serviceProvider, Stopwatch stopwatch) < while (stopwatch.IsRunning) < using (var serviceScope = serviceProvider.CreateScope()) < await new BlogController(serviceScope.ServiceProvider.GetService()).ActionAsync(); > Interlocked.Increment(ref _requestsProcessed); > > private static async void MonitorResults(TimeSpan duration, Stopwatch stopwatch) < var lastInstanceCount = 0L; var lastRequestCount = 0L; var lastElapsed = TimeSpan.Zero; stopwatch.Start(); while (stopwatch.Elapsed < duration) < await Task.Delay(TimeSpan.FromSeconds(1)); var instanceCount = BloggingContext.InstanceCount; var requestCount = _requestsProcessed; var elapsed = stopwatch.Elapsed; var currentElapsed = elapsed - lastElapsed; var currentRequests = requestCount - lastRequestCount; Console.WriteLine( $"[] " + $"Context creations/second: | " + $"Requests/second: "); lastInstanceCount = instanceCount; lastRequestCount = requestCount; lastElapsed = elapsed; > Console.WriteLine(); Console.WriteLine($"Total context creations: "); Console.WriteLine( $"Requests per second: "); stopwatch.Stop(); >
Как избежать лишних ошибок при CRUD в EF Core?
Никогда не делайте вычисления в вставку в одном коде. Всегда разделяйте формирование/подготовку объекта и его вставку/обновление. Просто разнесите по функциям: проверку введенных данным пользователем, вычисления необходимые предварительных данных, картирование или создание объекта, и собственно CRUD операцию.
Что делать, когда совсем дела плохо с производительностью приложения?
Пиво тут точно не поможет. А вот что поможет, так это разделение чтение и записи в архитектуре приложения с последующего разнесением по сокетам этих операций. Задумайтесь об использовании Command and Query Responsibility Segregation (CQRS) pattern, а также попробуйте, разделить таблицы на вставку и чтение между двумя базами данных.
Скоростных приложений вам, друзья и коллеги!
- entity framework core
- оптимизация
- c#.net
Dbset c что это
По умолчанию все типы сущностей, для которых определены в контексте данных наборы DbSet , включаются в модель и в дальнейшем сопоставляются с таблицами в базе данных. Например:
using Microsoft.EntityFrameworkCore; public class ApplicationContext : DbContext < public DbSetUsers < get; set; >= null!; public ApplicationContext() < Database.EnsureDeleted(); Database.EnsureCreated(); >protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) < optionsBuilder.UseSqlite("Data Source=helloapp.db"); >> public class User < public int Id < get; set; >public string? Name < get; set; >public int Age < get; set; >>
В данном случае, поскольку для класса User в классе контекста определено свойство типа DbSet
public DbSet Users < get; set; >= null!;
Таким образом, для сущности User будет создана таблица в бд.
Ссылочные nullable-типы и DbSet
Класс DbSet , как и другие типы, является ссылочным. А, начиная с C# 10 и .NET 6 автоматически применяется функциональность ссылочных nullable-типов. И переменные/свойства тех типов, которые не являются nullable, следует инициализировать некотором значением перед их использованием. Например, если мы напишем:
public DbSet Users
То мы столкнемся с предупреждением:
То есть нам надо инициализировать свойство типа DbSet. Хотя в этом нет большого смысла, так как контструктор базового класса DbContext гарантирует, что все свойства типа DbSet будут инициализированы и соответственно в принципе не будут иметь значение null .
Тем не менее проблема остается, поскольку мы сталкиваемся с предупреждением. Чтобы выйти из этой ситуации мы можем инициализировать свойство с помощью выражения null! , которое говорит, что данное свойство в принципе не будет иметь значение null:
public DbSet Users < get; set; >= null!;
Другое решение — инициализировать свойство значением типа Set :
public DbSet Users => Set();
Включение сущностей в модель без DbSet
Но кроме того, в модель также включаются типы, на которые есть ссылки в сущностях, которые уже включены в модель, например, через свойства DbSet.
Например, пусть у нас определены следующие сущности:
public class User < public int Id < get; set; >public string? Name < get; set; >public int Age < get; set; >// навигационное свойство public Company? Company < get; set; >> public class Company < public int Id < get; set; >public string? Name < get; set; >> public class Country < public int Id < get; set; >public string? Name < get; set; >>
И пусть у нас будет класс контекста данных:
using Microsoft.EntityFrameworkCore; public class ApplicationContext : DbContext < public DbSetUsers < get; set; >= null!; public ApplicationContext() < Database.EnsureDeleted(); Database.EnsureCreated(); >protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) < optionsBuilder.UseSqlite("Data Source=helloapp.db"); >>
После создания базы данных в ней будут созданы две таблицы: Users и Company. А третий класс — Country никак не используется в сущностях User и Company, для Country нет свойства DbSet в классе контекста, поэтому она не будет включена в контекст и для нее не будет создаваться таблица в бд.
Поскольку для типа User определен набор DbSet, то для имени таблицы будет применяться имя этого набора, а для второй таблицы будет использоваться имя класса Company.
Еще один способ включения сущности в модель представляет вызов Entity() объекта ModelBuilder в методе OnModelCreating() :
using Microsoft.EntityFrameworkCore; public class ApplicationContext : DbContext < public DbSetUsers < get; set; >= null!; public ApplicationContext() < Database.EnsureDeleted(); Database.EnsureCreated(); >protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) < optionsBuilder.UseSqlite("Data Source=helloapp.db"); >protected override void OnModelCreating(ModelBuilder modelBuilder) < modelBuilder.Entity(); > >
И если мы сейчас создадим и выполним миграции, то в базе данных будут уже три таблицы для сущностей:
Исключение из модели
Иногда возникают ситуации, когда надо, наоборот, исключить сущность из модели. Например, в примере выше сущность Company ссылается на класс Company, и, допустим, мы не хотим, чтобы в базе данных была таблица Company. В этом случае мы можем использовать Fluent API или аннотации данных.
Применение Fluent API заключается в вызове метода Ignore() :
using Microsoft.EntityFrameworkCore; public class ApplicationContext : DbContext < public DbSetUsers < get; set; >= null!; public ApplicationContext() < Database.EnsureDeleted(); Database.EnsureCreated(); >protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) < optionsBuilder.UseSqlite("Data Source=helloapp.db"); >protected override void OnModelCreating(ModelBuilder modelBuilder) < modelBuilder.Ignore(); > >
Аннотации данных предполагают установку над классом атрибута [NotMapped] :
using System.ComponentModel.DataAnnotations.Schema; public class User < public int Id < get; set; >public string? Name < get; set; >public int Age < get; set; >// навигационное свойство public Company? Company < get; set; >> [NotMapped] public class Company < public int Id < get; set; >public string Name < get; set; >>
При исключении сущности Company в базе данных будет только одна таблица Users, причем она не будет содержать столбца, который бы сопоставлялся со свойством Company класса User:
Dbset c что это
Данное руководство устарело. Актуальное руководство: Руководство по Entity Framework Core
Последнее обновление: 24.10.2018
Чтобы непосредственно начать работать с Entity Framework, создадим первое приложение. Для этого нам нужна будет, во-первых, среда разработки. В качестве среды разработки выберем Visual Studio 2017.
В окне создания проекта в левой части выберем секцию Visual C#->Windows Desktop и в центральной части окна в качестве типа проекта выберем Console App (.NET Framework) .
Теперь первым делом добавим новый класс, который будет описывать данные. Пусть наше приложение будет посвящено работе с пользователями. Поэтому добавим в проект новый класс User:
public class User < public int Id < get; set; >public string Name < get; set; >public int Age < get; set; >>
Это обычный класс, который содержит некоторое количество автосвойств. Каждое свойство будет сопоставляться с отдельным столбцом в таблице из бд.
Надо отметить, что Entity Framework при работе с Code First требует определения ключа элемента для создания первичного ключа в таблице в бд. По умолчанию при генерации бд EF в качестве первичных ключей будет рассматривать свойства с именами Id или [Имя_класса]Id (то есть UserId). Если же мы хотим назвать ключевое свойство иначе, то нам нужно будет внести дополнительную логику на c#.
Теперь для взаимодействия с бд нам нужен контекст данных. Это своего рода посредник между бд и классами, описывающими данные. Но, у нас по умолчанию еще не добавлена библиотека для EF. Чтобы ее добавить, нажмем на проект правой кнопкой мыши и выберем в контекстном меню Manage NuGet Packages.
Затем в появившемся окне управления NuGet-пакетами в окне поиска введем слово «Entity» и выберем пакет собственно Entity Framework и установим его:
После установки пакета добавим в проект новый класс UserContext:
using System; using System.Collections.Generic; using System.Data.Entity; namespace FirstEF6App < class UserContext : DbContext < public UserContext() :base("DbConnection") < >public DbSet Users < get; set; >> >
Основу функциональности Entity Framework составляют классы, находящиеся в пространстве имен System.Data.Entity. Среди всего набора классов этого пространства имен следует выделить следующие:
- DbContext : определяет контекст данных, используемый для взаимодействия с базой данных.
- DbModelBuilder : сопоставляет классы на языке C# с сущностями в базе данных.
- DbSet/DbSet : представляет набор сущностей, хранящихся в базе данных
В любом приложении, работающим с БД через Entity Framework, нам нужен будет контекст (класс производный от DbContext) и набор данных DbSet, через который мы сможем взаимодействовать с таблицами из БД. В данном случае таким контекстом является класс UserContext.
В конструкторе этого класса вызывается конструктор базового класса, в который передается строка «DbConnection» — это имя будущей строки подключения к базе данных. В принципе мы можем не использовать конструктор, тогда в этом случае строка подключения носила бы имя самого класса контекста данных.
И также в классе определено одно свойство Users, которое будет хранить набор объектов User. В классе контекста данных набор объектов представляет класс DbSet . Через это свойство будет осуществляться связь с таблицей объектов User в бд.
И теперь нам надо установить подключение к базе данных. Для установки подключения обычно используется файл конфигурации приложения. В проектах для десктопных приложений файл конфигурации называется App.config (как в нашем случае), в проектах веб-приложений — web.config . В нашем случае, поскольку у нас консольное приложение, это файл App.config . После добавления Entity Framework он выглядит примерно следующим образом:
Содержимое файла в каждом конкретном случае может отличаться. Но в любом случае после добавления EntityFramework в проект в нем будет содержаться элемент configSections. И после закрывающего тега добавим следующий элемент:
Все подключения к источникам данных устанавливаются в секции connectionStrings , а каждое отдельное подключение представляет элемент add . В конструкторе класса контекста UserContext мы передаем в качестве названия подключения строку «DbConnection», поэтому данное название указывается в атрибуте name=»DBConnection» .
Настройку строки подключения задает атрибут connectionString . В данном случае мы устанавливаем название базы данных, с которой будем взаимодействовать — userstore.
Теперь перейдем к файлу Program.cs и изменим его содержание следующим образом:
using System; namespace FirstEF6App < class Program < static void Main(string[] args) < using(UserContext db = new UserContext()) < // создаем два объекта User User user1 = new User < Name = "Tom", Age = 33 >; User user2 = new User < Name = "Sam", Age = 26 >; // добавляем их в бд db.Users.Add(user1); db.Users.Add(user2); db.SaveChanges(); Console.WriteLine("Объекты успешно сохранены"); // получаем объекты из бд и выводим на консоль var users = db.Users; Console.WriteLine("Список объектов:"); foreach(User u in users) < Console.WriteLine(". - ", u.Id, u.Name, u.Age); > > Console.Read(); > > >
Так как класс UserContext через родительский класс DbContext реализует интерфейс IDisposable , то для работы с UserContext с автоматическим закрытием данного объекта мы можем использовать конструкцию using .
В конструкции using создаются два объекта User и добавляются в базу данных. Для их сохранения нам достаточно использовать метод Add : db.Users.Add(user1)
Чтобы получить список данных из бд, достаточно воспользоваться свойством Users контекста данных: db.Users
В результате после запуска программа выведет на консоль:
Объекты успешно сохранены Список объектов: 1.Tom - 33 2.Sam - 26
Таким образом, Entity Framework обеспечивает простое и удобное управление объектами из базы данных. При том в данном случае нам не надо даже создавать базу данных и определять в ней таблицы. Entity Framework все сделает за нас на основе определения класса контекста данных и классов моделей. И если база данных уже имеется, то EF не будет повторно создавать ее.
Наша задача — только определить модель, которая будет храниться в базе данных, и класс контекста. Поэтому данный подход называется Code First — сначала пишется код, а потом по нему создается база данных и ее таблицы.
Возникает вопрос, а где же находится БД? Чтобы физически увидеть базу данных, мы можем подключиться к ней из Visual Studio через окно View->SQL Server Object Explorer . После этого мы можем увидеть в SQL Server Object Explorer созданную базу данных, посмотреть ее строение, таблицы, открыть и даже изменить данные в таблицах:
Физически база данных по умолчанию будет располагаться в папке пользователя, в частности, у меня она размещена в каталоге C:\Users\Eugene\ , только к ее названию буде добавляться стандартное расширение mdf — userstore.mdf.