История изменения
Эта тема особенно беспокоит меня в последнее время. Что-то похожее уже было, но сейчас проблема предельно заострилась и формулируется так: требуется обобщённый механизм отката изменений в состоянии хранилищ на произвольное количество шагов. Важно, что в хранилище задаются неизвестные на момент создания механизма зависимости между объектами.
Предусловия.
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);
...
}
Но это только низший уровень. При помощи контекстов получается строка, записываемая в таблицу истории изменений. Вставка одного объекта приводит к записи одной строки.
Если рассматривать изменение хранилища, как последовательность вставок записей об изменениях, то процедура отката - очевидна. Однако, не всё так просто. Как правило, объекты сложны и сохраняются каскадами, а каждый каскад - внутри транзакции. В то время, как транзакции являются логическими единицами, вставки - являются единицами физическими, отследить по этим вставкам транзакции - не представляется возможным. Поэтому необходимо вводить маркеры, которые собой знаменовали бы начало транзакции. Я пишу это для того, чтобы лучше понять, каким образом должна быть написана спецификация на эту функциональность, не обманывай себя, о случайный читатель. Такими маркерами может быть вставка записи об объекте высокого уровня. Тогда разбиваем весь набор записей на классы по признаку уровень и выполняем откат от одной высокоуровневой записи до другой. Надо подумать, не является ли высокий уровень объекта достаточным условием для констатации отката транзакции полностью...
Комментариев нет:
Отправить комментарий