Асинхронные запросы и ленивая загрузка в Entity Framework - PullRequest
4 голосов
/ 02 февраля 2012

Мы используем Entity Framework 4.2 с первым подходом к модели и генерацией кода DbContext.

Допустим, у нас есть следующая модель данных в структуре сущностей:

class Person
{
    public string Name { get; set; }
    public Address Address { get; set; }
}

class Address
{
    public string City; { get; set; }
}

Сценарий следующий:

  1. ViewModel хочет загрузить некоторые данные из базы данных
  2. Для загрузки данных создается задача (асинхронная операция). это потому что мы не хотим, чтобы пользовательский интерфейс зависал при загрузке данных.
  3. Задача (которая выполняется в отдельном потоке) создает новые контекст базы данных и загружает данные (например, объект Person) из базы данных
  4. Задача завершается и контекст базы данных уничтожается.
  5. Главный поток уведомляется о завершении задачи. Основная нить может теперь получить доступ к загруженному объекту Person.
  6. Представление пытается отобразить имя и адрес человека в текстовом поле через привязку данных
  7. Представление обращается к Person.Name (здесь нет проблем)
  8. Представление обращается к Person.Address.City -> OOPS! Контекст базы данных уже удален (так как загрузка выполнялась в отдельном потоке) и из-за отложенной загрузки Person.Address недоступен!

На этапе 3 человек загружается следующим образом:

using (DatabaseContext context = new DatabaseContext())
{
    Person person = from p in context.Persons.FirstOrDefault();
    return person;
}

Хорошо, я знаю, что (теоретически) я мог бы форсировать загрузку объекта Address двумя способами: 1) Используйте DbSet.Include, например:

context.Persons.Include("Address").FirstOrDefault();

2) Доступ к Person.Address, пока контекст базы данных еще жив, так как это приведет к загрузке адреса

Person person = context.Persons.FirstOrDefault();
Address address = person.Address;
return person;

Конечно, первое является предпочтительным решением, потому что оно не так страшно, как второе (просто получить доступ к свойству, чтобы принудительно загрузить данные, а затем отбросить результат, уродливо). Кроме того, в случае коллекции (например, список лиц), я должен был бы пройтись по коллекции и получить доступ к Адресу отдельно для каждого человека. Проблема с первым решением состоит в том, что только в DbSet есть метод Include, а во всех остальных коллекциях, возвращаемых по запросам, нет. Итак, допустим, у меня следующая структура базы данных

class Resource {}
class Person : Resource { public Address Address { get; set; } }
class Appointment { public IList<Resource> Resources { get; set; } }

и я хочу загрузить все конкретные встречи и включить адрес в каждый ресурс, который является человеком, у меня проблемы (или, по крайней мере, я не мог найти способ написать запрос для него). Это связано с тем, что context.Appointments.Resources не относится к типу DbSet, а представляет собой коллекцию ICollection, у которой нет метода Include. (Хорошо, возможно, в этом случае я мог бы написать запрос как-нибудь, начиная с context.Persons вместо context.Appointments, чтобы я мог использовать Include, но есть много сценариев, где это невозможно)


Так что в основном вопросы :

  1. Это правильный способ асинхронного доступа к базе данных?
  2. Как решить проблемы с ленивой загрузкой? Отключение отложенной загрузки не является решением (разве это может быть сделано только для определенных объектов?)

1 Ответ

2 голосов
/ 02 февраля 2012

Вы можете установить реализацию стратегии включения при запуске вашего приложения или теста.

Сначала определите метод Extension: Include, который определяет фактический включающий механизм (по умолчанию ничего не делая)

/// <summary>
/// Extension methods specifically for include since this is essential for DomainContext but not part of IQueryable by default
/// </summary>
public static class QueryableIncludeExtensions
{
    public static IIncluder Includer = new NullIncluder();

    public static IQueryable<T> Include<T, TProperty>(this IQueryable<T> source, Expression<Func<T, TProperty>> path)
         where T : class
    {
        return Includer.Include(source, path);
    }

    public interface IIncluder
    {
        IQueryable<T> Include<T, TProperty>(IQueryable<T> source, Expression<Func<T, TProperty>> path) where T : class;
    }

    internal class NullIncluder : IIncluder
    {
        public IQueryable<T> Include<T, TProperty>(IQueryable<T> source, Expression<Func<T, TProperty>> path)
             where T : class
        {
            return source;
        }
    }
}

Затем создайте реализацию EF, например:

internal class DbIncluder : QueryableIncludeExtensions.IIncluder
{
    public IQueryable<T> Include<T, TProperty>(IQueryable<T> source, Expression<Func<T, TProperty>> path)
        where T : class
    {
        return DbExtensions.Include(source, path);
    }
}

Наконец, подключите реализацию DbIncluder к проекту, для которого он нужен, в моем случае я сделал это следующим образом:

public class DomainContext : DbContext, IDomainContext
{
    static DomainContext()
    {
        // register the DbIncluder for making sure the right call to include is made (standard is null)
        QueryableIncludeExtensions.Includer = new DbIncluder();
    }

Теперь у IQueryable всегда есть метод расширения: Включить доступно. Вы можете расширить это до IEnumerable, если хотите. Фактическая реализация просто устанавливается QueryableIncludeExtensions.Includer

...