1. 程式人生 > >數據更新接口與延遲更新

數據更新接口與延遲更新

idp 其中 des ech oledb emf 客戶 返回結果 words


title: 數據更新接口與延遲更新
tags: [OLEDB, 數據庫編程, VC++, 數據庫]
date: 2018-02-12 14:29:35
categories: windows 數據庫編程
keywords: OLEDB, 數據庫編程, VC++, 數據庫,數據庫數據更新, 延遲提交
---
在日常使用中,更新數據庫數據經常使用delete 、update等SQL語句進行,但是OLEDB接口提供了額外的接口,來直接修改和更新數據庫數據。

OLEDB數據源更新接口

為何不使用SQL語句進行數據更新

常規情況下,使用SQL語句比較簡單,利用OLEDB中執行SQL語句的方法似乎已經可以進行數據庫的任何操作,普通的增刪改查操作似乎已經夠用了。確實,在某種情況下,這些內容已經夠了,能夠執行SQL語句並得到結果集已經夠了,但是某些情況下並不合適使用SQL語句。
技術分享圖片


SQL語句的執行一般經過這樣幾個步驟:

  1. 數據庫通過sql語句對SQL語句進行分析,生成一些可以被數據庫識別的步驟,在這裏我們叫它計劃任務
  2. 數據庫根據計劃任務中的相關操作,調用對應的核心組件來執行SQL語句中規定的操作。
  3. 將操作得到的結果集返回到應用程序

我們可以簡單的將SQL語句理解為一種運行在數據庫平臺上的一個腳本語言,它與一般的腳本語言一樣需要對每句話進行解釋執行。根據解釋的內容調用對應的功能模塊來完成用戶的請求。如果我們能夠跳過SQL語句的解釋,直接調用對應的核心組件,那麽就能大幅度的提升程序的性能。OLEDB中的數據更新的相關接口就是完成這個操作的。
至此我們可能有點明白為什麽不用SQL語句而是用OLEDB的相關接口來實現對應的更新操作。主要是為了提高效率。

數據修改的相關接口

數據修改的相關接口主要是IRowsetChange,接口中提供的主要方法如下:

  • DeleteRows: 刪除某行
  • InsertRows: 插入行
  • SetData: 更新行

通過這些接口就可以直接對數據庫進行相關的增刪改操作,而由於查詢的操作條件復雜特別是涉及到多表查詢的時候,所以OLEDB沒有提供對應的查詢接口。

更新數據

更新數據需要IRowsetChange接口,而打開該接口需要設置結果集的相關屬性。
一般需要設置下面兩個屬性:

  1. DBPROP_UPDATABILITY:該屬性表示是否允許對數據進行更新,它是一個INT型的數值,該屬性有3個標誌的候選值:DBPROPVAL_UP_CHANGE,允許對數據進行更新,也就是打開SetData方法;DBPROPVAL_UP_DELETE:允許對數據進行刪除,打開的是DeleteRows方法。DBPROPVAL_UP_INSERT,允許插入新行,該標誌打開InsertRows方法。這3個值一般使用或操作設置對應的標識位。
  2. DBPROP_IRowsetUpdate:該屬性是一個BOOL值,表示是否打開IRowsetChange接口。如果要使用IRowsetChange接口,需要將該屬性設置為TRUE。

它們屬於屬性集DBPROPSET_ROWSET。使用命令對象來設置
設置完屬性後,調用Execute執行SQL語句並獲取到接口IRowsetChange。
** PS:使用IRowsetChange接口有一個特殊的要求,必須使用IRowsetChange替代IRowset等接口,作為創建並打開結果集對象的第一個接口。也就是說Execute方法中的最後一個表示結果集對象的參數必須是IRowsetChange。**

數據更新模式

一般來說,使用OLEDB的接口對數據庫中的數據進行操作時,操作的結果是實時的反映到數據庫中的。
對於一般的應用程序來說。采用數據更新的接口雖然在一定程度上解決的效率的問題,但是使用實時更新的模式仍然有一些問題:

  1. 修改立即反映到數據庫中,不利於數據庫中數據完整性維護和數據安全
  2. 如果是網絡中的數據庫,會形成很多小的網絡數據包傳輸,造成網絡傳輸效率低下。

因此OLEDB提供了另外一種更新模式——延遲更新

延遲更新

延遲更新本質上提供了一種將所有更新都在本地中緩存起來,最後再一口氣將所有更新都一次性提交的機制,它與數據庫中的事務不同,事務是將一組操作組織起來,當其中某一步操作失敗,那麽回滾事務中的所有數據,強調的是一個完整性維護,而延遲提交並不會自己校驗某一步是否錯誤,它強調的是將某些更改一次性的提交。
延遲提交與實時提交有下面幾個優點:

  1. 當多個客戶端都在修改數據庫中的數據時,有機會將某些客戶端對數據的修改通知到其他客戶端。
  2. 可以合並對一行數據多列的修改並一次提交到數據源上
  3. 網絡數據庫中可以將對不同表的不同操作合並成一個大的網絡數據包,提高網絡的使用效率。
  4. 當更新不合適的時候有機會進行回滾

打開延遲更新的接口

要使用延遲更新必須申請打開OLEDB的IRowsetUpdate接口,這個申請主要通過設置結果集的DBPROP_IRowsetUpdate屬性來實現,這個屬性是一個bool類型的值。
同時要打開延遲更新的接口必須先打開IRowsetChange接口,所以它們二者一般都是同時打開的。
另外為了方便操作一般也會將結果集的DBPROP_CANHOLDROWS屬性打開,該屬性允許在上一次對某行數據的更改未提交的情況下插入新行。如果不設置該屬性,那麽在調用SetData方法進行更新後就必須調用IRowsetUpdate的Update接口進行提交,否則在提交之前數據庫不允許進行Insert操作(但是允許進行SetData操作)
在使用延遲更新的時候需要註意一個問題。當我們使用了DBPROP_CANHOLDROWS屬性後,數據源為了維護方便,會額外返回一個第0行的數據。通常改行數據是一個INT型,由數據提供者進行維護,它一般是只讀的,如果嘗試對它進行修改可能會返回一個錯誤造成對數據的其他修改操作也失敗。在這種情況下,可以考慮建立2個訪問器,一個包含第0行,只用來做顯示使用,而另外一個更新的訪問器不綁定第0行。
一般情況下可以通過檢測返回結果集中的列信息中的標誌字段來確定哪些列可以進行變更,哪些列是只讀列等標誌來創建多個不同用途的行訪問器
下面是延遲更新的例子:

BOOL ExecSql(IOpenRowset *pIOpenRowset, IRowsetChange* &pIRowsetChange)
{
    COM_DECLARE_BUFFER();
    COM_DECALRE_INTERFACE(IDBCreateCommand);
    COM_DECALRE_INTERFACE(ICommandText);
    COM_DECALRE_INTERFACE(ICommandProperties);

    LPOLESTR lpSql = OLESTR("select * from aa26;");
    BOOL bRet = FALSE;

    //TODO:query出ICommandProperties接口並設置對應屬性
    //設置屬性,允許返回的結果集對數據進行增刪改操作
    dbProp[0].colid = DB_NULLID;
    dbProp[0].dwOptions = DBPROPOPTIONS_REQUIRED;
    dbProp[0].dwPropertyID = DBPROP_UPDATABILITY;
    dbProp[0].vValue.vt = VT_I4;
    dbProp[0].vValue.intVal = DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_DELETE | DBPROPVAL_UP_INSERT;

    //設置屬性,打開IRowsetUpdate接口實現延遲更新
    dbProp[1].colid = DB_NULLID;
    dbProp[1].dwOptions = DBPROPOPTIONS_REQUIRED;
    dbProp[1].dwPropertyID = DBPROP_IRowsetUpdate;
    dbProp[1].vValue.vt = VT_BOOL;
    dbProp[1].vValue.boolVal = VARIANT_TRUE;

    //設置屬性,允許結果集在不提交上次行的更改的情況下插入行
    dbProp[2].colid = DB_NULLID;
    dbProp[2].dwOptions = DBPROPOPTIONS_REQUIRED;
    dbProp[2].dwPropertyID = DBPROP_CANHOLDROWS;
    dbProp[2].vValue.vt = VT_BOOL;
    dbProp[2].vValue.boolVal = VARIANT_TRUE;

    dbPropset[0].cProperties = 3;
    dbPropset[0].guidPropertySet = DBPROPSET_ROWSET;
    dbPropset[0].rgProperties = dbProp;

    hRes = pICommandProperties->SetProperties(1, dbPropset);
    COM_SUCCESS(hRes, _T("設置屬性失敗,錯誤碼為:%08x\n"), hRes);

    hRes = pICommandText->SetCommandText(DBGUID_DEFAULT, lpSql);
    COM_SUCCESS(hRes, _T("設置SQL失敗,錯誤碼為:%08x\n"), hRes);

    hRes = pICommandText->Execute(NULL, IID_IRowsetChange, NULL, NULL, (IUnknown**)&pIRowsetChange);
    COM_SUCCESS(hRes, _T("執行SQL語句失敗,錯誤碼為:%08x\n"), hRes);
    bRet = TRUE;
__CLEAR_UP:
    SAFE_RELEASE(pIDBCreateCommand);
    SAFE_RELEASE(pICommandText);
    SAFE_RELEASE(pICommandProperties);

    return bRet;
}

int _tmain(int argc, TCHAR* argv[])
{
    CoInitialize(NULL);
    COM_DECLARE_BUFFER();
    COM_DECALRE_INTERFACE(IOpenRowset);
    COM_DECALRE_INTERFACE(IRowsetChange);
    COM_DECALRE_INTERFACE(IColumnsInfo);
    COM_DECALRE_INTERFACE(IAccessor);
    COM_DECALRE_INTERFACE(IRowset);
    COM_DECALRE_INTERFACE(IRowsetUpdate);

    HRESULT hRes = S_FALSE;
    DBORDINAL cColumns = 0;
    DBCOLUMNINFO* ColumnsInfo = NULL;
    OLECHAR *pColumnsName = NULL;
    DBBINDING *pDBBindinfo = NULL;
    HACCESSOR hAccessor = NULL;
    HROW* phRow = NULL;
    HROW hNewRow = NULL;
    DBCOUNTITEM cRows = 10; //一次讀取10行
    DBCOUNTITEM dbObtained = 0; //真實讀取的行數

    LPVOID pInsertData = NULL;
    LPVOID pData = NULL;
    LPVOID pCurrData = NULL;

    DWORD dwOffset = 0;
    if(!CreateSession(pIOpenRowset))
    {
        COM_PRINTF(_T("創建回話對象失敗\n"));
        goto __CLEAR_UP;
    }
    
    if (!ExecSql(pIOpenRowset, pIRowsetChange))
    {
        COM_PRINTF(_T("執行SQL語句失敗\n"));
        goto __CLEAR_UP;
    }

    hRes = pIRowsetChange->QueryInterface(IID_IRowset, (void**)&pIRowset);
    COM_SUCCESS(hRes, _T("查詢接口IRowsets失敗,錯誤碼為:%08x\n"), hRes);

    hRes = pIRowset->QueryInterface(IID_IColumnsInfo, (void**)&pIColumnsInfo);
    COM_SUCCESS(hRes, _T("查詢接口IColumnsInfo失敗,錯誤碼為:%08x\n"), hRes);
    
    hRes = pIColumnsInfo->GetColumnInfo(&cColumns, &ColumnsInfo, &pColumnsName);
    COM_SUCCESS(hRes, _T("獲取結果集列信息失敗,錯誤碼為:%08x\n"), hRes);

    pDBBindinfo = (DBBINDING*)MALLOC(sizeof(DBBINDING) * (cColumns - 1));
    for(int i = 0; i < cColumns; i++)
    {
        //取消第0行的綁定,防止修改數據時出錯
        if (ColumnsInfo[i].iOrdinal == 0)
        {
            continue;
        }
        pDBBindinfo[i - 1].iOrdinal = ColumnsInfo[i].iOrdinal;
        pDBBindinfo[i - 1].bPrecision = ColumnsInfo[i].bPrecision;
        pDBBindinfo[i - 1].bScale = ColumnsInfo[i].bScale;
        pDBBindinfo[i - 1].cbMaxLen = ColumnsInfo[i].ulColumnSize * sizeof(WCHAR);
        if (ColumnsInfo[i].wType == DBTYPE_I4)
        {
            //整型數據轉化為字符串時4個字節不夠,需要根據表中的長度來分配對應的空間
            //數據庫中表示行政單位的數據長度為6個字節
            pDBBindinfo[i - 1].cbMaxLen = 7 * sizeof(WCHAR);
        }

        pDBBindinfo[i - 1].dwMemOwner = DBMEMOWNER_CLIENTOWNED;
        pDBBindinfo[i - 1].dwPart = DBPART_STATUS | DBPART_LENGTH | DBPART_VALUE;
        pDBBindinfo[i - 1].eParamIO = DBPARAMIO_NOTPARAM;
        pDBBindinfo[i - 1].obStatus = dwOffset;
        pDBBindinfo[i - 1].obLength = dwOffset + sizeof(DBSTATUS);
        pDBBindinfo[i - 1].obValue = dwOffset + sizeof(DBSTATUS) + sizeof(ULONG);
        pDBBindinfo[i - 1].wType = DBTYPE_WSTR; //為了方便,類型由數據庫進行轉化
        dwOffset = dwOffset + sizeof(DBSTATUS) + sizeof(ULONG) + pDBBindinfo[i - 1].cbMaxLen;
        dwOffset = COM_UPROUND(dwOffset, 8); //8字節對齊
    }

    //讀取數據
    hRes = pIRowsetChange->QueryInterface(IID_IAccessor, (void**)&pIAccessor);
    COM_SUCCESS(hRes, _T("查詢IAccessor接口失敗,錯誤碼為:%08x\n"), hRes);
    hRes = pIAccessor->CreateAccessor(DBACCESSOR_ROWDATA, cColumns - 1, pDBBindinfo, 0, &hAccessor, NULL);
    COM_SUCCESS(hRes, _T("創建訪問器失敗,錯誤碼為:%08x\n"), hRes);
    hRes = pIRowset->GetNextRows(DB_NULL_HCHAPTER, 0, cRows, &dbObtained, &phRow);
    COM_SUCCESS(hRes, _T("獲取行數據失敗,錯誤碼為:%08x\n"), hRes);
    
    pData = MALLOC(cRows * dwOffset);
    for (int i = 0; i < dbObtained; i++)
    {
        pCurrData = (BYTE*)pData + dwOffset * i;
        hRes = pIRowset->GetData(phRow[i], hAccessor, pCurrData); //獲取當前行
        COM_SUCCESS(hRes, _T("獲取數據失敗, 錯誤碼為:%08x\n"), hRes);

        DisplayColumnData(pDBBindinfo, cColumns - 1, pCurrData);

//      將前10行第3列的數據修改為中國
        LPOLESTR pUpdateData = (LPOLESTR)((BYTE*)pCurrData + pDBBindinfo[1].obValue);
        ULONG* pLen = (ULONG*)((BYTE*)pCurrData + pDBBindinfo[1].obLength);
        *pLen = 4;
        StringCchCopy(pUpdateData, pDBBindinfo[1].cbMaxLen, _T("中國"));

        hRes = pIRowsetChange->SetData(phRow[i], hAccessor, pCurrData);
        COM_SUCCESS(hRes, _T("修改數據失敗, 錯誤碼為:%08x\n"), hRes);
    }
    
    //插入一行數據
    pInsertData = MALLOC(dwOffset);
    ZeroMemory(pInsertData, dwOffset);
    
    //為了方便直接在原來數據的基礎上進行修改
    CopyMemory(pInsertData, pData, dwOffset);
    DBSTATUS status[4] = {0};
    ULONG uSize[4] = {0};
    LPOLESTR lpValues[4] = {0};
    
    for (int i = 0; i < 4; i++)
    {
        status[i] = *(DBSTATUS*)((BYTE*)pInsertData + pDBBindinfo[i].obStatus);
        uSize[i] = *(DBSTATUS*)((BYTE*)pInsertData + pDBBindinfo[i].obLength);
        lpValues[i] = (LPOLESTR)((BYTE*)pInsertData + pDBBindinfo[i].obValue);
    }

    StringCchCopy(lpValues[0], pDBBindinfo[0].cbMaxLen, _T("100001"));
    StringCchCopy(lpValues[1], pDBBindinfo[1].cbMaxLen, _T("測試"));
    hRes = pIRowsetChange->InsertRow(NULL, hAccessor, pInsertData, &hNewRow);
    COM_SUCCESS(hRes, _T("插入數據失敗, 錯誤碼為:%08x\n"), hRes);
    
    //刪除一行數據
    hRes = pIRowsetChange->DeleteRows(NULL, 1, &phRow[9], NULL);
    COM_SUCCESS(hRes, _T("查詢IRowsetChange接口失敗,錯誤碼為:%08x\n"), hRes);

    hRes = pIRowsetChange->QueryInterface(IID_IRowsetUpdate, (void**)&pIRowsetUpdate);
    COM_SUCCESS(hRes, _T("查詢IRowsetChange接口失敗,錯誤碼為:%08x\n"), hRes);
    ////提交上述修改
    hRes = pIRowsetUpdate->Update(DB_NULL_HCHAPTER, 0, NULL, NULL, NULL, NULL);
    COM_SUCCESS(hRes, _T("提交數據更新失敗, 錯誤碼為:%08x\n"), hRes);
    
    //取消修改
    //hRes = pIRowsetUpdate->Undo(DB_NULL_HCHAPTER, 0, NULL, NULL, NULL, NULL);
    COM_SUCCESS(hRes, _T("取消更新失敗, 錯誤碼為:%08x\n"), hRes);
    pIRowset->ReleaseRows(dbObtained, phRow, NULL, NULL, NULL);
    pIRowset->ReleaseRows(1, &hNewRow, NULL, NULL, NULL);

__CLEAR_UP:
    SAFE_RELEASE(pIOpenRowset);
    SAFE_RELEASE(pIRowsetChange);
    SAFE_RELEASE(pIColumnsInfo);
    SAFE_RELEASE(pIRowsetUpdate);
    SAFE_RELEASE(pIRowset);
    SAFE_RELEASE(pIAccessor);

    CoTaskMemFree(ColumnsInfo);
    CoTaskMemFree(pColumnsName);

    FREE(pInsertData);
    FREE(pData);
    FREE(pDBBindinfo);
    CoUninitialize();
    return 0;
}

在例子中仍然是首先進行數據庫連接,創建出Session對象,創建Command對象,設置結果集對象的相關屬性並執行sql語句。但是與之前不同的是,在執行SQL語句時不再返回IRowset接口而是返回IRowsetChange接口。然後利用IRowsetChange接口Query出其他需要的接口。接著仍然是綁定,與之前不同的是,在綁定中加了一個判斷。跳過了第0行的綁定,以免它影響到後面的更新操作,然後打印輸出對應的查詢結果。並且在顯示每行數據之後,調用SetData對數據進行更改。
接著準備一個對應的緩沖,放入插入新行的數據。在這為了方便我們直接先拷貝了之前的返回的結果集中的一行的數據,然後再在裏面進行修改,修改後調用InsertRows,插入一行數據。
插入操作完成後,調用DeleteRows函數刪除一行數據
最後調用IRowsetUpdate接口的Update方法提交之前的更新,或者調用Undo取消之前的更改
源代碼查看


數據更新接口與延遲更新