вторник, 16 сентября 2014 г.

Управление способом работы с базой данных чертежа: с использованием транзакции, или же с её эмуляцией

Маленький пример на тему того, как не меняя программный код можно управлять способом работы с базой данных чертежа: с использованием транзакции, или же с её эмуляцией. Используя такой подход можно моментально поменять способ работы сразу во многих местах, не внося правки в исходный код.
// © Андрей Бушман, 2014
// Commands.cs
// AutoCAD 2009 SP3
// Код предназначен для демонстрации того, как можно, не внося изменений в 
// исходный код, менять способ работы с базой данных чертежа: с использованием 
// транзакции или же с её эмуляцией.
 
using System;
 
#if AUTOCAD
using cad = Autodesk.AutoCAD.ApplicationServices.Application;
using Ap = Autodesk.AutoCAD.ApplicationServices;
using Db = Autodesk.AutoCAD.DatabaseServices;
using Ed = Autodesk.AutoCAD.EditorInput;
using Gm = Autodesk.AutoCAD.Geometry;
using Rt = Autodesk.AutoCAD.Runtime;
#endif
 
[assembly: Rt.CommandClass(typeof(Bushman.CAD.Sandbox.TransactionGames.Commands
    ))]
 
namespace Bushman.CAD.Sandbox.TransactionGames {
    public sealed class Commands {
 
        /// <summary>
        /// Тестовая команда
        /// </summary>
        [Rt.CommandMethod("SwitchTransactionSample")]
        public void SwitchTransactionSample() {
 
            Ap.Document doc = cad.DocumentManager.MdiActiveDocument;
            if (null == doc)
                return;
            Db.Database db = doc.Database;
            Db.TransactionManager tm = db.TransactionManager;
            Ed.Editor ed = doc.Editor;
 
            Ed.PromptKeywordOptions pso = new Ed.PromptKeywordOptions(
                "Использовать транзакцию?");
            String yes = "Yes";
            String no = "No";
            pso.Keywords.Add(yes);
            pso.Keywords.Add(no);
            pso.AppendKeywordsToMessage = true;
            pso.Keywords.Default = yes;
 
            Ed.PromptResult pr = ed.GetKeywords(pso);
            ed.WriteMessage("Вы выбрали: {0}\n", pr.StringResult);
 
            if (Ed.PromptStatus.OK != pr.Status) {
                ed.WriteMessage("Выполнение команды прервано.\n");
            }
 
            Boolean transaction_use = pr.StringResult == yes;
 
            // Т.к. OpenCloseTransaction наследуется от Transaction, то 
            // получить Transaction можно следующим образом:
            using (Db.Transaction tr = GetTransaction(tm, transaction_use)) {
 
                Db.DBText text = new Db.DBText();
                text.SetDatabaseDefaults(db);
                text.Height = 30;
                Double y = 100;
                String text_string = String.Empty;
 
                if (transaction_use) {
                    text_string = "Текст создан с использованием транзакции.";
                }
                else {
                    y = 200;
                    text_string = "Текст создан с эмуляцией транзакции.";
                }
 
                text.Position = new Gm.Point3d(100, y, 0);
                text.TextString = text_string;
 
                Db.BlockTableRecord model_space = tr.GetObject(
                    db.CurrentSpaceId, Db.OpenMode.ForWrite)
                    as Db.BlockTableRecord;
 
                model_space.AppendEntity(text);
                tr.AddNewlyCreatedDBObject(text, true);
 
                tr.Commit();
            }
        }
 
        /// <summary>
        /// Данный метод позволяет выбрать способ работы с базой данных 
        /// чертежа: с использованием транзакции или же с её эмуляцией.
        /// </summary>
        /// <param name="tm">Менеджер транзакций</param>
        /// <param name="emulate"><c>true</c> - использовать транзакцию; 
        /// <c>false</c> - эмулировать транзакцию. 
        /// </param>
        /// <returns>Возвращается объект <c>Transaction</c> (
        /// <c>OpenCloseTransaction</c> унаследован от этого класса).</returns>
        Db.Transaction GetTransaction(Db.TransactionManager tm,
            Boolean transaction_use) {
            Db.Transaction tr = transaction_use ? tm.StartTransaction() :
                tm.StartOpenCloseTransaction();
            return tr;
        }
    }
}

3 комментария:

Евгений Хрущ комментирует...

В чём преимущество работы без транзакции? В Автокаде работал только с транзакциями, а в Теиге только без них. Так же во втором случае не требуется блокировать документ. Поэтому использовал подобный подход:
У меня есть библиотека CADEngine в виде класса с функциями для более удобной работы в разных кад-системах.

public Transaction StartTransaction(bool lock_document = false) {
if (lock_document) {
DocUnlock();
var doc = Application.DocumentManager.MdiActiveDocument;
LastDocLoc = doc.LockDocument();
}
return Transaction.StartTransaction();
}
public void DocUnlock() {
if (LastDocLoc == null) return;
LastDocLoc.Dispose();
LastDocLoc = null;
}

public void Commit(Transaction tr) {
tr.Commit();
DocUnlock();
}

Но после этого я решил не городить огород с лишними "using cad.StartTransaction"... и сделал такой метод:
public void MakeInTransaction(Action action) {
MakeInTransaction(null, action);
}

public void MakeInTransaction(Transaction tr, Action action) {
var need_commiting = tr == null;
if (UseTransactions) {
tr = tr ?? StartTransaction(!WithoutDocument);
}
try {
action(tr);
} finally {
if (need_commiting && UseTransactions && tr != null) {
Commit(tr);
tr.Dispose();
}
}
}

Таким образом, я в коде не беспокоясь о блокировке документа, о создании и об отправке транзакции пишу:
cad.MakeInTransaction(tr => {
// код, использующий транзакцию
});
Всё остальное делается само.
Если же мне нужно в особых случаях не создавать новую транзакцию, а действовать в рамках старой, я пишу:
cad.MakeInTransaction(trn, tr => {
// Даже если существующая, вроде бы, транзакция trn окажется == null, tr будет создана новая.
});

Андрей Бушман комментирует...

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

Некоторые вещи сделать с транзакцией невозможно в принципе. Пример: использование HandOwerTo.

Андрей Бушман комментирует...

Александр Наумович меня поправил:

Транзакции были изначально. Просто в ObjectARX в 99% случаев все примеры без использования транзакций, а в AutoCAD .NET API с точностью до наоборот.