Какова наилучшая практика для нескольких "Include" -s в Entity Framework? - программирование
Подтвердить что ты не робот

Какова наилучшая практика для нескольких "Include" -s в Entity Framework?

Скажем, у нас есть четыре объекта в модели данных: категории, книги, авторы и книжные страницы. Также предположим, что отношения "Книги-Книги", "Книги-Авторы" и "Книги-Книжные страницы" являются "один ко многим".

Если экземпляр сущности категории извлекается из базы данных, включая "Книги", "Книжные книги" и "Books.Authors" - это станет серьезной проблемой производительности. Более того, не включая их приведет к тому, что исключение "Ссылка на объект не задано на экземпляр объекта".

Какова наилучшая практика для использования нескольких вызовов метода Include?

  • Напишите один метод GetCategoryById и включите все элементы внутри (проблема с производительностью)
  • Напишите один метод GetCategoryById и отправьте список отношений для включения (возможно, но все еще кажется недостаточно элегантным).
  • Напиши такие методы, как GetCategoryByIdWithBooks, GetCategoryByIdWithBooksAndBooksPages и GetCategoryByIdWithBooksAndAuthors (не практично).

EDIT: по второму варианту я имел в виду примерно следующее:

public static Category GetCategoryById(ModelEntities db, int categoryId, params string[] includeFields)
{
    var categories = db.Categories;

    foreach (string includeField in includeFields)
    {
        categories = categories.Include(includeField);
    }

    return categories.SingleOrDefault(i => i.CategoryId == categoryId);
}

При вызове нам нужен такой код:

Category theCategory1 = CategoryHelper.GetCategoryById(db, 5, "Books");
Category theCategory2 = CategoryHelper.GetCategoryById(db, 5, "Books", "Books.Pages");
Category theCategory3 = CategoryHelper.GetCategoryById(db, 5, "Books", "Books.Authors");
Category theCategory4 = CategoryHelper.GetCategoryById(db, 5, "Books", "Books.Pages", "Books.Authors");

Существуют ли какие-либо отличительные признаки этого подхода?

4b9b3361

Ответ 1

Напишите один метод GetCategoryById и отправьте список отношений для включения (возможно, но все еще кажется недостаточно элегантным)

Напиши такие методы, как GetCategoryByIdWithBooks, GetCategoryByIdWithBooksAndBooksPages и GetCategoryByIdWithBooksAndAuthors (непрактично)

Комбинация этих двух в настоящее время - мой подход. Я знаю, какие свойства я хочу включить для каждого контекста, поэтому я скорее их код с ручным кодом (как вы сказали сами, ленивая загрузка не всегда является опцией, и если это так, вы будете повторите то же повторяющееся синтаксическое Include() -подобное при сопоставлении данных с моделями DTO).

Это разделение заставляет вас больше думать о том, какие типы данных вы хотите выставить, поскольку такой код доступа к данным, как это, обычно скрывается под сервисом.

Используя базовый класс, содержащий виртуальный метод, вы можете переопределить для запуска требуемого Include() s:

using System.Data.Entity;

public class DataAccessBase<T>
{
    // For example redirect this to a DbContext.Set<T>().
    public IQueryable<T> DataSet { get; private set; }

    public IQueryable<T> Include(Func<IQueryable<T>, IQueryable<T>> include = null)
    {
        if (include == null)
        {
            // If omitted, apply the default Include() method 
            // (will call overridden Include() when it exists) 
            include = Include;
        }

        return include(DataSet);
    }

    public virtual IQueryable<T> Include(IQueryable<T> entities)
    {
        // provide optional entities.Include(f => f.Foo) that must be included for all entities
        return entities;
    }
}

Затем вы можете создать экземпляр и использовать этот класс как есть или расширить его:

using System.Data.Entity;

public class BookAccess : DataAccessBase<Book>
{
    // Overridden to specify Include()s to be run for each book
    public override IQueryable<Book> Include(IQueryable<Book> entities)
    {
        return base.Include(entities)
                   .Include(e => e.Author);
    }

    // A separate Include()-method
    private IQueryable<Book> IncludePages(IQueryable<Book> entities)
    {
        return entities.Include(e => e.Pages);
    }

    // Access this method from the outside to retrieve all pages from each book
    public IEnumerable<Book> GetBooksWithPages()
    {
        var books = Include(IncludePages);
    }
}

Теперь вы можете создать экземпляр BookAccess и вызвать на нем методы:

var bookAccess = new BookAccess();

var allBooksWithoutNavigationProperties = bookAccess.DataSet;
var allBooksWithAuthors = bookAccess.Include();
var allBooksWithAuthorsAndPages = bookAccess.GetBooksWithPages();

В вашем случае вы можете создать отдельные пары методов IncludePages и GetBooksWithPages -alike для каждого представления вашей коллекции. Или просто напишите его как один метод, существует метод IncludePages для повторного использования.

Вы можете связать эти методы так, как вам нравится, поскольку каждый из них (а также метод расширения Entity Framework Include()) возвращает еще один IQueryable<T>.

Ответ 2

Как указано в комментариях @Colin, вам нужно использовать ключевое слово virtual при определении свойств навигации, чтобы они могли работать с ленивой загрузкой. Предполагая, что вы используете Code-First, ваш класс Book должен выглядеть примерно так:

public class Book
{
  public int BookID { get; set; }
  //Whatever other information about the Book...
  public virtual Category Category { get; set; }
  public virtual List<Author> Authors { get; set; }
  public virtual List<BookPage> BookPages { get; set; }
}

Если ключевое слово virtual не используется, то класс прокси, созданный EF, не сможет ленить загрузить связанный объект/сущности.

Конечно, если вы создаете новую книгу, она не сможет выполнять ленивую загрузку и просто выбросит исключение NullReferenceException, если вы попытаетесь выполнить итерацию над BookPages. Вот почему вы должны сделать одну из двух вещей:

  • определить конструктор Book(), который включает BookPages = new List<BookPage>(); (тот же для Authors) или
  • убедитесь, что ТОЛЬКО у вас когда-либо было "new Book()" в вашем коде, когда вы создаете новую запись, которую вы немедленно сохраняете в базе данных, а затем отбрасываете, не пытаясь получить что-либо от нее.

Я лично предпочитаю второй вариант, но я знаю, что многие другие предпочитают 1-й.

<EDIT> Я нашел третий вариант, который должен использовать метод Create класса DbSet<>. Это означает, что вы должны называть myContext.Books.Create() вместо new Book(). См. Этот Q + A для получения дополнительной информации: Рамификации DbSet.Create против нового объекта() </EDIT>

Теперь другой способ, которым ленивая загрузка может сломаться, - это когда он выключен. (Я предполагаю, что ModelEntities - это имя вашего класса DbContext.) Чтобы отключить его, вы должны установить ModelEntities.Configuration.LazyLoadingEnabled = false; Pretty self explainatory, no?

Нижняя строка заключается в том, что вам не нужно использовать Include() всюду. Это действительно означало скорее скорее средство оптимизации, чем требование для того, чтобы ваш код функционировал. Использование Include() чрезмерно приводит к очень низкой производительности, потому что вы получаете гораздо больше, чем вам действительно нужно из базы данных, потому что Include() всегда будет включать все связанные записи. Скажем, что вы загружаете категорию, и есть 1000 книг, принадлежащих этой категории. Вы не можете отфильтровать его, чтобы включить в него только книги, написанные Джоном Смитом, при использовании функции Include(). Однако вы можете (при включенной ленивой загрузке) сделать следующее:

Category cat = ModelEntities.Categorys.Find(1);
var books = cat.Books.Where(b => b.Authors.Any(a => a.Name == "John Smith"));

Это фактически приведет к тому, что меньшее количество записей будет возвращено из базы данных и гораздо проще понять.

Надеюсь, что это поможет!;)

Ответ 3

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

Во-первых, обратите внимание, что объекты DbContextObjectContext) не являются потокобезопасными.

Если вы заинтересованы в повышении производительности, тогда первый вариант является самым простым.

С другой стороны, если вы беспокоитесь о производительности и готовы избавиться от объекта контекста после получения данных, тогда вы можете запрашивать данные с несколькими одновременными задачами (потоками), каждый из которых использует свой собственный контекст объект.

Если вам нужен контекст для отслеживания изменений данных, у вас есть прямой способ для одного запроса, чтобы добавить все элементы в контекст, или вы можете использовать метод Attach для "перестроения" исходного состояния и затем измените и сохраните.

Последний выглядит примерно так:

using(var dbContext = new DbContext())
{
    var categoryToChange = new Categories()
    {
        // set properties to original data
    };
    dbContext.Categories.Attach(categoryToChange);
    // set changed properties
    dbContext.SaveChanges();
}

К сожалению, нет никакой лучшей практики для удовлетворения всех ситуаций.

Ответ 4

В первом подходе db, скажем, вы создаете BookStore.edmx и добавляете объект Category и Book, и он генерирует контекст как public partial class BookStoreContext : DbContext, тогда это простая хорошая практика, если вы можете добавить частичный класс следующим образом:

public partial class BookStoreContext
{
    public IQueryable<Category> GetCategoriesWithBooks()
    {
        return Categories.Include(c => c.Books);
    }

    public IQueryable<Category> GetCategoriesWith(params string[] includeFields)
    {
        var categories = Categories.AsQueryable();
        foreach (string includeField in includeFields)
        {
            categories = categories.Include(includeField);
        }
        return categories;
    }

    // Just another example
    public IQueryable<Category> GetBooksWithAllDetails()
    {
        return Books
            .Include(c => c.Books.Authors)
            .Include(c => c.Books.Pages);
    }

    // yet another complex example
    public IQueryable<Category> GetNewBooks(/*...*/)
    {
        // probably you can pass sort by, tags filter etc in the parameter.
    }
}

Затем вы можете использовать его следующим образом:

var category1 = db.CategoriesWithBooks()
                      .Where(c => c.Id = 5).SingleOrDefault();
var category2 = db.CategoriesWith("Books.Pages", "Books.Authors")
                      .Where(c => c.Id = 5).SingleOrDefault(); // custom include

Примечание:

  • Вы можете прочитать несколько простых (так много сложных) шаблонов репозитория, чтобы расширить IDbSet<Category> Categories, чтобы сгруппировать общие Include и Where вместо статического CategoryHelper. Таким образом, вы можете иметь IQueryable<Category> db.Categories.WithBooks()
  • Вы не должны включать все дочерние объекты в GetCategoryById, потому что он не объясняет себя в имени метода, и это может вызвать проблемы с производительностью, если пользователь этого метода не является братом по поводу Books entites.
  • Несмотря на то, что вы не включаете всех, если вы используете ленивую загрузку, у вас все еще может быть потенциальная проблема N + 1
  • Если у вас есть 1000 из Books, то лучше вы нарисуете свою нагрузку примерно так: db.Books.Where(b => b.CategoryId = categoryId).Skip(skip).Take(take).ToList() или еще лучше добавьте выше описанный метод db.GetBooksByCategoryId(categoryId, skip, take)

Я сам предпочитаю явно загружать объекты, поскольку я буду "знать", что в настоящее время загружается, но ленивая загрузка полезна только если у вас есть условная загрузка дочерних объектов и должна использоваться в небольшой области контекста db, иначе я не могу управлять db и насколько большой результат.