пятница, 17 мая 2013 г.

How to persist aggregate root. Part I

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

    Рассмотрим первый, возможно наиболее часто используемый вариант - сохранение в реляционную базу данных. Можно сохранять сущности руками, а можно и с помощью ORM, в моем примере NHibernate, что мы и будем делать. А вот, кстати, и наши сущности:

    public class Order
    {
        public Order(int customerId)
        {
            CustomerId = customerId;
 
            OrderLines = new List<OrderLine>();
        }
 
        private Order()
        {
        }
 
        public int Id { getprivate set; }
 
        public int Version { getprivate set; }
 
        public int CustomerId { getprivate set; }
 
        public double Total { getprivate set; }
 
        public IList<OrderLine> OrderLines { getprivate set; }
 
        public void BuyProduct(string productId, int quantity, double price)
        {
            OrderLines.Add(new OrderLine(this, productId, quantity, price));
 
            Total += quantity * price;
        }
    }
 
    public class OrderLine
    {
        public OrderLine(Order order, string productId, int quantity, double price)
        {
            Order = order;
            ProductId = productId;
            Quantity = quantity;
            Price = price;
        }
 
        private OrderLine()
        {
        }
 
        public int Id { getprivate set; }
 
        public Order Order { getprivate set; }
 
        public string ProductId { getprivate set; }
 
        public int Quantity { getprivate set; }
 
        public double Price { getprivate set; }
    }

Весь код примера доступен здесь.

    Так как многие ORM поддерживают замечательную, на первый взгляд, возможность отложенной загрузки дочерней коллекции (lazy-load), то мы будем считать свойство Total на самом Order, например для случаев когда нам нужен для вывода только Total и не нужны данные с OrderItems. В случае "развесистых" AR с большим количеством вложенных коллекций на нескольких уровнях использование lazy-load с точки зрения производительности просто напрашивается.

    Тут то и спрятан подвох! Но прежде немного теории. AR это прежде всего consistency boundary. Именно исходя из соображений целостности нужно решать объединять ли 100500 сущностей под одним корнем или делать 100500 независимых AR или какое то промежуточное решение. Тема определения границ AR это отдельная сложная тема, достаточно хорошо изложена например здесь: Effective Aggregate Design. Таким образом мы должны и сохранять и загружать AR целостно, но так ли это в нашем случае? Нет, не так!

    Загрузим order, выведем в консоль значение поля Total, а затем пересчитаем это значение уже по коллекции объектов OrderLines. Но предположим что между этими двумя действиями кто то обновил order. Однако нас это затронуть не должно, у нас же consistency boundary, не так ли?

       using (var session = _sessionFactory.OpenSession())
       using (var transaction = session.BeginTransaction())
       {
           var order = session.Get<Order>(orderId);
 
           Console.WriteLine(order.Total);
 
           ConcurrentWriter(orderId);
 
           // Here collection is actually loaded
           Console.WriteLine(order.OrderLines.Sum(oi => oi.Quantity * oi.Price));
 
           transaction.Commit();
       }

       private void ConcurrentWriter(int orderId)
       {
           var thread = new Thread(() =>
           {
               using (var session = _sessionFactory.OpenSession())
               using (var transaction = session.BeginTransaction())
               {
                   var order = session.Get<Order>(orderId);
 
                   order.BuyProduct("Whiskey", 1, 10.12);
 
                   transaction.Commit();
               }
           });
 
           thread.Start();
 
           // give it chance to complete
           thread.Join(1000);
       }


    В результате мы видим в консоли два разных числа! При отложенной загрузке коллекции в мы загрузили не только уже имеющиеся OrderLines, но и еще то что добавил наш concurrent writer. А как же целостность данных?!

    Есть два известных мне способа "вернуть" целостность. Первый способ - открывать транзакцию с уровнем изоляции RepeatableRead. Но тогда нужно быть готовым к исключениям в точке обращения к коллекции OrderLines, потому что нашу транзакцию будут выбирать жертвой разрешения deadlock'а. Второй способ - отказаться от lazy и загружать весь AR целиком.

    Итог: использование реляционной базы данных для хранения AR это удобный в плане использования (ORM с их "плюшками", развитый инструментарий для самих СУБД) и быстроты реализации способ, имеющий "некоторые" проблемы с целостностью, если о них не задумываться. Для нагруженных решений и "развесистых" AR к недостаткам можно так же отнести необходимость нескольких дисковых операций для загрузки одного AR.

    В следующий раз (если он будет) попробуем хранить наш AR в документной базе.

Комментариев нет:

Отправить комментарий

Wider Two Column Modification courtesy of The Blogger Guide