AutoCAD二次开发,托管net,事务Transaction,事务管理器的理解 对于事务Transaction的理解,应该是CAD.net开发的重点和难点。 要想理解事务,我们需要先理解database。当CAD打开一个dwg的时候,就会在内存中生成一个database对象,这个database对象包含了dwg里的保存的所有内容。Database就是dwg在内存中的反应。我们可以把database理解为一个仓库。那么你想一想,一个工厂的仓库里所存放的物品,可以随意的拿出来放进去吗?如果一个工厂的仓库可以由任何人随意的取东西放东西,那么过不了多久,这个仓库里的东西有什么没有什么就没有人知道了,我们对这个仓库也就无从管理了。 CAD也是一样,dwg文件保存了用户的设计文件,那是设计者的心血,一旦损坏将会造成严重的损失。所以保护dwg文件的完整性正确性是至关重要的。所以,CAD不会让二开程序随意的存取数据库中的数据,这时候就需要事务来进行管理。我们可以把事务理解为仓库管理员,或者理解为仓库的出库入库手续。事务来确保dwg文件不会发生错误,一旦在存取过程中发生错误,事务会取消所有的动作,恢复原来的状态。 当我们在屏幕上选择了对象,那么CAD提供给你这个对象的id,为什么CAD不直接提供给你这个对象,而只是提供给你对象的id呢?因为出于对数据库对象的保护,在很多时候你只需要知道id就可以了。就好比总经理检查仓库,他不一定要检查每一件物品,只需要检查库存清单就可以了。当你真的需要这个物品的时候,CAD也不会直接把这个物品给你,而是需要打开事务,由事务的GetObject动作给你取出来。就好比你要到仓库里取东西,仓库管理员是不会让你自己进去拿的,要由他把这个东西拿出来给你。为什么要这么做呢?因为你在数据库中取出一个对象,并不是真的像现实中一样把对象拿出来给你。而是给你一个权限可以直接把手伸进仓库里去访问这个对象。那么此时仓库管理员就要时刻的盯着你,注意你的每一个动作,防止你搞破坏。 当我们用只读的方式获取对象,那么事务会关注你是不是对这个对象进行修改,如果你修改了,事务就会告诉你不行,更重要的是告诉数据库,拒绝你的修改。当我们用可写的方式获取对象,那么事务就会记录你的所有修改动作,最后在你决定提交之后,才会统一的提交所有的动作,他不会在你每次修改的时候马上让数据库接收这个修改,而是要统一进行提交,因为提交的时候,管理员要对每个动作进行审核,看一看是不是有错误,会不会有什么冲突,如果有任何不妥的地方,管理员都会拒绝你的所有修改动作。管理员就是这么严格,只要你希望做的一系列修改动作里有任何一个不合适的动作,那么他就会拒绝你所有的请求。因为他要保护数据库,保护dwg,他就是这么严谨。 而且,我们在可写打开对象的时候,所有的修改动作都必须是在事务全程监督下进行,中途他是不能离开的。比如仓库里存放了一个设备,某个修理工找到库管员说,把这个设备取出来我修一下,然后再放进去。那么库管员会把这个设备取出来交给修理工,然后自己忙自己的,等修理工修完之后再把这个设备入库。会这样做吗?管理不严格的公司可能会这么做。这么做的弊端就是再入库的时候库管员要自己来检查这个设备都是哪里进行了修改,然后进行登记,假如他检查漏了呢,假如修理工自己卸下来某个隐蔽的零件自己藏起来而库管员又没有发现呢?那么这个损失是不是要库管员承担呢?所以说我们的CAD不会这么做,他的库管员会全程站在旁边观察你所有的修理动作,并且全部记录在案。 也就是说,当我们可写打开对象的时候,要在事务内部完成整个修改过程。什么是事务内部呢?就是我们创建了事务之后,并且在事务销毁之前。比如: Transactiontr = db.TransactionManager.StartTransaction(); /// ///事务内部 /// tr.Dispose(); 或者: using(Transaction tr =db.TransactionManager.StartTransaction()) {/// ///事务内部 /// } 那么这种情况呢? if (pan) { Transactiontr = db.TransactionManager.StartTransaction()); /// /// 做了一些事情 /// ///这里没有销毁事务 } ///这里算事务内部吗? 在一组大括号内建立的事务对象,尽管后面没有销毁,在大括号结束的时候也会自动销毁的,所以后面这个事务已经没有了,也就是说管理员已经走了,你再对这个对象进行任何的修改动作都是无效的且不允许的。 那么假如我写这样一个函数: staticEntityOpenEntity(Database db, ObjectId id, OpenMode openMode) { using(Transactiontr = db.TransactionManager.StartTransaction()) { Entity ent= tr.GetObject(id, openMode) asEntity; returnent; } } 然后调用这个函数,把可写参数传进去得到这个对象: Entityent= OpenEntity(db, id, OpenMode.ForWrite); ///后面这里可以修改这个对象吗? 那么我们可以在后面修改这个对象吗?当然不能!因为函数内部的事务只在函数内部存在,离开这个函数这个事务已经不在了,你再想修改这个对象就不允许了。 所以我们必须在事务内部修改对象,而事务内部也就是这个事务对象存续期间,一个对象的存续区间就是他建立到销毁的这段区间,而他所在的那个大括号结束的时候,他必然也被销毁了。 事务还有一个特点,在一个事务内部不能进行多次tr.Commit()提交,也就是说我们一般都在事务销毁之前提交。不能提交一部分再操作一部分再提交最后销毁。这样是不允许的。所以一般在销毁之前的最后一步动作就是提交事务。 using(Transactiontr = db.TransactionManager.StartTransaction()) { /// 做了一些事情 /// 做了一些事情 /// 做了一些事情 /// 做了一些事情 tr.Commit(); } 这样的不行的: ///下面这么做是不行的 using(Transactiontr = db.TransactionManager.StartTransaction()) { /// 做了一些事情 tr.Commit(); /// 做了一些事情 tr.Commit(); } 当我们只读打开对象的时候,我们就不需要太关注事务的存续区间,我们因为是在只读的使用这个对象,所以离开事务的区间也是可以的,只要保证不对对象进行任何修改就好了,也就是说对于对象的属性字段,我们只gei,不set,就好。当然如果你修改了,就会报错,告诉你对象没有可写打开。 下面的问题是关于事务什么时候打开什么时候关闭的问题。 我们想象一下,一个工厂的仓库,我们是不是需要存取物品的时候打开,存取完成之后就关闭?仓库管理员是不是在需要存取物品的时候找他,不需要存取物品的时候就不用管他?所以事务也是一样,当我们需要事务的时候打开事务,不需要事务的时候关闭事务。有的人认为既然事务这么重要,我们是不是可以在程序的一开始就打开事务,在程序结束前提交并销毁事务。这样做有必要吗?我认为这么做是没必要的,这就好比如一个工厂的库管员兼任总经理,生产的全过程都在库管员的监督之下。 出库入库管理至关重要,也不容许一点差错,但是不代表一个工厂的整个生产过程都需要库管员来监督。因为库管员管的就是仓库,很多的生产过程其实和仓库没有半毛钱关系。 比如一个生产玩具的工厂,总经理说我这里有个设计图,车间按照这个生产出来一个玩具然后拿给我看。车间生产完了给总经理,总经理看了看说,这个还有些问题,我还要再修改一下设计,把生产记录保存起来,这个玩具就销毁吧。那么请问这整个过程和仓库有关系吗?没有半毛钱的关系,整个过程需要打开仓库吗?需要把库管员叫过来监督吗?需要事务吗?完全不需要。 很多人可能是看了一些教材的写法,但是理解的不透彻,所以搞不清对象和数据库的关系,比如CAD二次开始的官方 教程都是这么写的: public static void AddLine() { // 获得当前文档和数据库 Get the currentdocumentand database Document acDoc=Application.DocumentManager.MdiActiveDocument; Database acCurDb = acDoc.Database; // 启动一个事务 Start a transaction using (Transaction acTrans=acCurDb.TransactionManager.StartTransaction()) { // 以只读方式打开块表 Open the Block tableforread BlockTable acBlkTbl; acBlkTbl =acTrans.GetObject(acCurDb.BlockTableId,OpenMode.ForRead) as BlockTable; // 以写方式打开模型空间块表记录 Open the Block tablerecordModel space for write BlockTableRecord acBlkTblRec; acBlkTblRec=acTrans.GetObject(acBlkTbl[BlockTableRecord.ModelSpace], OpenMode.ForWrite) asBlockTableRecord; //注意这里!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! //注意这里!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! //注意这里!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // 创建一条起点为(5,5,0),终点为(12,3,0)的直线 Create a line that starts at5,5 andends at 12,3 Line acLine = new Line(new Point3d(5, 5, 0), newPoint3d(12, 3, 0)); acLine.SetDatabaseDefaults(); // 添加新对象到块表记录和事务中 Add the new object totheblock table record and the transaction acBlkTblRec.AppendEntity(acLine); acTrans.AddNewlyCreatedDBObject(acLine,true); // 保存新对象到数据库中 Save the new object tothedatabase acTrans.Commit(); } } 这是官方教程中关于创建一条直线的例子。 那么我想请问,创建一条直线,和数据库以及事务之间有什么必然的关系吗?需要先打开事务,然后创建直线,然后放入数据库这个过程吗?或者这样说,我在车间生产一个玩具,需要先把仓库的门打开,然后我开动机器生产,然后把这个玩具放入仓库,然后把仓库门关闭。需要这个过程吗?或者这样说,我生产这个玩具和把玩具放入仓库有什么必然的关系吗?我可不可以先在车间启动机器,生产玩具,然后再打开仓库,放入玩具,再关闭仓库? public static void AddLine() { // 获得当前文档和数据库 Get the currentdocumentand database Document acDoc=Application.DocumentManager.MdiActiveDocument; Database acCurDb = acDoc.Database; //注意这里!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! //注意这里!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! //注意这里!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // 创建一条起点为(5,5,0),终点为(12,3,0)的直线 Create a line that starts at5,5 andends at 12,3 Line acLine = new Line(new Point3d(5, 5, 0), newPoint3d(12, 3, 0)); // 启动一个事务 Start a transaction using (Transaction acTrans=acCurDb.TransactionManager.StartTransaction()) { // 以只读方式打开块表 Open the Block tableforread BlockTable acBlkTbl; acBlkTbl =acTrans.GetObject(acCurDb.BlockTableId,OpenMode.ForRead) as BlockTable; // 以写方式打开模型空间块表记录 Open the Block tablerecordModel space for write BlockTableRecord acBlkTblRec; acBlkTblRec=acTrans.GetObject(acBlkTbl[BlockTableRecord.ModelSpace], OpenMode.ForWrite) asBlockTableRecord; acLine.SetDatabaseDefaults(); // 添加新对象到块表记录和事务中 Add the new object totheblock table record and the transaction acBlkTblRec.AppendEntity(acLine); acTrans.AddNewlyCreatedDBObject(acLine, true); // 保存新对象到数据库中 Save the new object tothedatabase acTrans.Commit(); } } 或者我在车间生产了一个玩具,然后只是看一看,试一试,就销毁了,完全就没有想过要放入仓库,这样不可以吗? // 创建一条起点为(5,5,0),终点为(12,3,0)的直线 Create a line that starts at5,5 andends at 12,3 Line acLine = new Line(new Point3d(5, 5, 0), newPoint3d(12, 3, 0)); //用这条线做一些事情 acLine.Dispose(); 当然是可以的,所以说创建一个对象不代表就一定要加入数据库,就算加入数据库,也没必要先把事务打开。在任何我想要加入数据库的时候我再打开事务,加入数据库就行了。 你在数据库中取得的对象,和新建的对象,本质上是没有任何区别的,只是一个在数据库里,有objectid,一个在内存里,在加入数据库之前他没有objectid。比如我在数据库里(也就是屏幕上)获取一条线,然后我再创建一条线,但是我并不需要把他加入数据库,我只想用新建的这条线和数据库里获取的那条线来求个交点,之后我就销毁这条新建的对象。这完全是可以的,也是完全不需要去加入数据库的。并不是说要把这条新建的线加入数据库才能求交点。 所以,我的观点,事务在需要的时候打开,在完成任务的时候马上关闭。这并不是考虑任何效率和速度的问题,而是保证我们的思路清晰。在程序开头打开事务,在程序结束关闭,也就是所谓的一个事务干到底的做法,我是反对的。而且,这么做也不能多次提交。 比如这样的过程,我在屏幕上选择一些对象,操作之后绘制到屏幕上。(加入数据库并刷新就相当于绘制到屏幕上),再选择,再操作,再绘制,再选择,再操作,再绘制,这样的过程就不能用一个事务干到底的方式。总而言之,这种做法只是没有对事务理解透彻而已。 如果把CAD二开比作弹钢琴,那么你设计的程序是要弹奏一首曲子,那么你肯定会在程序开始的时候打开钢琴盖子,在程序结束的时候关闭钢琴盖子。但是假如你的程序是创作一首钢琴曲,那么还需要在你想开始创作的时候打开钢琴盖子,在创作完成之后关闭钢琴盖子吗?创作的过程当然要弹奏,但是有时候你在花园里散步也是在创作,你在喝茶的时候也是在创作,你在床上躺着的时候也是在创作,那么这个时候,你的钢琴盖子需要打开吗? 这是一个是否思路清晰的问题,而不是一个关于效率的问题。 以上仅代表个人观点,仅供参考。 PS:本人承接CAD二开代写复杂几何算法,欢迎关注我的B站,鸿的几何空间。https://space.bilibili.com/475629013
|