2/10/2008 9:57:00 PM

В субботу задумался, а как можно реализовать возможность объекта учавствовать в транзакциях? Чтобы можно было так же, как с БД: открыл транзакцию, понаизменял объект, потом решил: "не понравилось!" и откатил все изменения. А объект чтобы не пострадал.

Требования вырисовывались такие:

  1. До момента подтверждения транзакции все изменения, сделанные во время транзакции, должны быть видны только в рамках этой транзакции. За ее пределами объект должен выглядеть нетронутым.
  2. В случае вложенных транзакция (транзакция Б внутри транзакции А), если транзакция Б завершается, ее изменения становятся доступны для транзакции А, но по прежнему остаются невидимыми вне пределов транзакции А до ее фиксации.
  3. В случае отката транзакции объект остается в том же самом состоянии, как и до начала этой транзакции.
  4. Создавать такие объекты должно быть так же просто, как и "обычные" типы.

Задача показалась интересной, сходу решения в интернете не нашел, ну так оно и интереснее, стал делать сам.

Сначала решил было использовать механизм контекстов .NET Framework. Вроде, указал атрибутом контекст объекту - а дальше все будет делаться само. После пары часов возни с контекстами от этой идеи решил отказаться по двум причинам. Первая в том, что там объект должен наследоваться от ContextBoundObject, при этом для него, понятно, создаются Real и Transparent прокси, плюс рефлексия, короче, производительность. Вторую причину я уже не помню, что-то там толи уж очень сложно получалось, толи не получалось вовсе, уже не суть.

Пришлось стереть все и думать дальше.
В результате мысль довела до того, что я решил использовать майкрософтовский же подход, который они применили для реализации DependencyProperties.
Помните декларацию таких свойств?

// Using a DependencyProperty as the backing store for Name. This enables animation, styling, binding, etc...
public static readonly DependencyProperty NameProperty =
    DependencyProperty.Register("Name", typeof(string), typeof(MyClass), new UIPropertyMetadata(0));
 
public string Name
{
    get { return (string)GetValue(NameProperty); }
    set { SetValue(NameProperty, value); }
}

То, что надо! Разработчик только декларирует свойство, а то, какие уж там транзакции и как они учитывают ему уже не надо думать.

Получается примерно следующая конструкция:

public class Person : TransactionObject
{
    private static readonly TransactionProperty NameProperty
        = TransactionProperty.Register(typeof(Person), typeof(string));
 
    public string Name
    {
        get { return (string)base.GetValue(NameProperty); }
        set { base.SetValue(NameProperty, value); }
    }
}

Похоже, правда? :)
Наследуемся от TransactionObject (у Майкрософта DependencyObject) и регистрируем TransactionProperties (у Майкрософта DependencyProperties).

Далее, в .NET Framework есть такое пространство имен: System.Transactions, в котором уже реализованы классы Transaction, CommittableTransaction, TransactionScope и другие. В принципе, все это как раз и предназначено для поддержки транзакций, и было бы грех это не использовать. Правда, здесь меня ожидали два неприятных сюрприза: ни от одного из этих классов нельзя наследоваться - раз, и замечательный во всех отношениях TransactionScope ведет себя просто безобразно: когда объекту приходит команда Commit или Rollback, то совершенно невозможно определить, в рамках какой транзакции были вызваны эти самые Commit и Rollback.

Короче, пришлось делать собственный класс: ObjectTransactionScope, лишенный этого недостатка.
В результате использование транзакции для объекта типа Person выглядит следующим образом:

1. Person user = new Person();
2. user.Name = "Greg Johns";
3. Console.WriteLine("Initial value: " + user.Name);
4. Console.WriteLine();
5.  
6. using (ObjectTransactionScope tran1 = new ObjectTransactionScope(user))
7. {
8.     user.Name = "Henry Smith";
9.     Console.WriteLine("In transaction #1:\t" + user.Name);
10.     using (ObjectTransactionScope tran2 = new ObjectTransactionScope(user, TransactionScopeOption.RequiresNew))
11.     {
12.         user.Name = "Walter Simpson";
13.         Console.WriteLine("In transaction #2:\t" + user.Name);
14.         Console.WriteLine("\t(Transaction #2 is not committed)");
15.     }
16.     Console.WriteLine("In transaction #1:\t" + user.Name);
17.     Console.WriteLine("\t(Transaction #1 commit)");
18.     tran1.Complete();
19. }
20.  
21. Console.WriteLine();
22. Console.WriteLine("Finally:\t" + user.Name);

Здесь сначала мы работаем с экземпляром безо всяких транзакций (строки 2,3), затем открываем первую транзакцию и меняем имя пользователя (8). Если в этот момент кто-то обратится из другого потока к этому же объекту и прочитает свойство Name, он должен увидеть Greg Johns, а не Henry Smith, так как транзакция tran1 еще не была зафиксирована. Хотя "изнутри" этой транзакции будет "виден" именно Генри Смит (строка 9).

В строке 10 мы открываем новую транзакцию для объекта, причем требуем, чтобы была создана именно новая транзакция.
Снова меняем имя на Walter Simpson, поведение то же самое - изменение видно только внутри транзакции tran2.

В строке 15 транзакция tran2 заканчивается и, так как она не была подтверждена, происходит "откат" всех изменений (точнее сказать, изменения не фиксируются), поэтому после выхода из транзакции tran2 внутри tran1 мы продолжим видеть все того же Генри Смита.

Я знаком с Генри, он хороший парень, поэтому транзакцию tran1 фиксируем (строка 18).
Соответственно, после выхода из транзакции tran1 (строка 22) мы, уже вне всяких транзакций, увидим, что имя пользователя было изменено на Henry Smith. И все остальные эти изменения тоже увидят.

Кажется, достаточно просто и, главное, стандартно: регистрация свойств и обычное поведение из System.Transactions.

На реализацию механизма ушли суббота и воскресенье (если честно, то часа по 3-4 в день), прототип готов :)

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

А пока код проекта - в аттачменте.
Вопросы, предложения, баги и т.д. - в комменты или на почту ;)

Transactions.zip (48.77 kb)

Comments (8) -

2/13/2008 10:27:24 AM

Сергей Шишкин

Интересная статья, спасибо! Вот несколько замечаний к реализации:
1. Данный пример плохо поддерживает наследование. Метод GetProperties класса TransactionPropertiesRegistry должен возвращать свойства не только указанного типа, но и всех его предков.
2. Утечки памяти. Значения свойств хранятся в статическом реестре и никогда не удаляются. Было бы неплохо, после смерти объекта удалять значения его свойств.
3. В случае использования объекта в качестве ключа словаря стоит создавать словарь с указанием IEqualityComparer. По умолчанию словарь сравнивает ключи с помощью метода Equals, который может быть переопределен. В нашем же случае больше подойдет сравнение ссылок - ReferenceEquals.

Сергей Шишкин Germany

2/13/2008 10:50:25 AM

Сергей Шишкин

По поводу невозможности определения транзакции во время Commit или Rollback...
А как же статическое свойство System.Transactions.Transaction.Current? Мне все-таки непонятно, почему не подошел стандартный TransactionScope? Использование TransactionScope было бы привычнее для пользователей класса Person.

Сергей Шишкин Germany

2/13/2008 11:43:45 AM

Alexey Raga

Сергей, спасибо! Кое-что из этого я уже пофиксил Smile
По поводу компаратора не подумал, правда. Спасибо за замечание.

Чем не подходит стандартный TransactionScope?
Дело в том, что в случае его использования в методах Rollback и иже с ним System.Transactions.Transaction.Current уже указывает на родительскую транзакцию (ну, или содержит null если таковой нет), а не на текущую, с которой, собственно, и нужно работать.
А мне нужно было знать идентификаторы как текущей, так и предыдущей реализации.

Там еще одна проблема есть: фиксация транзакции не есть операция атомарная. То есть, я там бегу по всем свойствам и "фиксирую" их по очереди. И если в момент, когда "зафиксировано" 52 свойства из 1094-х..... ;)
Ну, грязное чтение возможно, понятно.
Пока не придумал, как красивее этого избежать Smile

Alexey Raga

2/15/2008 6:12:04 AM

Бергман

Так а разве есть какие-то сходства с БД?

Бергман Russia

2/15/2008 3:23:29 PM

zvenyka

Было интересно,спасибо

zvenyka

2/15/2008 9:08:08 PM

Alexey Raga

БД я упомянул только для того, чтобы показать, для чего нужны транзакции.

Alexey Raga

2/21/2008 12:40:00 AM

маразм

хм, почитаю сутра, сейчас не идет что-то

маразм

2/27/2008 3:32:20 PM

Дмитрий

Похожее поведение у библиотеки DataObjects, которая на самом деле является ORM системой, но механизм отката изменений свойств похожий.

Дмитрий Russia

Comments are closed

Powered by BlogEngine.NET 2.5.0.6

About the author

Alexey Raga Alexey Raga
.NET software developer.

E-mail me Send mail

Twitter


Recent posts

Archive

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2012

Sign in