Когда в сети говорят об ORM в .net — очень часто в качестве сферы применения «по умолчанию» рассматривается ASP.Net (и MVC в частности).
Происходит это, как мне кажется, потому, что именно веб — среда наиболее динамичная, открытая и быстро подхватывающая новые веяния, вследствие полной свободы в технических решениях. В случае, к примеру, Win Forms приложений переход со второго фреймворка на 4-ый — сложнейший шаг (апдейт должен затронуть всех пользователей, а у некоторых, может быть, еще win2000 :)), а для ASP.Net разработчиков проапгрейдить сервер, обычно, не проблема.
Еще одна причина может быть в том, что «принципы работы» в случае веба типичны, и рабочий цикл мал — пришел запрос, сгенерили ответ, отдали клиенту, вернулись в начальное состояние. Следовательно и подходы, применяемые одной компанией, легко адаптируются и в другой.
Desktop-приложения же часто очень велики, с длинным циклом разработки и, обычно, с устоявшимися внутри компании методиками работы, которые не всегда легко переносимы.
В этом посте я хотел бы рассказать об одной из специфических трудностей, с которыми можно столкнуться при работе с ORM в настольных приложениях, а именно: управление контекстом/сессией.
Корень проблемы прост: если в web наиболее простой и одновременно удобной практикой считается использование session-per-request, когда все запросы к БД в рамках ответа на пользовательский запрос, идут в общей сессии. Это даёт уверенность, что все объекты, с которыми идет работа получены именно из этой сессии и ситуация, в которой у вас есть два доменных объекта из двух разных сессий фактически невозможна.
В настольных приложениях аналогичного «request» просто не существует. Распространенной практикой является session-per-presenter(/viewmodel), но и этот принцип может столкнуться с проблемой в реализации.
Допустим, к примеру, мы используем репозитории:
public class SomeViewModel { //как обеспечить общую сессию в двух репозиториях? - приходится SomeViewModel создавать через фабрику. public SomeViewModel (IRepository<User> userRepo, IRepository<Order> orderRepo) {} //можно использовать фабрику репозиториев, которая будет во все созданные репозитории подсовывать одну и ту же сессию. public SomeViewModel (IRepositoryFactory repoFactory) { _userRepo = repoFactory.Resolve<User>(); _orderRepo = repoFactory.Resolve<Order>(); } //а можно использовать юнитОфВорк из <a href="http://www.arturdr.ru/net/sloy-dostupa-k-dannyim-v-net-prilozheniyah/">предыдущего поста</a>, тогда заморачиваться с фабриками не придется public SomeViewModel (IUnitOfWork unitOfWork) { } }
С репозиториями разобрались, чуть сложнее с сервисами, если мы используем анемичную доменную модель.
public class UserService { public void ProcessOrder(User user, Order order) { } }
Проблема: далеко не факт, что пришедший нам в эту функцию user и order запрошены из одного и того же контекста. Проблема вторая — непонятно, должен ли этот сервис самостоятельно сохранить данные в БД (сделать commit у сессии) или нет.
Неплохим решением в данном случае видится активное использование DDD, при котором число таких сервисов должно сократиться до минимума. Оставшиеся сервисы будут попадать в одну из следующих категорий:
- Сервисы, проводящие некоторые длительные\ресурсоемкие операции. Такие сервисы, очевидно, должны работать с собственными Сессиями и самостоятельно их коммитить. Для окончательного разрушения двусмысленности такие сервисы могут принимать не сущности User или Order, а их идентификаторы (User.Id и Order.Id).
- Сервисные операции, завязанные на объемную дополнительную выборку из БД, которые не получилось сделать частью доменной модели. Такие сервисы должны наряду с прочими аргументами принимать UnitOfWork, из которого они и будут осуществлять это дополнительную выборку. Таким образом все запрошенные сущности будут гарантированно получены из одной сессии.
Еще одно проявление той же проблемы — обмен данными между различными вьюшками приложения. Очевидно, что сессия у каждой вьюшки своя, и при взаимодействии вьюшек весьма вероятно перемешивание сущностей из разных сессий в рамках одной вьюшки.
Собственно, решается эта проблема аналогично проблемам с сервисами — вьюшки между собой должны обмениваться не сущностями, а их идентификаторами (эмулируя data transfer object — DTO), либо, если вьюшки очень тесно связаны, например, одна является частью другой, они должны работать с одной сессией (инъектированной в конструкторе).