воскресенье, февраля 26, 2006

История изменения

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

Предусловия.
1. Ничто не попадает в хранилище иначем, чем через адаптер.
2. Допустимо использование контекстов

Тогда.
Всю эту работу можно сделать довольно аккуратно при помощи атрибутов и контекстов.


[AttributeUsage(AttributeTargets.Class)]
public sealed class ReplicationTracingAttribute: ContextAttribute
{
public ReplicationTracingAttribute(): base("ReplicationTracing")
{
}
public override void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg)
{
ctorMsg.ContextProperties.Add(new ReplicationTracingProperty());
}
}

при этом
ReplicationTracingProperty
выглядит так:

public class ReplicationTracingProperty: IContextProperty, IContributeObjectSink
{

Если теперь метод
GetObjectSink
интерфейса
IContributeObjectSink
определить так:
public IMessageSink GetObjectSink(MarshalByRefObject obj, IMessageSink nextSink)
{
return new ReplicationAspect(nextSink, obj);
}
то можно воспользоваться такой удобной вещью, как доступ к свойствам метода до того, как сам этот метод выполняется.

Смысл тут простой. На требуемые методы адаптеров навешиваются атрибуты отслеживания (см. выше), при вызове методов классов, реализующих адаптеры, поток выполнения заходит сначала в приёмник вызова. Этот самый приёмник что-то вроде пазухи, потайного кармана, расположенного между внутренней и наружной частью одежды. Если мы рассматриваем обычную модель работы со стеком, то между вызовом команды CALL процессора и снятием первого аргумента со стека в самой команде ничего нет. Вызвали - сняли со стека аргумент. А здесь, с приёмником, ситуация такая: закинули в стек аргументы, вызвали, а перед вызовом выполняется некий специальный код (это неинтересно объяснять, лучше один раз глянуть в MSDN), заворачивающий вызов метода в спец. оболочку, дающую доступ как к параметрам до вызова, так и к параметрам и возврату после вызова. Это чем-то похоже на генерирование защитного кода для тестирования обращения за пределы стека в Rational Purify, но гораздо более общо.

Ну так вот. Используя контексты вызова, можно протоколировать вызовы команд соответствующего сервера БД. В самом деле. В момент вызова уже известно, каким именно образом код будет сохранять (изменять, удалять данные) и всё, что потребуется - это просто посмотреть, кто именно вызывает методы изменения и с какими аргументами. Очень просто.

public IMessage SyncProcessMessage(IMessage Msg)
{
bool logOperation = Preprocess(Msg);
IMessage ret = m_NextSink.SyncProcessMessage(Msg);

if (logOperation)
PostProcess(Msg, ret);

return ret;
}

Не все операции надо протоколировать (например - можно протоколировать и SELECT, но зачем? Это задача аудита БД, мне нужно только протоколировать изменения и обеспечить откаты[в хорошем смысле]). Но если нужно протоколировать изменения, то метод PostProcess, вызываемый после собственно метода - то самое место, где будет выполняться запись протокола. В методе PostProcess для вызванного метода получаем перечень наших атрибутов, ну а дальше понятно. Единственное, что тут может потребоваться - это пояснить, как применяются и зачем тут атрибуты.

У меня они применяются так:

public interface IDataStorageAdapter
{
[AdapterReplication(SqlAction.Insert)]
long Insert(BaseItem Param);

[AdapterReplication(SqlAction.Update)]
int Update(BaseItem Param);

[AdapterReplication(SqlAction.Delete)]
int Delete(long Param);
...
}

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

Если рассматривать изменение хранилища, как последовательность вставок записей об изменениях, то процедура отката - очевидна. Однако, не всё так просто. Как правило, объекты сложны и сохраняются каскадами, а каждый каскад - внутри транзакции. В то время, как транзакции являются логическими единицами, вставки - являются единицами физическими, отследить по этим вставкам транзакции - не представляется возможным. Поэтому необходимо вводить маркеры, которые собой знаменовали бы начало транзакции. Я пишу это для того, чтобы лучше понять, каким образом должна быть написана спецификация на эту функциональность, не обманывай себя, о случайный читатель. Такими маркерами может быть вставка записи об объекте высокого уровня. Тогда разбиваем весь набор записей на классы по признаку уровень и выполняем откат от одной высокоуровневой записи до другой. Надо подумать, не является ли высокий уровень объекта достаточным условием для констатации отката транзакции полностью...

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