1. 程式人生 > >AE SDK 翻譯之: Geodatabase API best practices

AE SDK 翻譯之: Geodatabase API best practices

http://help.arcgis.com/en/sdk/10.0/arcobjects_net/conceptualhelp/index.html#/Geodatabase_API_best_practices/000100000047000000/

本主題討論如何在地理資料庫應用程式程式設計介面(API)中使用某些元件,以優化效能,防止資料損壞並避免意外的行為。 伴隨每個最佳實踐的描述都是程式碼示例,顯示不正確或正確的模式。

  1. 在這個話題包括:

關於地理資料庫API最佳實踐
瞭解回收
儲存FindField結果
在編輯會話中執行DDL
在Store觸發的事件中呼叫Store
GetFeature和GetFeatures
不小心重複使用變數
插入和關係類通知
修改模式物件


關於地理資料庫API最佳實踐
本主題旨在成為使用地理資料庫API的開發人員的“備忘單”。 有許多可以提高效能的最佳實踐,以及許多可能會損害效能或導致意想不到的結果的常見錯誤。 本主題中的資訊是兩者的組合,基於支援事件,論壇帖子和其他第三方程式碼中的程式碼示例。
本主題中的程式碼示例用於說明周圍段落中的文字,而不僅僅是提供可複製和貼上到應用程式中的程式碼。 在某些情況下,程式碼被用來說明一個程式設計模式,以避免。 在複製和貼上本主題中的任何程式碼之前,請確保從程式碼的周圍段落文字中,這是一個使用練習的例子,而不是一個要避免的事例。

瞭解回收
回收是遊標的一個屬性,決定了如何建立遊標中的行。 可以啟用或禁用回收功能,並通過API作為布林引數暴露給幾個遊標例項化方法,包括ITable.Search和ISelectionSet.Search。
如果啟用了回收功能,遊標只會為單個行分配記憶體,而不管遊標返回的行數是多少。 這提供了記憶體使用和執行時間方面的效能優勢,但是對於某些工作流程有缺陷。
在任何時候只有一行將被引用的情況下,例如,繪製幾何圖形或將當前行的ObjectID顯示到控制檯視窗時,回收很有用。 當需要以某種方式比較遊標中的多行時,或者正在編輯行時,請避免回收。 考慮下面的程式碼示例,比較特徵遊標的前兩個幾何,看它們是否相等:

public static void RecyclingInappropriateExample(IFeatureClass featureClass, Boolean
    enableRecycling)
{
    using(ComReleaser comReleaser = new ComReleaser())
    {
        // Create a search cursor.
        IFeatureCursor featureCursor = featureClass.Search(null, enableRecycling);
        comReleaser.ManageLifetime(featureCursor);

        // Get the first two geometries and see if they intersect.
IFeature feature1 = featureCursor.NextFeature(); IFeature feature2 = featureCursor.NextFeature(); IRelationalOperator relationalOperator = (IRelationalOperator)feature1.Shape; Boolean geometriesEqual = relationalOperator.Equals(feature2.Shape); Console.WriteLine("Geometries are equal: {0}", geometriesEqual); } }

如果啟用了回收功能,則前面的程式碼始終返回true,因為feature1和feature2引用將指向同一個物件。 第二個NextFeature呼叫不會建立一個行,它會覆蓋現有行的值。 對IRelationalOperator.Equals的呼叫是將幾何與自身進行比較。 出於同樣的原因,“兩個”特徵的ObjectID或屬性值之間的任何比較也表示相等。
禁用回收是一種更為謹慎的方法,因為不恰當地使用非回收遊標不可能返回像以前的程式碼示例那樣的意外結果,但是會對效能造成重大的損失。
以下程式碼示例在要素類上開啟一個搜尋游標並查詢每個要素區域的總和。 因為這不會引用任何以前提取的行,所以這是循環遊標的理想候選者:

public static void RecyclingAppropriateExample(IFeatureClass featureClass, Boolean
    enableRecycling)
{
    using(ComReleaser comReleaser = new ComReleaser())
    {
        // Create a search cursor.
        IFeatureCursor featureCursor = featureClass.Search(null, enableRecycling);
        comReleaser.ManageLifetime(featureCursor);

        // Create a sum of each geometry's area.
        IFeature feature = null;
        double totalShapeArea = 0;
        while ((feature = featureCursor.NextFeature()) != null)
        {
            IArea shapeArea = (IArea)feature.Shape;
            totalShapeArea += shapeArea.Area;
        }

        Console.WriteLine("Total shape area: {0}", totalShapeArea);
    }
}

上面的程式碼示例已在檔案地理資料庫中的要素類上進行了測試,其中包含大約500,000個要素,其中包含以下結果(在下文中,〜表示大致):
這個過程的工作環境增加了約4%,而廢棄物的回收率則提高了約48%。
隨著殘疾人的回收,這個方法花了大約2.25倍的時間執行。
其他類似的工作流程在不恰當地使用非循環遊標時可能會導致更大的差異,例如工作集增加近250%,執行時間比啟用迴圈的時間長12倍。

儲存FindField結果
方法,如IClass.FindField和IFields.FindField用於根據名稱檢索資料集或欄位集合中欄位的位置。 依靠FindField而不是硬編碼的欄位位置是一個好習慣,但過度使用FindField可能會影響效能。 考慮下面的程式碼示例,其中從遊標的特徵中檢索“NAME”屬性:

public static void ExcessiveFindFieldCalls(IFeatureClass featureClass)
{
    using(ComReleaser comReleaser = new ComReleaser())
    {
        // Open a cursor on the feature class.
        IFeatureCursor featureCursor = featureClass.Search(null, true);
        comReleaser.ManageLifetime(featureCursor);

        // Display the NAME value from each feature.
        IFeature feature = null;
        while ((feature = featureCursor.NextFeature()) != null)
        {
            Console.WriteLine(feature.get_Value(featureClass.FindField("NAME")));
        }
    }
}

儘管FindField呼叫並不是一個昂貴的操作,但是當涉及大量的功能時,成本會相加。 更改程式碼以便重新使用FindField結果通常會將效能提高3-10%(在某些情況下甚至更高),而且不需要太多的工作。
下面的程式碼示例顯示了一個額外的好習慣(檢查FindField值不是-1)。 如果找不到欄位,則FindField返回-1。 如果-1值被用作Value屬性的引數(C#中的get_Value和set_Value方法),則不會返回描述性錯誤訊息,因為Value無法知道客戶端要訪問的欄位。

public static void SingleFindFieldCall(IFeatureClass featureClass)
{
    using(ComReleaser comReleaser = new ComReleaser())
    {
        // Open a cursor on the feature class.
        IFeatureCursor featureCursor = featureClass.Search(null, true);
        comReleaser.ManageLifetime(featureCursor);

        // Display the NAME value from each feature.
        IFeature feature = null;
        int nameIndex = featureClass.FindField("NAME");

        // Make sure the FindField result is valid.
        if (nameIndex ==  - 1)
        {
            throw new ArgumentException("The NAME field could not be found.");
        }

        while ((feature = featureCursor.NextFeature()) != null)
        {
            Console.WriteLine(feature.get_Value(nameIndex));
        }
    }
}

在編輯會話中執行DDL
資料定義語言(DDL)命令是修改資料庫模式的資料庫命令。示例包括建立表格,向表格新增新欄位或刪除索引。觸發DDL命令的方法(如IFeatureWorkspace.CreateTable或IClass.AddField)不應在編輯會話中呼叫,因為DDL命令將提交當前開啟的任何事務,如果發生錯誤,則無法回滾任何不需要的編輯。
這種做法也延伸到從資料庫的角度來看不是真正的DDL的地理資料庫模式修改(例如修改域),因為這些型別的操作明確地提交了它們的改變。一個真實的例子是一個自定義編輯應用​​程式,它根據使用者的編輯將新值新增到編碼值域,然後在應用程式嘗試提交編輯時意外失敗。這種情況下的方法是維護使用者提供的值列表,然後在編輯會話停止後新增它們。
在Store觸發的事件中呼叫Store
地理資料庫API公開了幾個事件,這些事件允許開發人員在方法上呼叫Store時應用自定義行為,例如IObjectClassEvents.OnCreate和IRelatedObjectClassEvents.RelatedObjectCreated。開發人員實現通過這些方法定義自定義行為的類擴充套件或事件處理程式,以及像這樣的其他方法,應確保在觸發事件的行上不再呼叫Store,即使自定義行為導致行被修改。在物件上再次呼叫儲存會從模型中觸發事件模型,從而導致意外的行為。在某些情況下,這會導致無限遞迴,從而導致應用程式掛起,而在其他情況下,將返回錯誤資訊,這些資訊可能難以解釋。
下面的程式碼示例顯示了一個簡單的“時間戳”示例,用於維護要建立的要素上當前使用者的名稱,但會根據資料來源生成不同的錯誤型別:

private static void EventHandlerInitialization(IFeatureClass featureClass)
{
    IObjectClassEvents_Event objectClassEvents = (IObjectClassEvents_Event)
        featureClass;
    objectClassEvents.OnCreate += new IObjectClassEvents_OnCreateEventHandler
        (OnCreateHandler);
}

private static void OnCreateHandler(IObject obj)
{
    obj.set_Value(NAME_INDEX, Environment.UserName);
    obj.Store(); // Do not do this!
}

GetFeature和GetFeatures
IFeatureClass介面公開了兩個類似的方法 - GetFeature和GetFeatures - 通過它們的ObjectID檢索要素。 前者檢索單個特徵並獲取一個整型引數,而後者則建立一個遊標,返回在整型陣列引數中指定的特徵(它也有一個引數,指定遊標是否被回收)。
出於效能考慮,任何時候使用已知ObjectID檢索多個特徵時,總是使用GetFeatures方法。 比較以下兩個程式碼示例:
IFeatureClass.GetFeatures使用一致的陣列引數,這使得它在.NET中不安全; IGeoDatabaseBridge.GetFeatures以互操作性安全的方式提供相同的功能。

private static void GetFeatureExample(IFeatureClass featureClass, int[] oidList)
{
    int nameFieldIndex = featureClass.FindField("NAME");
    foreach (int oid in oidList)
    {
        IFeature feature = featureClass.GetFeature(oid);
        Console.WriteLine("NAME: {0}", feature.get_Value(nameFieldIndex));
    }
}

private static void GetFeaturesExample(IFeatureClass featureClass, int[] oidList)
{
    int nameFieldIndex = featureClass.FindField("NAME");
    using(ComReleaser comReleaser = new ComReleaser())
    {
        IGeoDatabaseBridge geodatabaseBridge = new GeoDatabaseHelperClass();
        IFeatureCursor featureCursor = geodatabaseBridge.GetFeatures(featureClass,
            ref oidList, true);
        comReleaser.ManageLifetime(featureCursor);

        IFeature feature = null;
        while ((feature = featureCursor.NextFeature()) != null)
        {
            Console.WriteLine("NAME: {0}", feature.get_Value(nameFieldIndex));
        }
    }
}

前面的程式碼示例具有相同的效能級別,如果請求單個功能,但GetFeatures示例的效能優於兩個功能(特別是遠端資料庫)上的GetFeature示例,並且兩者之間的差異隨著更多功能的增加而增加請求。 有100個功能,GetFeature示例通常需要多達10-12次執行,而有1000個功能,通常需要長達20倍的時間。

不小心重複使用變數
使用Geodatabase API時,不小心重複使用變數會導致兩種型別的複雜情況。 建立集合(如欄位集合)時,最常見的是第一種型別的複雜化。 請參閱以下程式碼示例,該示例旨在建立一組包含ObjectID欄位和字串欄位的欄位:

private static IFields FieldSetCreation()
{
    // Create a field collection and a field.
    IFields fields = new FieldsClass();
    IFieldsEdit fieldsEdit = (IFieldsEdit)fields;
    IField field = new FieldClass();
    IFieldEdit fieldEdit = (IFieldEdit)field;

    // Add an ObjectID field.
    fieldEdit.Name_2 = "OBJECTID";
    fieldEdit.Type_2 = esriFieldType.esriFieldTypeOID;
    fieldsEdit.AddField(field);

    // Add a text field.
    fieldEdit.Name_2 = "NAME";
    fieldEdit.Type_2 = esriFieldType.esriFieldTypeString;
    fieldsEdit.AddField(field);

    return fields;
}

此程式碼不能如預期的那樣工作的原因可能不會立即顯現,並且在生成的欄位集用於建立表時返回的錯誤訊息可能沒有多大幫助(這將對現有的重複欄位桌子)。實際發生的是最終的欄位集包含兩個欄位(兩個相同的字串欄位)。由於“field”和“fieldEdit”變數仍然引用已新增的ObjectID欄位,該欄位物件正在被修改,然後再次新增到集合中。這可以避免使用以下兩種不同的方法:
新增每個欄位後,將欄位和fieldEdit變數重新分配給新建立的欄位物件。
為將要新增到集合的每個欄位使用一組單獨的變數,即“oidField”和“oidFieldEdit”
不小心重複使用變數導致的第二種複雜情況就是失去了所有應該使用ComReleaser類或Marshal.ReleaseComObject方法顯式釋放的物件的引用。考慮下面的程式碼示例:

private static void CursorReassignment(IFeatureClass featureClass, IQueryFilter
    queryFilter)
{
    // Execute a query...
    IFeatureCursor featureCursor = featureClass.Search(queryFilter, true);
    IFeature feature = null;
    while ((feature = featureCursor.NextFeature()) != null)
    {
        // Do something with the feature...
    }

    // Re-execute the query...
    featureCursor = featureClass.Search(queryFilter, true);
    feature = null;
    while ((feature = featureCursor.NextFeature()) != null)
    {
        // Do something with the feature...
    }

    // Release the cursor.
    Marshal.ReleaseComObject(featureCursor);
}

在這種情況下發生的問題是隻有例項化的第二個遊標物件實際上被釋放。 由於對第一個引用的唯一引用丟失,所以第一個遊標現在依賴於被非確定性垃圾收集釋放。 當使用ComReleaser類時也會發生同樣的問題。 生命週期管理是針對物件的,而不是針對變數的。 例如,在下面的程式碼示例中,只有第一個游標被正確管理:

private static void CursorReassignment(IFeatureClass featureClass, IQueryFilter
    queryFilter)
{
    using(ComReleaser comReleaser = new ComReleaser())
    {
        // Execute a query...
        IFeatureCursor featureCursor = featureClass.Search(queryFilter, true);
        comReleaser.ManageLifetime(featureCursor);
        IFeature feature = null;
        while ((feature = featureCursor.NextFeature()) != null)
        {
            // Do something with the feature...
        }

        // Re-execute the query...
        featureCursor = featureClass.Search(queryFilter, true);
        feature = null;
        while ((feature = featureCursor.NextFeature()) != null)
        {
            // Do something with the feature...
        }
    }
}

插入和關係類通知
通知(也稱為訊息傳遞)是關係類的屬性,它定義了在參與關係類的兩個物件類之間傳送哪個方向的訊息。以下是四種通知型別:
無 - 簡單的關係的典型
正向 - 典型的複合關係
落後
兩個(雙向)
這些訊息確保組合關係,功能連結註釋類和許多自定義類擴充套件的正確行為。但是,這種行為確實是有代價的。對觸發通知的資料集進行編輯和插入明顯比對不觸發任何通知的資料集上的同樣操作要慢。
對於插入,可以通過確保所有通知的類在任何插入發生之前開啟來減輕此效能影響。以下程式碼示例基於模式,其中Parcel要素類參與了擁有Owners表的複合關係類,並且正在對要素類進行插入:

public static void NotifiedClassEditsExample(IWorkspace workspace)
{
    // Open the class that will be edited.
    IFeatureWorkspace featureWorkspace = (IFeatureWorkspace)workspace;
    IFeatureClass featureClass = featureWorkspace.OpenFeatureClass("PARCELS");
    ITable table = featureWorkspace.OpenTable("OWNERS");

    // Begin an edit session and operation.
    IWorkspaceEdit workspaceEdit = (IWorkspaceEdit)workspace;
    workspaceEdit.StartEditing(true);
    workspaceEdit.StartEditOperation();

    // Create a search cursor.
    using(ComReleaser comReleaser = new ComReleaser())
    {
        IFeatureCursor featureCursor = featureClass.Insert(true);
        comReleaser.ManageLifetime(featureCursor);
        IFeatureBuffer featureBuffer = featureClass.CreateFeatureBuffer();
        comReleaser.ManageLifetime(featureBuffer);

        for (int i = 0; i < 1000; i++)
        {
            featureBuffer.Shape = CreateRandomPolygon();
            featureCursor.InsertFeature(featureBuffer);
        }

        featureCursor.Flush();
    }

    // Commit the edits.
    workspaceEdit.AbortEditOperation();
    workspaceEdit.StopEditing(false);
}

在這種情況下確保通知班級的表現好處是非常重要的。 在上述情況下,如果插入了1,000個要素,則無法開啟通知的類通常會導致應用程式執行10-15次,只要通知的類開啟即可。 當一個類觸發多個類的通知時,這個特別重要,因為這個因子乘以被通知的類的數量(即如果通知五個未開啟的類,則執行時間增加50-75倍)。

修改模式物件
每種型別的地理資料庫物件資料集,域,欄位等都在API中具有相應的類。開發人員應該意識到這些類可以分為以下兩類:
那些在地理資料庫(即表)中自動保留模式更改的表
那些沒有的,就是領域,領域,指標
一個經典的例子是IClass.AddField和IFieldsEdit.AddField。當呼叫前者時,API會向資料庫表中新增一個欄位。當後者被呼叫時,一個欄位被新增到記憶體中的欄位集合中,但是不會改變實際的表格。許多開發人員已經發現了開啟一個表,獲取一個欄位集合,並新增一個新的領域是不正確的工作流程的艱難的方式。
其他無效的工作流程包括以下內容:
使用IFieldEdit介面修改已經在地理資料庫中建立的欄位
使用IIndexesEdit介面修改已經在地理資料庫中建立的索引集合
使用IIndexEdit介面修改已經在地理資料庫中建立的索引
另一個類似的工作流程是從工作空間中檢索一個域並對其進行修改,例如,將新的程式碼新增到編碼的值域中。雖然這些更改不會自動保留在地理資料庫中,但可以呼叫IWorkspaceDomains2.AlterDomain以使用修改後的物件覆蓋持久化域。


要使用本主題中的程式碼,請在Visual Studio專案中引用以下程式集。 在程式碼檔案中,您需要使用(C#)或Imports(VB .NET)指令來使用相應的名稱空間(如果與程式集名稱不同,則在下面的括號中給出):
ESRI.ArcGIS.ADF.Connection.Local
ESRI.ArcGIS.Geodatabase
ESRI.ArcGIS.System(ESRI.ArcGIS.esriSystem)