Skip to content
C#
事务与数据库

事务与数据库

事务是 AutoCAD .NET API 中最核心也最容易出错的概念。所有对 Database 的操作都必须在事务保护下进行。


为什么需要事务

AutoCAD 是一个多文档、支持撤销/重做的应用程序。事务机制提供:

  1. 原子性:多个操作要么全部提交,要么全部回滚
  2. 一致性:保证数据库始终处于有效状态
  3. 隔离性:事务之间的操作不相互干扰
  4. 持久性:提交后变更持久化到 DWG

🔬 事务本质上是 ObjectARX C++ 层的 AcDbTransaction 的托管包装。每个打开的对象都注册到事务中,事务结束时自动清理。


Transaction 生命周期

Database db = doc.Database;

using (Transaction tr = db.TransactionManager.StartTransaction())
{
    // 1. 在事务内打开/操作对象
    Circle circle = tr.GetObject(id, OpenMode.ForRead) as Circle;

    // 2. 显式提交
    tr.Commit();

    // 3. using 块结束时:
    //    - 如果已 Commit → 变更永久生效
    //    - 如果未 Commit → 自动回滚所有变更
    //    - Dispose 释放非托管资源
}

关键状态图

StartTransaction
    │
    ▼
  活动状态 ←── 操作对象(GetObject, AppendEntity...)
    │
    ├── Commit()  → 变更生效
    │
    └── Dispose() → 未提交的变更回滚
                    (已提交则无操作)

事务中的对象获取

using (Transaction tr = db.TransactionManager.StartTransaction())
{
    // GetObject —— 通过 ObjectId 获取对象
    Line line = tr.GetObject(lineId, OpenMode.ForRead) as Line;

    // 获取符号表
    BlockTable bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);

    // 获取命名对象字典
    DBDictionary dict = (DBDictionary)tr.GetObject(
        db.NamedObjectsDictionaryId, OpenMode.ForRead);
}

OpenMode 详解

OpenMode 含义 使撤销变大 阻止其他操作
ForRead 只读
ForWrite 读写 否(同文档不同事务可读)
ForNotify 通知
// 典型的先读后写升级
Entity ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
if (ent.Layer == "OLD")
{
    ent.UpgradeOpen();         // 升级为写
    ent.Layer = "NEW";
    ent.DowngradeOpen();       // 降级回读(可选)
}

添加新对象到事务

using (Transaction tr = db.TransactionManager.StartTransaction())
{
    // 方式 1:通过 BlockTableRecord 添加
    BlockTable bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
    BlockTableRecord modelSpace = (BlockTableRecord)tr.GetObject(
        bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite);

    Circle circle = new Circle(center, Vector3d.ZAxis, 50);
    modelSpace.AppendEntity(circle);
    tr.AddNewlyCreatedDBObject(circle, true);     // ← 关键!纳入事务

    tr.Commit();
}

🚨 新创建的 DBObject 必须调用 tr.AddNewlyCreatedDBObject(obj, true) 将其纳入事务管理。否则 Commit 时不会写入数据库,且可能在 Dispose 时崩溃。


事务嵌套

AutoCAD 支持嵌套事务:

using (Transaction outer = db.TransactionManager.StartTransaction())
{
    // 外层操作
    outer.AddNewlyCreatedDBObject(entity1, true);

    using (Transaction inner = db.TransactionManager.StartTransaction())
    {
        // 内层操作
        inner.AddNewlyCreatedDBObject(entity2, true);
        inner.Commit();    // entity2 暂存(并不立即写入 DWG)
    }

    // 如果 outer 未 Commit → entity2 的变更也会回滚
    outer.Commit();        // entity1 + entity2 同时写入
}

🔬 内层事务提交后,变更写入"外层事务的缓冲区"而非直接写入 DWG。只有最外层事务 Commit 后,所有嵌套变更才同步写入数据库。


OpenCloseTransaction(性能优化)

// 传统事务:适用于修改操作
using (Transaction tr = db.TransactionManager.StartTransaction())
{
    // 产生撤销记录,开销较大
}

// 开放关闭事务:适用于只读查询、数据导出
using (Transaction tr = db.TransactionManager.StartOpenCloseTransaction())
{
    // 更快、无撤销记录、内存占用更少
    BlockTable bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
    BlockTableRecord modelSpace = (BlockTableRecord)tr.GetObject(
        bt[BlockTableRecord.ModelSpace], OpenMode.ForRead);

    foreach (ObjectId id in modelSpace)
    {
        Entity ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
        if (ent != null)
        {
            // 只读遍历大量实体 —— OpenCloseTransaction 更合适
        }
    }
}

⚡ 对比:遍历 100,000 个实体时,OpenCloseTransaction 比普通 Transaction 快 2-3 倍,且不增加 DWG 文件大小(无撤销数据)。


Database 级别操作

获取数据库信息

// 单位
double insUnits = db.Insunits;              // 插入单位
double measurement = db.Measurement;         // 0=英制 1=公制

// 范围(需先更新)
db.UpdateExt(true);   // true = 包含已删除实体占用的空间
Extents3d ext = db.Extmax;
Extents3d allExt = new Extents3d(db.Extmin, db.Extmax);

// 系统变量
object snapMode = Application.GetSystemVariable("OSMODE");
Application.SetSystemVariable("OSMODE", 0);

// 字典
DBDictionary layoutDict = (DBDictionary)tr.GetObject(
    db.LayoutDictionaryId, OpenMode.ForRead);

// 自定义数据存储(命名对象字典)
DBDictionary nod = (DBDictionary)tr.GetObject(
    db.NamedObjectsDictionaryId, OpenMode.ForRead);
if (!nod.Contains("MYAPPDATA"))
{
    Xrecord xrec = new Xrecord();
    xrec.Data = new ResultBuffer(
        new TypedValue((int)DxfCode.Text, "My custom data"));
    nod.UpgradeOpen();
    ObjectId xrecId = nod.SetAt("MYAPPDATA", xrec);
    tr.AddNewlyCreatedDBObject(xrec, true);
}

读取/保存 DWG

// 保存当前文档
doc.Database.SaveAs(newPath, DwgVersion.Current);

// 以只读方式侧载另一个 DWG
using (Database extDb = new Database(false, true))  // false=不关联文档, true=无撤销
{
    extDb.ReadDwgFile(externalPath, FileOpenMode.OpenForReadAndAllShare, false, "");

    using (Transaction tr = extDb.TransactionManager.StartOpenCloseTransaction())
    {
        // 读取外部 DWG 的数据
    }
}

事务中的对象关系

ObjectId vs DBObject 生命周期

// ✅ ObjectId 在事务外仍然有效(轻量值类型)
ObjectId savedId;
using (Transaction tr = db.TransactionManager.StartTransaction())
{
    Line line = new Line(start, end);
    modelSpace.AppendEntity(line);
    tr.AddNewlyCreatedDBObject(line, true);
    savedId = line.ObjectId;          // 保存 ID 到事务外
    tr.Commit();
}
// savedId 在此仍有效

// 🚨 DBObject 在事务外无效!
// 事务结束后,通过 GetObject 获取的 DBObject 引用被释放
ObjectId id;
using (Transaction tr = db.TransactionManager.StartTransaction())
{
    Line line = tr.GetObject(id, OpenMode.ForRead) as Line;
    double len = line.Length;      // ✅ 事务内可以访问
    tr.Commit();
}
// double len = line.Length;       // ❌ line 已被释放!AccessViolation!

🚨 这是 AutoCAD 二次开发中最常见的崩溃原因。永远不要在事务外持有 DBObject 引用。应该持有 ObjectId,需要时通过新事务重新 GetObject


事务工具类封装

public static class TransactionHelper
{
    /// <summary>
    /// 在只读事务中执行操作
    /// </summary>
    public static void Read(Database db, Action<Transaction> action)
    {
        using (Transaction tr = db.TransactionManager.StartOpenCloseTransaction())
        {
            action(tr);
            // 只读操作不 Commit
        }
    }

    /// <summary>
    /// 在读写事务中执行操作
    /// </summary>
    public static void Write(Database db, Action<Transaction> action)
    {
        using (Transaction tr = db.TransactionManager.StartTransaction())
        {
            action(tr);
            tr.Commit();
        }
    }

    /// <summary>
    /// 获取模型空间
    /// </summary>
    public static BlockTableRecord GetModelSpace(Database db, Transaction tr)
    {
        BlockTable bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
        return (BlockTableRecord)tr.GetObject(
            bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite);
    }
}

// 使用
TransactionHelper.Write(db, tr =>
{
    Line line = new Line(new Point3d(0,0,0), new Point3d(100,0,0));
    BlockTableRecord ms = TransactionHelper.GetModelSpace(db, tr);
    ms.AppendEntity(line);
    tr.AddNewlyCreatedDBObject(line, true);
});