Шаблон EF и репозитория - заканчивающийся несколькими DbContexts в одном контроллере - какие-либо проблемы (производительность, целостность данных) - PullRequest
8 голосов
/ 04 февраля 2012

Большая часть моих знаний о ASP.NET MVC 3 основана на чтении книги Адама Фримена и Стивена Сендерсона Pro Framework ASP.NET MVC 3 Framework.Для моего тестового приложения я старался очень внимательно придерживаться их примеров.Я использую шаблон репозитория плюс Ninject и Moq, что означает, что модульное тестирование работает достаточно хорошо (т.е. без необходимости извлекать данные из базы данных).

В книге репозитории используются так:

public class EFDbTestChildRepository
{
    private EFDbContext context = new EFDbContext();

    public IQueryable<TestChild> TestChildren
    {
        get { return context.TestChildren; }
    }

    public void SaveTestChild(TestChild testChild)
    {
        if (testChild.TestChildID == 0)
        {
            context.TestChildren.Add(testChild);
        }
        else
        {
            context.Entry(testChild).State = EntityState.Modified;
        }
        context.SaveChanges();
    }
}

И вот DbContext, который идет с ним:

public class EFDbContext : DbContext
{
    public DbSet<TestParent> TestParents { get; set; }
    public DbSet<TestChild> TestChildren { get; set; }
}

Обратите внимание: для простоты в этом извлеченном примере я оставил здесь интерфейс ITestChildRepository, который затем будет использовать Ninject.

В других источниках я видел более общий подход к хранилищу, когда одного хранилища достаточно для всего приложения.Очевидно, что в моем случае я получаю довольно большой список репозиториев в своем приложении - в основном по одному для каждой сущности в моей доменной модели.Не уверен насчет плюсов и минусов в отношении двух подходов - я просто следовал книге, чтобы быть в безопасности.

Чтобы окончательно добраться до моего вопроса: каждый репозиторий имеет свой собственный DbContext - private EFDbContext context = new EFDbContext();.Рискну ли я в конечном итоге с несколькими DbContexts в одном запросе?И приведет ли это к значительным потерям производительности?Как насчет вероятности конфликтов между контекстами и каких-либо последствий для целостности данных?

Вот пример, в котором я получил более одного хранилища в контроллере.

Мои две таблицы базы данныхсвязаны с отношением внешнего ключа.Классы модели моего домена:

public class TestParent
{
    public int TestParentID { get; set; }
    public string Name { get; set; }
    public string Comment { get; set; }

    public virtual ICollection<TestChild> TestChildren { get; set; }
}

public class TestChild
{
    public int TestChildID { get; set; }
    public int TestParentID { get; set; }
    public string Name { get; set; }
    public string Comment { get; set; }

    public virtual TestParent TestParent { get; set; }
}

Веб-приложение содержит страницу, позволяющую пользователю создать новый TestChild.На нем есть поле выбора, содержащее список доступных TestParent для выбора.Вот как выглядит мой контроллер:

public class ChildController : Controller
{
    private EFDbTestParentRepository testParentRepository = new EFDbTestParentRepository();
    private EFDbTestChildRepository testChildRepository = new EFDbTestChildRepository();

    public ActionResult List()
    {
        return View(testChildRepository.TestChildren);
    }

    public ViewResult Edit(int testChildID)
    {
        ChildViewModel cvm = new ChildViewModel();
        cvm.TestChild = testChildRepository.TestChildren.First(tc => tc.TestChildID == testChildID);
        cvm.TestParents = testParentRepository.TestParents;
        return View(cvm);
    }

    public ViewResult Create()
    {
        ChildViewModel cvm = new ChildViewModel();
        cvm.TestChild = new TestChild();
        cvm.TestParents = testParentRepository.TestParents;
        return View("Edit", cvm);
    }

    [HttpPost]
    public ActionResult Edit(TestChild testChild)
    {
        try
        {
            if (ModelState.IsValid)
            {
                testChildRepository.SaveTestChild(testChild);
                TempData["message"] = string.Format("Changes to test child have been saved: {0} (ID = {1})",
                                                        testChild.Name,
                                                        testChild.TestChildID);
                return RedirectToAction("List");
            }
        }
        catch (DataException)
        {
            //Log the error (add a variable name after DataException)
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
        }

        // something wrong with the data values
        return View(testChild);
    }
}

Недостаточно иметь доступный EFDbTestChildRepository, но мне также нужен EFDbTestParentRepository.Они оба назначены частным переменным контроллера - и вуаля, мне кажется, что два DbContexts были созданы.Или это не правильно?

Чтобы избежать проблемы, я пытался использовать EFDbTestChildRepository, чтобы добраться до TestParents.Но это, очевидно, вызовет только те, которые уже подключены хотя бы к одному TestChild - так что не то, что я хочу.

Вот код для модели представления:

public class ChildViewModel
{
    public TestChild TestChild { get; set; }
    public IQueryable<TestParent> TestParents { get; set; }
}

Пожалуйстадайте мне знать, если я забыл включить соответствующий код.Большое спасибо за ваш совет!

Ответы [ 3 ]

5 голосов
/ 04 февраля 2012

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

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

var parent = parentRepo.TestParents.Single(p => p.Id == 1);
var child = new Child { TestParent = parent };
childrenRepo.SaveTestChild(child);

Этот простой код не будет работать, поскольку parent уже присоединен кконтекст внутри parentRepo, но childrenRepo.SaveTestChild попытается присоединить его к контексту внутри childrenRepo, что вызовет исключение, поскольку объект не должен быть присоединен к другому контексту.(Здесь на самом деле обходной путь, потому что вы могли бы установить свойство FK вместо загрузки parent: child.TestParentID = 1. Но без свойства FK это было бы проблемой.)

Как решить такую ​​проблему?

Одним из подходов может быть расширение EFDbTestChildRepository новым свойством:

public IQueryable<TestParent> TestParents
{
    get { return context.TestParents; }
}

В приведенном выше примере кода вы можете использовать только один репозиторий, и код будет работать.Но, как вы можете видеть, имя «EFDbTest Child Repository» больше не соответствует цели нового репозитория.Теперь это должен быть «EFDbTest ParentAndChild Repository».

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

Альтернативное решение состоит в том, чтобы внедрить контекст в репозитории (а не создавать его в репозиториях), чтобы убедиться, что каждый репозиторий используеттот же контекст.(Контекст часто абстрагируется в интерфейс IUnitOfWork.) Пример:

public class MyController : Controller
{
    private readonly MyContext _context;
    public MyController()
    {
        _context = new MyContext();
    }

    public ActionResult SomeAction(...)
    {
        var parentRepo = new EFDbTestParentRepository(_context);
        var childRepo = new EFDbTestChildRepository(_context);

        //...
    }

    protected override void Dispose(bool disposing)
    {
        _context.Dispose();
        base.Dispose(disposing);
    }
}

Это дает вам один контекст на контроллер, который вы можете использовать в нескольких репозиториях.

Следующий шаг можетсоздать отдельный контекст для каждого запроса путем внедрения зависимостей, например ...

private readonly MyContext _context;
public MyController(MyContext context)
{
    _context = context;
}

... и затем сконфигурировать контейнер IOC для создания одного экземпляра контекста, который может быть внедрен в несколько контроллеров.

2 голосов
/ 11 февраля 2014

Как и обещал, я выложу свое решение.

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

Я нашел этот превосходный блог: Правильное использование шаблонов репозитория и единиц работы в ASP.NET MVC , которое ведет меня в правильном направлении. Редизайн основан на шаблоне UnitOfWork. Это позволяет мне иметь только один параметр конструктора для всех моих контроллеров вместо «бесконечных параметров конструктора». И после этого я смог также внедрить упреждающее кэширование, которое решило большую часть ранее упомянутых проблем, с которыми я столкнулся.

Теперь у меня есть только эти классы:

  • IUnitOfWork
  • EFUnitOfWork
  • IGenericRepository
  • EFGenericRepository

См. Упомянутый блог для полной информации и реализации этих классов. Просто в качестве примера, IUnitOfWork содержит определения репозитория для всех сущностей, которые мне нужны, например:

namespace MyWebApp.Domain.Abstract
{
  public interface IUnitOfWork : IDisposable
  {
    IGenericRepository<AAAAA> AAAAARepository { get; }
    IGenericRepository<BBBBB> BBBBBRepository { get; }
    IGenericRepository<CCCCC> CCCCCRepository { get; }
    IGenericRepository<DDDDD> DDDDDRepository { get; } 
    // etc.

    string Commit();
  }
}

Внедрение зависимостей (DI) - это всего лишь одно утверждение (я использую Ninject):

ninjectKernel.Bind<IUnitOfWork>().To<EFUnitOfWork>();

Контроллеры-конструкторы ремонтопригодны:

public class MyController : BaseController
{
  private MyModel mdl = new MyModel();

  private IUnitOfWork _context; 

  public MyController(IUnitOfWork unitOfWork)
  {
    _context = unitOfWork;

    // intialize whatever needs to be exposed to the View:
    mdl.whatever = unitOfWork.SomeRepository.AsQueryable(); 
  }

 // etc.

В контроллере я могу использовать _context для доступа ко всем репозиториям, если это необходимо. Приятной частью этого является то, что ему требуется всего лишь один метод Commit () - вызов для сохранения измененных данных для всех репозиториев:

_context.Commit();
2 голосов
/ 04 февраля 2012

Рискну ли я получить несколько DbContexts в одном запросе?

Да.Каждый экземпляр репозитория собирается создавать свои собственные экземпляры DbContexts.В зависимости от размера и использования приложения, это может не быть проблемой, хотя это не очень масштабируемый подход.Есть несколько способов справиться с этим, хотя.В моих веб-проектах я добавляю DbContext (s) в коллекцию Context.Item запроса, таким образом, он доступен всем классам, которым это требуется.Я использую Autofac (аналогично Ninject) для управления тем, какие DbContexts создаются в определенных сценариях и как они хранятся, например, у меня есть другой «менеджер сеансов» для контекста WCF и тот, который используется для контекста Http.

И не приведет ли это к значительному снижению производительности?

Да, но опять же не массово, если приложение относительно небольшое.Однако по мере роста вы можете заметить накладные расходы.

Как насчет вероятности конфликтов между контекстами и любых последствий для целостности данных?

Одна из причиндля использования ORM, подобного этому, необходимо сохранить изменения в DbContext.Если вы создаете несколько экземпляров контекста для каждого запроса, вы теряете это преимущество.Вы не заметите конфликтов или какого-либо влияния целостности как таковой, если не будете обрабатывать много обновлений асинхронно.

...