NHibernate session management

Эту статью я хотел бы посвятить работе с NHibernate. Зачастую при обсуждении тех или иных реализаций object-relational mapper'ов много внимания уделяется статическим аспектам их работы, в основном возможностям и реализации O/R mapping'а. Однако, рано или поздно, mapping написан, и нужно перейти к решению поведенческих вопросов, в частности, осуществлению базовых CRUD операций.

В рамках двух статей планирую осветить такие вопросы.

  1. Понятие session и transaction scope.
  2. Подходы к выбору гранулярности session/transaction и их совместное использование.
  3. Дизайн и реализация helper class'ов для облегчения задач управления сессией. Понятие unit of work, применение такой feature NHibernate'а как "contextual session".
  4. Реализация паттерна Repository в контексте использования NHibernate.
  5. Возможности формирования запросов в NHibernate, и адаптация этих возможностей в реализации Repository паттерна.
Первые три вопроса будут рассмотрены в этом посте.

Session/transaction scope

При изучении NHibernate, практически во всех примерах, которые показывают как работать с persistent сущностями, можно увидеть такой код.

using(var session = SessionFactory.OpenSession())
using(var transaction = session.BeginTransaction())
{
    //do some work, get all orders from DB for example
    
    transaction.Commit();
}

За время обучения NHibernate'у, этот код настолько отпечатывается в мозгу, что воспринимаешь его как нечто данное по-умолчанию, и не задумываешься, что это всего лишь один из возможных подходов к управлению уровнем гранулярности сессий/транзакций.

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

ISession - a scope of object identity

NHibernate ISession определяет scope of object identity. Выглядит весьма запутано. Для непосвященного человека это определение отнюдь не расшифровывает термин ISession, а лишь вводит еще один термин scope of object identity. Scope of object identity - это условия, при которых .NET identity объекта (ReferenceEquals(a, b)) эквивалетно database identity (a.Id == b.Id).

К примеру, в базе данных есть запись в таблице Order. Database identity определяется первичным ключом таблицы. То есть, если сравнивать две записи, то они будут считаться идентичными, если у них одинаковые значения первичного ключа (a.Id == b.Id). Между прочим, реляционная модель не разрешает наличие двух записей с одним и тем же значением первичного ключа.

Если рассматривать .NET application domain, то здесь в качестве object identity выступает адрес объекта в памяти. Если для двух объектов выполняется условие ReferenceEquals(a, b), то такие объекты будут считаться идентичными.

Далее, рассмотрим такой сценарий. Приложение делает два запроса на получение заказа с ID равным 100.

using(var session = SessionFactory.OpenSession())
using(var transaction = session.BeginTransaction())
{
    var a = session.Get<Order>(100);
    var b = session.Get<Order>(100);
}

В данном случае приложение оперирует двумя объектами a и b, которые представляют одну и ту же запись в базе данных. Так вот, scope of object identity определяет границы, в которых a будет идентично b (ReferenceEquals(a, b)). В коде выше это условие будет выполнятся, так как оба запроса находятся в рамках одного и того же object identity scope, который декларируется при помощи класса ISession.

Если бы запросы делались в разных сессиях, то условие идентичности a и b не выполнялось бы, несмотря на то, что оба объекта представляют одну и ту же запись в таблице базы данных.

using(var session = SessionFactory.OpenSession())
using(var transaction = session.BeginTransaction())
{
    var a = session.Get<Order>(100);
}

using(var session = SessionFactory.OpenSession())
using(var transaction = session.BeginTransaction())
{
    var b = session.Get<Order>(100);
}

Assert.IsFalse(ReferenceEquals(a, b));

Различают такие подходы к определению scope of object identity:

  • no identity scope - нет никаких гарантий, что если два раза сделан запрос на получение одной строки из БД, то приложение будет оперировать идентичными объектами.
  • transaction-scoped identity, условие идентичности гарантируется в рамках транзакции (имеется ввиду не database transaction).
  • process-scoped identity, условие идентичности гарантируется в рамках всего приложения, application domain, если быть конкретней.

В NHibernate реализована transaction-scoped identity, правильней даже сказать, user-defined scoped identity. Работа с сессией осуществляется при помощи классов ISessionFactory, ISession.

ITransaction - a scope of database transaction

Scope ITransaction'а полностью соответсвует транзакции на уровне БД. То есть, начало новой транзакции session.BeginTransaction() приводит к началу новой транзакции на уровне базы данных. Соответсвенно, transaction.Commit() и transaction.Rollback() завершают транзакцию на уровне БД, открытую ранее.

Так как работа с ITransaction приводит к открытию транзакции на уровне БД, то нужно стремится, чтобы scope ITransaction был настолько мал, насколько это действительно нужно. Длинные транзакции - это нечто, чего следует избегать, так как это приводит к удержанию блокировок и неэффективному разделению ресурсов. Обычно transaction scope соответсвует выполнению операции либо группы операций по взаимодействию с базой (выборка заказов, последовательная работа с балансом двух счетов).

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

Введем еще одно понятие: Conversation или user transaction.

Conversation

Conversation, или user transaction - это некая единица работы с системой с точки зрения пользователя. Обычно она состоит из последовательности шагов, набора запросов к приложению, набора запросов по работе с БД. К примеру, это может быть процедура order placement, которая включает такие шаги: выбора единиц заказа, варианта доставки, варианта оплаты, и т.д. В рамках этих шагов приложению может быть необходимо осуществить N запросов к БД, и розумеется, в данном случае нельзя полагаться на database-level transaction для изоляции конкурентных conversation'ов.

Далее будут показаны различные варианты выбора уровня гранулярности session/transaction scope.

Level of session/transaction scope granularity

Session per request

Первый и самый простой вариант - это когда гранулярность transaction и session scope совпадают.

session-per-request

В данной ситуации нас не волнует понятие conversation, либо оно вообще не применимо. Зачастую этот подход применяется для обслуживания запросов приложения, например http request'a в ASP.NET приложении, либо запроса веб-сервиса, либо просто выполнение кода по взаимодействию с БД в консольном приложении.

Если в рамках session scope помимо взаимодействию с БД выполняется дополнительная работа, то ее длительность может стать неприемлимой для transaction scope, и мы прийдем к вариции session-per-request "1 session - N транзакций". Этот вариант наиболее ярко проявляется, если в рамках session scope, происходит несколько независимых запросов к БД, между которыми есть некие потенциально длительные вычисления (получение заказа, подсчет цен, сохранение цен вместе с измененным заказом).

session-per-request-with-multiple-trasactions

Session per request with detached objects

В этой ситуации в рассмотрение входит понятие conversation. К примеру, пользователь делает запрос на получение первых 10 заказов, далее редактирует их, и потом делает запрос на обновление. Сколько времени займет редактирование - неизвестно. Вопрос: "Какой должен быть scope сессии?" Должен ли он охватывать всю user transaction, начиная с момента выборки ордеров, и заканчиваю их обновлением, либо же для каждого отдельного шага должен создаваться отдельный session scope?

В данном подходе применяется последний вариант. А для того, чтобы обеспечить возможность повторного использования сущностей, которые были получены на первом этапе (получение первых 10 заказов) во время второго этапа (обновление заказов), применяется понятие NHibernate'а "detached objects". Это значит, что объекты, полученные в рамках одного session scope покидают его, становясь "detached", и далее, прикрепляются к другому session scope. Такие "detached" объекты могут быть переданы в другой layer приложения, возможно даже в другой application domain, либо вообще за рамки системы, и более того, возвращены обратно позже (Silverlight UI layer).

Session per request with detached objects

Session per conversation

Этот подход противоположен предыдущему, то есть session scope соответствует conversation scope. В интервале между запросами к приложению, session отсоединяется от database connection при помощи операции session.Disconnect(). Таким образом, экземпляр IDbConnection может быть возвращен в connection pool и повторно использован для обслуживания другого запроса в то время, пока пользователь, скажем, редактирует заказы. На мой взгляд, этот подход находит применение в довольно экзотических случаях и редко. Я бы не рекомендовал его исопользование без видимых на то причин.

Session-per-conversation

Ну а так как использовать database transaction для изоляции конкуретного доступа в рамках long-running conversation мы не можем, то в действие вступают механизмы optimistic locking'а. Хотя это уже тема для отдельной статьи.

Session management

Why do we need anything else?

Для задач управления сессиями/транзакциями используются классы ISessionFactory, ISession, ITransaction. Эти интерфейсы удобны, и можно было бы пользоваться ими напрямую. Однако, есть аргументы в пользу реализации некоторого слоя поверх этих интерфейсов, либо набора helper-class'ов, если слово "слой" звучит слишком серьезно. Вот некоторые из этих аргументов.

  1. Клиенты data-access layer приложения не должны знать об использовании NHibernate.

    Что касается меня, то я к этому отношусь довольно спокойно, и не вижу ничего криминального, если знания об использовании NHibernate вытекут наружу из data-access layer. Во-первых, скрыть полностью врятли удасться, во-вторых, это сложно, в-третьих, скорее всего ограничить себя и пожертвовать возможностью в полной мере использовать фичи NHibernate. Ну и самое главное: "А нужно ли это?". Подобный вопрос возникает, когда задаются целью скрыть знание о том, что данные сохраняются в БД, а не в файле к примеру. Нужно ответить на вопрос: "Насколько вероятно, что back-end storage поменяется с БД на файловую систему?". Скорее всего, 0.001%. Тоже самое касается механизма работы data-access layer. "Как часто меняется реализация persistence layer? Как часто переезжаем с dataset'ов на NHibernate, с NHibernate на data reader'ы?". На текущем проекте, к примеру, через 2 года после старта, переезжаем с Entity Framework на NHibernate. Не думаю, что стоит вкладывать усилия на скрытие этой информации. Изменения маловероятны. Более того, думаю, что цель ограничения масштаба возможных изменений все равно достигнута не будет.

  2. Объект ISession может использоваться в месте отличном от того, где он был создан.

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

  3. Вероятно использование Repository либо DAO, которым необходим объект ISessionFactory/ISession.
  4. Применяются dependency injection практики. Было бы неплохо иметь возможность делать constructor injection текущей открытой сессии там, где она необходима (в том же Repository/DAO), и дать возможность контейнеру разрешить зависимости автоматически.

Session management helper classes

Вот что получилось в итоге сделать.

IPersistenceContext

Предоставляет доступ к текущей открытой сессии при помощи свойства CurrentSession, а также объектам ISessionFactory, с помощью которой была создана сессия, ну и configuration'у всего persistence layer'а.

public interface IPersistenceContext
{
    Configuration       Configuration { get; }
    ISessionFactory     SessionFactory { get; }
    ISession            CurrentSession { get; }
}

Предполагается, что можно сделать constructor injection IPersistenceContext в те классы, где необходимо взаимодействие с persistent слоем, к примеру EntityRepository. Также IPersistenceContext может быть с легкостью подменен на fake object в тестах.

public class EntityRepository<T> : IEntityRepository<T>
{
    //constructor injection of IPersistenceContext
    public EntityRepository(IPersistenceContext persistenceContext)
    {
        ...
    }

    public T Get(object id)
    {
        return _persistenceContext.CurrentSession.Get<T>(id);
    }
    ...
}

Implementation of IPersistentContext

Реализация PersistentContext принимает Configuration, и в ее обязанности входит управление жизненным циклом ISessionFactory, а также предоставление доступа к текушей открытой сессии. Наверное, здесь - это единственный интересный момент.

public class PersistenceContext : IPersistenceContext, IDisposable
{
    private readonly Configuration _configuration;
    private readonly ISessionFactory _sessionFactory;
 
    public PersistenceContext(Configuration configuration)
    {
        _configuration = configuration;
        _sessionFactory = _configuration.BuildSessionFactory();
    }

    public Configuration Configuration { get { return _configuration; }}
    public ISessionFactory SessionFactory { get { return _sessionFactory; } }

    public ISession CurrentSession
    {
        get
        {
            if (!CurrentSessionContext.HasBind(SessionFactory))
            {
                OnContextualSessionIsNotFound();
            }
            var contextualSession = SessionFactory.GetCurrentSession();
            if (contextualSession == null)
            {
                OnContextualSessionIsNotFound();
            }
            return contextualSession; 
        }
    }

    public void Dispose()
    {
     SessionFactory.Dispose();
    }

    private static void OnContextualSessionIsNotFound()
    {
        throw new InvalidOperationException("Ambient instance of contextual session is not found. Open the db session before.");
    }
}

Используется feature NHibernate'а под названием contextual session. Таким образом, все, что ожидает этот класс, это то, что текущая открытая сессия будет ассоциирована с неким контекстом и доступна через ISessionFactory.GetCurrentSession().

Использование CallSessionContext в качестве стратегии хранения текущей открытой сессии.

Доступны такие стратегии места хранения текущей сессии.

  • ManagedWebSessionContext, WebSessionContext, текущая сессия хранится в HttpContext.
  • CallSessionContext, текущая сессия хранится в CallContext.
  • ThreadStaticSessionContext, текущая сессия хранится в thread static.

Из различных вариантов выбора контекста сессии остановился на использовании call context. Call context предоставляет слоты, где можно сохранить объекты, которые будут доступны ниже по call stack, а также дочерним потокам. Более подробно o CallContext здесь. Из ограничений использования этого подхода, это то, что нельзя делать вложенные session scope, но думаю это и не надо.

Конфигурация выбора нужной реализации ICurrentSessionContext проста. Вот как это выглядит при использовании Fluent NHibernate.

Fluently.Configure().Database(MsSqlConfiguration.MsSql2005
    .ConnectionString("bar")
    .CurrentSessionContext(typeof(CallSessionContext).FullName))

Или с помощью классической конфигурации NHibernate:

var cfg = new Configuration();
cfg.Properties[Environment.ProxyFactoryFactoryClass] = typeof(ProxyFactoryFactory).AssemblyQualifiedName;
cfg.Properties[Environment.CurrentSessionContextClass] = typeof(CallSessionContext).FullName;

Демаркация unit of work'а

Последнее, что осталось сделать - это обозначить границы session scope, то есть наш unit of work. Просто создать новый ISession c помощью ISessionFactory.OpenSession() по-старинке недостаточно. Нужно еще связать созданную сессию с CurrentSessionContext.

public class DbSession : IDisposable
{
    private readonly ISessionFactory _sessionFactory;

    public DbSession(IPersistentContext persistentContext)
    {
        _sessionFactory = persistentContext.SessionFactory;
        CurrentSessionContext.Bind(_sessionFactory.OpenSession());
    }
 
    public void Dispose()
    {
        var session = CurrentSessionContext.Unbind(_sessionFactory);
        if (session != null && session.IsOpen)
        {
            try
            {
                if (session.Transaction != null && session.Transaction.IsActive)
                {
                    session.Transaction.Rollback();
                }
            }
            finally
            {
                session.Dispose();
            }
        }
    }
}

Основная обязанность класса это управление жизненным циклом сессии, привязывание/отвязывание текущей сессии от CurrentSessionContext. Класс не знает о выбранной стратегии реализации ICurrentSessionContext. Получается, что DbSession и PersistenceContext напрямую друг о друге ничего не знают, но знают куда положить и где забрать текущую сессию, вся коммуникация осуществляется через CurrentSessionContext.

Usability/testability

Имея такие сервисы как ISessionFactory, IPersistenceContext, IEntityRepository становится просто тестировать приложение, подменяя зависимости на fake в тестах. При использовании DI/IoC контейнера, благодаря использованию constructor injection, становится возможно зарегистрировать все сервисы в контейнере и дать ему возможность автоматически разрешить зависимости.

Вот как выглядит использование описанных выше коснтрукций в различных сценариях.


// this routine is usually executed only one per application run
// configure persistent layer and create session factory
Configuration configuration = ConfigurePersistenceLayer();
IPersistentContext persistentContext = new PersistentContext(configuration);

// somewhere in application
// new DbSession opens new NH ISession and attach to CurrentSessionContext
using (new DbSession(persistenceContext))
{
     // repository usage
     var orderRepository = new EntityRepository<Order>(persistenceContext);
     var order = orderRepository.Get(1);
        
     // usage of transaction with scope different from session scope
     using(var transaction = persistenceContext.CurrentSession.BeginTransaction())
     {
         // direct using of current session
         var allOrders = persistenceContext.CurrentSession.Query<Order>().ToList();
          
         transaction.Rollback();
     }

     // do not use current session, create new stateless session
     var statelessSession = persistenceContext.SessionFactory.OpenStatelessSession();
}

// direct using session out of DbSession scope through SessionFactory
var session = persistenceContext.SessionFactory.OpenSession();

Resources

To be continued...

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

  • Реализация паттерна Repository в контексте использования NHibernate.
  • Возможности формирования запросов в NHibernate, и адаптация этих возможностей для реализации Repository паттерна.

13 comments:

ShurikEv said...

Можно узнать по каким причинам вас не устроил EF? Какую версию использовали?

Alexey Samoshkin said...

Проект на .net 3.5. EF версии 1.0. Бегло смотрел на EF 4.0, но его не использовал. Поэтому сравнить EF 4.0 и NH не могу. Обозначу лишь наиболее критичные вещи и те преимущества NH, которые я наиболее сильно почувствовал.


1) Transparent persistence
Возможность использовать POCO, которые не знают о том, что будут сохранены. Довольно мало правил, которые NH накладывает на дизайн POCO объектов. Многих смущает необходимость помечать методы и свойства как virtual, но меня это особо не утомляет. В EF - это designer generated classes, которые наследуются от EntityObject, и дабы не тянуть за собой EF по всем слоям приложения, приходится, помимо O/R меппинга, затем делать O/O меппинг на свои POCO объекты.

2) Rich mapping features
В NH это полная поддержка entity и value types, развитый меппинг коллекций (bag, set, list, dictionary, collection of components), множество identity generation стратегий (identity, assigned, guid, guid.comb, hilo), поддержка составных ключей (особенно важно для legacy схем БД), возможность сделать однонаправленную ассоциацию (в EF по умолчанию все ассоциаци двунаправленные, и сделать однонаправленную можно вручную правя *.edmx и удалив NavigationProperty, но это hack), наличие extension point'ов меппинга (например, POCO может содержать свойство класса Money, на базе данных это две колонки Currency + Amount, и тип-посредник для меппинга MoneyUserType)

3) Удобство описания меппинга.
Если использовать FluentNhibernate, то меппинг писать становится даже приятно. В EF 1.0 - это огромный xml *.emdx файл, в который в любом случае приходится залазить, так как возможностей дизайнера не хватает для сложных случаев.

4) Ужасные, огромные и нечитабельные sql запросы, которые генерит EF.
Запросы NH процентов на 80% похожи на те, что я бы писал вручную.

5) Querying features
LINQ-to-entities - да, это классная фишка EF, но LINQ по своей природе скрывает работу с базой, и когда нужен больший контроль над sql запросом, то LINQ не хватает. В EF это значит пересесть на хранимки, либо использовать e-sql. Описывать запрос в нетипизированном виде как строку, которая представляет собственный EF query dialect - это последнее, чего бы я хотел.
Здесь более подробно о возможностях строить запросы в NH

6) Большое количество extension point'ов
У NH огромное количество extension point'ов, которые позволяют влиять чуть ли не на все аспекты его работы. К примеру, Interceptor and events.

7) Testability
POCO объекты легко создавать в тестах. Основные классы-фасады NH (ISession, ISessionFactory) - это интерфейсы, а значит их можно подменить в тестах.

8) Direct sql using
Возможность напрямую использовать sql, а результаты его выполнения, к примеру, смеппить на POCO. В EF 1.0 - это довольно сложно.

Ну и другие возможности, такие как transitive persistence, bathing support, optimistic locking mechanizm, second-level cache, которые в EF или довольны бедны или отсуствуют.

Если интересно, то у Ayende Rahien есть статья, в которой он сравнивает EF 4.0 и NH.

тми said...

отличный пост!

пишите еще!

Anonymous said...

Очень интересный материал, видно, что вы серьезно подготовились. Хотелось бы увидеть пример использования с коллекциями.
Т.е. пусть у нас будет entity Bookshelf, содержащий
IList Books.
К примеру, нам нужно получить все элементы Bookshelf, пройтись по списку выбирая те из них, у которых Books.Count > 0. При этом в процессе нам нужно будет сделать запись, к примеру в тот же Bookshelf:

using (new DbSession(persistenceContext)){
foreach(Bookshelf bs in EntityRepository.GetAll()){
if (bs.Books.Count > 0){
//do some work 1
} else {
Bookshelf newBs = new Bookshelf();
EntityRepository.Save(newBs);
}
}
}

Интересует как вы реализовывали методы в репозитории? Ведь использовать вложенные Session нельзя из-за CallSessionContext, а ведь бывают случаи, когда ведь просто нужно засейвить элемент. Или вы всегда пишете вручную дополнительный scope для сессии для атомарных операций?

Alexey Samoshkin said...

Да, верно, использование CallSessionContext ограничивает нас в возможности использования вложенных ISession. Однако до сих пор, там, где мне сперва казалось, что есть необходимость во вложенных сессиях, позже я понимал, что либо вложенные сессии вовсе не нужны, либо находил work-around.

Что касается вашего примера, то здесь вложенные сессии также не требуются. Для этого надо показать, как реализован репозиторий (хотел написать отдельный пост, посвященный как раз этой теме).
==============================================
public interface IEntityRepository : IQueryable
{
IEnumerable Items { get; }

T Get(object id);
T Load(object id);
void Add(T entity);
void Remove(T entity);

void Clear(bool all);

IEntityQuery SubmitQuery(ICriterion criterion);
}
==============================================

Репозиторий в данном случае - представляет собой коллекцию элементов, загруженных в first level cache. + возможность осуществлять запросы. Методы Load/Get/Add/Remove - всего лишь прокси для работы с ISession.
==============================================
public T Get(object id)
{
return _persistenceContext.CurrentSession.Get(id);
}

public void Add(T entity)
{
_persistenceContext.CurrentSession.SaveOrUpdate(entity);
}
==============================================

В качестве ISession, с которой работает Repository, используется текущая открытая сессия, доступная через IPersistenceContext.CurrentSession, который в свою очередь передается в Repository через constructor injection.
==============================================
public EntityRepository(IPersistenceContext persistenceContext)
{
_persistenceContext = persistenceContext;
}

protected ISession CurrentSession
{
get { return persistenceContext.CurrentSession;
}
==============================================

Для вашего примера это значит следующее:
==============================================
// открыли новую ISession, IPersisteceContext.CurrentSession теперь ссылается на нее
using (new DbSession(persistenceContext))
using (var transaction = persistenceContext.CurrentSession.BeginTransaction())
{
// создали репозиторий, передали ему IPersistenceContext;
// по-хорошему будем использовать контейнер, и IPersistenceContext и Repository будут в нем зарегистрированы
var bookShelfRepository = new EntityRepository(persistenceContext);

// Итерируем репозиторий. Можем это сделать, так как он IQueryable; будет использован LINQ-to-NH провайдер
foreach(Bookshelf bs in bookShelfRepository)
{
//если мало книг, добавим парочку
if (bs.Books.Count < 5){
bs.Books.Add(new Book("NHibernate in Action"));
bs.Books.Add(new Book("Domain Driven Design"));
}
}

// и добавим еще один BookShelf без книг
repository.Add(new BookShelf("For developers"));

// коммитим тразакцию, сохраняем изменения.
transaction.Commit();
}
============================================

1) Книги добавлялись в коллекцию persistent сущности Bookshelf, находяшейся в текущей ISession. То есть если в меппинге коллекции указано каскадирование(cascade="save-update"), то репозиторий вообще не надо использовать. Новые книги будут автоматически сохранены. Если нет, то используем repository.Add(new Book(...)), и потом эту книгу еще добавим и в коллекцию Bookshelf.Books.

2) Для добавления нового bookshelf'a используем repository.Add(...). Это равносильно прямому использованию IPersistenceContext.CurrentSession.SaveUpdate(). Как видно, используется текущая открытая сессия, и никаких вложенных сессий не требуется.

Вообще, благодаря наличию automatic dirty checking и transitive persistence, NHibernate делает много работы по отслеживанию изменений, поэтому репозиторий используется в основном для осуществления запросов. Добавление/обновление данных по большей части происходит автоматически во время session flushing.

Anonymous said...

Очень интересно было бы посмотреть на реализацию IQueryable в EntityRepository. Можете выложить куда-то исходник, либо в статье рассмотрите (в комментариях теги летят :-( ).

Anonymous said...

Интересно как вы обрабатываете ситуацию работы с БД на форме? Т.е. Пусть у нас есть 2 связанные сущности и две формы, имеющие гриды для редактирования этих сущностей. Где границы ответственности сессии? Все приложение, форма, либо конкретные методы, отвечающие за сохранение/чтение данных?

Также интересно было бы увидеть обзор сравнения различных типов маппинга коллекций AsBag, AsList, AsSet, ... и соответствие коллекций .NET (IList, IQuerable, ICollection, ...) типу маппинга.

Также, интересует, возможно вы сталкивались с ошибкой "Illegal attempt to associate a collection with two open sessions" при работе с ManyToMany?

Alexey Samoshkin said...

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

Опубликую только не раньше выходных. (6/5/2011).

Anonymous said...

Ну и где же статья?

Anonymous said...

Будет ли продолжение статьи? (вопросы 4 и 5)

Anonymous said...

Просим, просим!!

Anonymous said...

Статью! Статью!

Anonymous said...

Sorry - this looks very interesting; I wish it was in English!

Post a Comment