Entity Framework Code First на практике

Здравствуйте!

В этой публикации хочу поделиться личным опытом использования Entity Framework (EF) в реальном приложении и дать несколько полезных советов использования данного фреймворка.

Я занимаюсь разработкой enterprise приложений на платформе .NET больше 7 лет, за это время перепробовал несколько ORM библиотек, но сейчас для новых проектов использую Entity Framework Code First.

Изначально, как следует из названия, данный подход предполагал, что база данных, хранящая данные приложения, описывается сначала с помощью кода, а затем фреймворк сам создает или обновляет её. Однако многие разработчики предпочли использовать прямо противоположный подход, когда сначала создается база данных, а затем уже к ней мапятся объекты. Это особенно удобно для enterprise приложений, где данные почти всегда ставятся во главу угла и используется довольно продвинутая СУБД типа Oracle или MSSQL Server. Мне не нравится дизайнер EF, когда в нем количество таблиц переваливает за сотню. Поэтому Code First был воспринят лично мной как нечто безумно удобное и крайне полезное.

Еще раз хочу повторить, что, на мой взгляд, в enterprise приложении главное — это данные, а не ваше приложение. Приложения приходят и уходят, заменяются на более новые, а данные остаются и используются годами. Поэтому в любом моем приложении база данных всегда создается проектировщиком базы данных, а не моим приложением.

Первое, что нужно сделать, если у вас уже есть готовая база данных — это создать классы, которые бы соответствовали таблицам. Вручную это делать очень утомительно, поэтому:

Совет №1. Генерация класса на основе таблицы


Cкрипт T-SQL, который можно взять отсюда, реально работает и очень удобен.

Совет №2. Прегенерация view


EF известен очень долгим временем обработки первого запроса на получение и сохранение данных. Для того, чтобы исправить эту ситуацию, можно использовать прегенерацию view.

Прегенерация view — это нечто, что происходит внутри фреймворка при выполнении первого запроса. Этот процесс можно перенести на этап компиляции вашего приложения, что существенно сократит скорость выполнения первого запроса. Для разных версий EF способы несколько отличаются. Я использовал этот подход и полностью остался им доволен.

Совет №3. Массовое обновление данных с помощью DetectChanges


По умолчанию фреймворк настроен на то, чтобы автоматически отслеживать изменения, которые затем нужно будет сохранить в базу данных.
Например, рассмотрим такой простой пример:

var dbContext = new MyDbContext();
//Добавляем большое число записей в некоторую таблицу
for(var i=0;i<1000;i++)
{   
    dbContext.People.Add(new Person());//!!! ОЧЕНЬ ДОЛГО РАБОТАЕТ
}
//Сохраняем изменения в БД
dbContext.SaveChanges();


Запустив его, многие наверняка будут удивлены, что основное время будет потрачено не собственно на запрос к базе данных, а на вставку объектов в сам dbContext. Дело в том, что при изменении данных внутри DbContext-а происходит масса проверок и других малоизученных вещей, подробнее о которых можно прочесть здесь. Чтобы этого избежать можно отключить слежение за изменениями в классе DbContext, а затем явно вызвать метод DetectChanges(), который сам эти изменения обнаружит. Так, данный код будет работать значительно быстрее:

var dbContext = new MyDbContext();
//Отключаем автоматическое слежение за изменениями
dbContext.Configuration.AutoDetectChangesEnabled = false;
//Добавляем большое число записей в некоторую таблицу
for(var i=0;i<1000;i++)
{   
    dbContext.People.Add(new Person());//теперь этот метод работает значительно быстрее
}
dbContext.ChangeTracker.DetectChanges(); //Обновляем сведения об изменениях. Работает быстро
//Сохраняем изменения в БД
dbContext.SaveChanges();


Совет №4. Следите за sql-запросами, которые формирует фреймворк


Это довольно универсальный совет, применимый, наверное, ко всем ORM, но многие почему-то о нем забывают. И после внедрения приложения, открыв профайлер и увидев 50 запросов к БД для показа довольно простой формы, бывают крайне удивлены.

Между тем, фреймворк предлагает механизмы для некоторой оптимизации. Среди этих механизмов наиболее известный — метод Include, который «вытягивает» дочерние объекты в том же запросе. Например:

var dbContext = new MyDbContext();
//Наряду с объектом Person в этом запросе также будет получен дочерний объект Address
var person = dbContext.People.Include(p=>p.Address).FirstOrDefault();
//Запроса к БД не будет
var address = person.Address;


Совет №5. Изолируйте логику работы с базой данных


Хотя в названии EF есть слово «фреймворк», его можно использовать и как обычную библиотеку просто для упрощения работы с базой данных.
В большинстве книг и презентаций мы видим код, похожий на этот:

//Контроллер MVC 
public class PersonController: Controller
{
    //Контекст EF приложения
    AppDbContext dbContext;
    public PersonController(AppDbContext dbContext)
    {
        this.dbContext = dbContext;
    }
    
    public IActionResult ViewPerson(int id)
    {
        var person = dbContext.People.First(p=>p.Id == id);
        return View(person);
    }
}


Проблема этого кода в том, что он плотно привязывает все части приложения к Entity Framework. Это не хорошо и не плохо само по себе, но при этом вы должны понимать, что существенно осложняете себе жизнь, если архитектуру доступа к данным придется менять. А вариантов тут может быть масса:

  • Вы внезапно обнаружите, что определенный запрос работает крайне медленно и решите заменить его на хранимую процедуру или View
  • Вы с удивлением обнаружите, насколько сложно сохранять данные, полученные в разных DbContext-ах
  • Вы с удивлением будете наблюдать классы TransactionScope, DbConnection внутри контроллера, когда понадобится нетривиальная логика обновления данных
  • Вы задумаетесь о повторном использовании кода для работы с БД
  • Вы решите использовать вместо EF NHibernate
  • А почему бы не использовать SOA архитектуру, и не начать получать и сохранять данные через веб-сервисы?

Мне нравится подход, когда EF используется ТОЛЬКО внутри проекта с названием DAL (DataAccess и т.п.) в классах с названием Repository, например:

//Репозиторий для получения объектов класса Person
public class PersonRepository: IPersonRepository
{
    public Person GetPerson(int personId)
    {
        var dbContext = new AppDbContext();
        var person = dbContext.People.First(p=>p.Id == id);
        return person;
    }
    
    public void SavePerson(Person person)
    {
        var dbContext = new AppDbContext();
        var dbPerson = dbContext.People.First(p=>p.Id == person.Id);
        dbPerson.LastName = person.LastName;
        .....
        dbContext.SaveChanges();
        return person;
    }
}


Этот подход хорош тем, что ваше приложение будет знать только о ваших объектах и о том, как их можно получить из БД и как сохранить в БД. Поверьте, этим вы существенно облегчите себе жизнь.
Share post

Comments 22

    +6
    Совет №3. Массовое обновление данных с помощью DetectChanges

    В EF 6 добавлены методы DbSet.AddRange и DbSet.RemoveRange, которые решают данную проблему.

    Кстати, иногда для ускорения вставки так же рекомендуют отключить DbContext.Configuration.ValidateOnSaveEnabled, но это может привести к соответствующим результатам, поэтому этим стоит пользоваться, только если проверка данных была предварительно произведена.
      +5
      Дополнительно:
      var dbContext = new AppDbContext();

      Упущен контроль времени жизни контекста данных.
      Одно из общих правил работы с контекстом данных состоит в том, что контекст необходимо освобождать, как только он перестает быть необходимым.
      Т.е. в данном случае необходимо воспользоваться оператором using:

      using (var dbContext = new AppDbContext()){
      ...
      }
      
        +3
        Сильно зависит от того, как вы используете LazyLoading. Внезапно получить ObjectDisposedException при работе с объектом, полученным из БД, не особо приятно.
        Совет здесь один — освобождайте явно контекст, когда он вам 100% больше не нужен.
          +2
          Согласен, это возможно в случае наличия Navigation Properties.

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

          Хотел бы отметить, что наличие свойств с отложенной загрузкой вне рамок конкретного репозитория, или какого-то дополнительного уровня (например уровня сервисов), может привести к неожиданному поведению в процессе дальнейшего использования.
          Зачастую возникают случаи, когда пользователь ожидает 1 вызов базы данных, в то время, как получает N+1 вызов (например, к этому может привести обращение к свойству навигации, которое является коллекцией каких-то элементов, в цикле foreach).

          Общими рекомендациями при работе со свойствами навигации являются следующие:
          1. Не использовать отложенную загрузку или перенести ее включение на этап оптимизации.
          2. Не использовать свойства навигации выше определенного уровня абстракции (например, уровня репозитория или сервисов). При этом подключать необходимые свойства навигации при помощи методов Explicit loading или Eager loading (при помощи метода Include).
          3. Использовать DbContext, время жизни которого есть возможность контролировать (например, использовать для этого IoC контейнер).
      0
      У меня есть вопрос:

      Пусть у вас есть сущность Person, пусть она имет следующие атрибуты: Id, Name, Age, Phone, etc.
      Положим также, что вам нужно иметь метод который умеет фильтровать такие сущности по некоторым из перечисленых атрибутов.
      Как будет выглядеть метод репозитория, который реализует данную функциональность?
      Будет ли он просто возвращать dbContext.People?
        0
        Я бы сделал что-то типа:
        public Person[] FindPeople(PersonFilter filter)
        {
        	using(var dbContext = new AppDbContext())
        	{
        		var query = dbContext.People;
        		if(filter.Age != null)
        			query = query.Where(p=>p.Age == filter.Age);
        		//Остальная фильтрация
        		...
        		return query.ToArray();
        	}
        }
        

        Смысл в том, что метод делает ровно то, что вы от него ждете — выдает вам из БД фильтрованный список записей. В параметры вы можете ему передавать поля фильтрации, пейджинга, сортировки и т.п. — все будет делаться в этом методе.
          0
          AsQuerable() еще нужно не забыть вызвать:
          var query = dbContext.People.AsQuerable();
          
            0
            Этож тормозить будет
              0
              Что будет тормозить? Это же пример подхода к построению репозитория. Вся его прелесть в том, что вы можете оптимизировать этот метод фильтрации как угодно, а потребители этого метода ничего не заметят.
                +1
                Получение всех объектов Person со всеми полями, а потом компиляция Expression Tree в метод.
                  +2
                  Где вы здесь видите получение ВСЕХ объектов Person (всех что есть в БД)? В этом методе сначала строится LINQ запрос (query), а затем по нему вытянутся из БД только нужные Person-ы. Или вы говорите о том, что само построение SQL запроса из ExpressionTree будет тормозить?
                  Вопрос в том, что вам, возможно, не нужны ВСЕ поля класса Person в результате — тогда можно выдавать какой-нибудь PersonView[] с нужными полями.
                    0
                    Это я чето неправильно прочитал. Но сути не меняет.

                    Чтобы вернуть Person нужно прочитать все его поля. А это почти 100% не дает возможности построить адекватный покрывающий индекс да и тупо дольше работает, чем с проекцией.

                    А если вы начинаете плодить PersonView, то у вас появляется копипаста.

            0
            А проекцию как сделать? Без нее будет тормозить.
            Также вы предлагаете для каждого класса Entity написать руками EntityFilter и один метод, который этот EntityFilter применяет к IQueryable? Не проще пользоваться IQueryable?
              0
              Это больше архитектурный вопрос. Многое зависит от вашего конкретного случая. Всякое решение имеет плюсы и минусы. IQuerable вносит дополнительную неопределенность в ваш слой доступа к данным, что лично мне не нравится. Я люблю определенность.
            0
            существует несколько способов реализации репозиториев. В общем случае, можно использовать:
            public class Repository<T> {
               IList<T> LoadAllMatching<TOrderBy>(Expression<Func<T, bool>> whereCondition, Expression<Func<T, TOrderBy>> orderBy, bool desc);
            }

            и в коде реализовать как:
             var query = GetQuery<T>().Where(whereCondition);
             query = desc ? query.OrderByDescending(orderBy) : query.OrderBy(orderBy);

            Но лучше этого избегать и рассмотреть specification pattern
              0
              Какой в этом смысл? Почему нельзя просто пользоваться IQueryable?
                +1
                Зачем вам тогда репозиторй вообще? EF сам по себе представляет абстракцию, с которой вы работаете. Если вы возвращаете из репозитория IQuerable, то репозиторий по сути превращается в некий helper для DbContext.
                С тем же успехом можно просто методы в ваш контекст добавлять и работать с ними.
                Также проблема IQuerable в том, что вы перекладываете логику (включая всевозможные оптимизации) построения реального запроса к БД на потребителя вашего метода, т.к. он может накрутить на ваш IQuerable еще кучу всякой логики.
                  +1
                  В этом-то и вопрос, стоит ли вообще использовать паттерн Repository при использовании EF?
                  EF с одной стороны реализует Unit of Work через класс DbContext, с другой стороны реализует Specification(ну или их аналог) через предикаты в IQueryable.
                  И тут мы оказываемся на перепутье: с одной сторны не плохо было бы изолировать логику работы с БД в Repository, с другой стороны это уже сделал за нас EF,
                  Получается в Repository стоит добавлять только следующие методы:
                  1. Часто повторяющиеся, не однострочные, например какой-нибдуь метод которые делает пару join'ов и который встречается больше одного раза.
                  2. Использующие не типизированный код, например, вызов каких-нибудь нетривильных SQL-запросов

                  А все остальные запросы к EF контексту оставить как есть, т.е. запрос вида var person = dbContext.People.First(p=>p.Id == id); использовать напрямую.

                  Что думаете?
                    +1
                    В основном согласен, в плане того, что специфические запросы реализовать в виде методов, а остальное дать на откуп пользователям, которые могут выполнить специфические запросы или целые комбинации запросов. В процессе использования будут кристаллизированны и другие необходимые методы.

                    Зачем вам тогда репозиторй вообще?

                    Не стоит забывать о том, что Repository является дополнительным уровнем абстракции, который вы контролируете. Нет привязки к конкретному хранилищу или технологии хранения. Так же, можно вводить дополнительные ограничения, которые диктуются другими используемыми принципами проектирования, например, репозитории могут быть реализованы только для корневых объектов (aggregate root), т.е. появляется дополнительный контроль над выполнением принципов DDD.
                      +4
                      Я скажу так. EF — это фреймворк и это не только получение, но и сохранение данных. Как и любой фреймворк, он навязывает вам определенную архитектуру по работе с БД. В частности, паттерн UnitOfWork.
                      Если вы делаете общедоступным свой DbContext или методы, возвращающие IQuerable, вы подсаживаете все части своего приложения на логику работы ORM. Хотя это дает гибкость в получении и сохранении любых данных в любом месте вашего приложения, есть и весьма значительные минусы, например:
                      — у вас появляются похожие запросы в разных частях приложения. Приходится все равно делать что-то типа репозитория, чтобы избежать дублирования кода
                      — вы начинаете сохранять данные через DbContext.SaveChanges() в куче мест вашего приложения. Затем возникают сложные ситуации, где надо использовать транзакции, хранимые процедуры, определенную последовательность сохранения и т.п. Вам приходится все равно выносить всю эту логику из клиентского кода в репозиторий.
                      — вы понимаете, что EF — вообще отстой, делает очень неэффективные запросы, ест кучу памяти, при сохранении выдает вам какие-то странные и непонятные exception-ы и вообще начинает вас выводить из себя. А оказывается, что на него всё завязано и чтобы это переделать, надо переписать и ЗАНОВО ПРОТЕСТИРОВАТЬ 70% приложения. Вот тут вы начинаете по другому делать СЛЕДУЮЩЕЕ приложение :)
                        +1
                        — у вас появляются похожие запросы в разных частях приложения. Приходится все равно делать что-то типа репозитория, чтобы избежать дублирования кода

                        Не придется, обычных функций достаточно и нет необходимости прятать IQueryable.

                        — вы начинаете сохранять данные через DbContext.SaveChanges() в куче мест вашего приложения. Затем возникают сложные ситуации, где надо использовать транзакции, хранимые процедуры, определенную последовательность сохранения и т.п. Вам приходится все равно выносить всю эту логику из клиентского кода в репозиторий.


                        Не придется. Транзакции — это бизнес-функционал. Если транзакции тащить в репозитории, то вся БЛ переедет в репозитории.

                        — вы понимаете, что EF — вообще отстой, делает очень неэффективные запросы, ест кучу памяти, при сохранении выдает вам какие-то странные и непонятные exception-ы и вообще начинает вас выводить из себя. А оказывается, что на него всё завязано и чтобы это переделать, надо переписать и ЗАНОВО ПРОТЕСТИРОВАТЬ 70% приложения. Вот тут вы начинаете по другому делать СЛЕДУЮЩЕЕ приложение :)

                        А как тут поможет репозиторий? На него не будет завязано? Или вы думаете что можно «просто» поменять EF на рукопашные запросы? А если у вас LL использовался?
                0
                Ответили выше.

              Only users with full accounts can post comments. Log in, please.