Creazione di un provider aggiornabile
Visual C++ supporta provider o provider aggiornabili in grado di aggiornare (scrivere in) l'archivio dati. In questo argomento viene illustrato come creare provider aggiornabili usando modelli OLE DB.
In questo argomento si presuppone che si inizi con un provider utilizzabile. La creazione di un provider aggiornabile prevede due passaggi. È prima necessario decidere come il provider apporta modifiche all'archivio dati; in particolare, se le modifiche devono essere eseguite immediatamente o posticipate fino a quando non viene eseguito un comando di aggiornamento. La sezione "Creazione di provider aggiornabili" descrive le modifiche e le impostazioni da eseguire nel codice del provider.
Successivamente, è necessario assicurarsi che il provider contenga tutte le funzionalità per supportare qualsiasi elemento che il consumer potrebbe richiederlo. Se il consumer vuole aggiornare l'archivio dati, il provider deve contenere codice che rende persistenti i dati nell'archivio dati. Ad esempio, è possibile usare la libreria di runtime C o MFC per eseguire tali operazioni sull'origine dati. La sezione "Scrittura nell'origine dati" descrive come scrivere nell'origine dati, gestire i valori NULL e predefiniti e impostare i flag di colonna.
Nota
UpdatePV è un esempio di provider aggiornabile. UpdatePV è uguale a MyProv, ma con supporto aggiornabile.
Creazione di provider aggiornabili
La chiave per rendere aggiornabile un provider è comprendere le operazioni che il provider deve eseguire nell'archivio dati e come si vuole che il provider esegua tali operazioni. In particolare, il problema principale è se gli aggiornamenti all'archivio dati devono essere eseguiti immediatamente o posticipati (in batch) fino a quando non viene eseguito un comando di aggiornamento.
È innanzitutto necessario decidere se ereditare da IRowsetChangeImpl
o IRowsetUpdateImpl
nella classe del set di righe. A seconda di quale di queste opzioni si sceglie di implementare, la funzionalità di tre metodi sarà interessata: SetData
, InsertRows
e DeleteRows
.
Se si eredita da IRowsetChangeImpl, la chiamata a questi tre metodi modifica immediatamente l'archivio dati.
Se si eredita da IRowsetUpdateImpl, i metodi rinviano le modifiche all'archivio dati fino a quando non si chiama
Update
,GetOriginalData
oUndo
. Se l'aggiornamento comporta diverse modifiche, vengono eseguite in modalità batch (si noti che l'invio in batch delle modifiche può comportare un notevole sovraccarico di memoria).
Si noti che IRowsetUpdateImpl
deriva da IRowsetChangeImpl
. Di conseguenza, IRowsetUpdateImpl
offre funzionalità di modifica e funzionalità batch.
Per supportare l'updatabilità nel provider
Nella classe del set di righe ereditare da
IRowsetChangeImpl
oIRowsetUpdateImpl
. Queste classi forniscono interfacce appropriate per la modifica dell'archivio dati:Aggiunta di IRowsetChange
Aggiungere
IRowsetChangeImpl
alla catena di ereditarietà usando questo modulo:IRowsetChangeImpl< rowset-name, storage-name >
COM_INTERFACE_ENTRY(IRowsetChange)
Aggiungere anche alla sezione nellaBEGIN_COM_MAP
classe del set di righe.Aggiunta di IRowsetUpdate
Aggiungere
IRowsetUpdate
alla catena di ereditarietà usando questo modulo:IRowsetUpdateImpl< rowset-name, storage>
Nota
È necessario rimuovere la
IRowsetChangeImpl
linea dalla catena di ereditarietà. Questa eccezione alla direttiva menzionata in precedenza deve includere il codice perIRowsetChangeImpl
.Aggiungere quanto segue alla mappa COM (
BEGIN_COM_MAP ... END_COM_MAP
):Se si implementa Aggiungi alla mappa COM IRowsetChangeImpl
COM_INTERFACE_ENTRY(IRowsetChange)
IRowsetUpdateImpl
COM_INTERFACE_ENTRY(IRowsetUpdate)
Se si implementa Aggiungi alla mappa del set di proprietà IRowsetChangeImpl
PROPERTY_INFO_ENTRY_VALUE(IRowsetChange, VARIANT_FALSE)
IRowsetUpdateImpl
PROPERTY_INFO_ENTRY_VALUE(IRowsetUpdate, VARIANT_FALSE)
Nel comando aggiungere quanto segue alla mappa del set di proprietà (
BEGIN_PROPSET_MAP ... END_PROPSET_MAP
):Se si implementa Aggiungi alla mappa del set di proprietà IRowsetChangeImpl
PROPERTY_INFO_ENTRY_VALUE(IRowsetChange, VARIANT_FALSE)
IRowsetUpdateImpl
PROPERTY_INFO_ENTRY_VALUE(IRowsetChange, VARIANT_FALSE)PROPERTY_INFO_ENTRY_VALUE(IRowsetUpdate, VARIANT_FALSE)
Nella mappa del set di proprietà è necessario includere anche tutte le impostazioni seguenti, come appaiono di seguito:
PROPERTY_INFO_ENTRY_VALUE(UPDATABILITY, DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_INSERT | DBPROPVAL_UP_DELETE) PROPERTY_INFO_ENTRY_VALUE(CHANGEINSERTEDROWS, VARIANT_TRUE) PROPERTY_INFO_ENTRY_VALUE(IMMOBILEROWS, VARIANT_TRUE) PROPERTY_INFO_ENTRY_EX(OWNINSERT, VT_BOOL, DBPROPFLAGS_ROWSET | DBPROPFLAGS_READ, VARIANT_TRUE, 0) PROPERTY_INFO_ENTRY_EX(OWNUPDATEDELETE, VT_BOOL, DBPROPFLAGS_ROWSET | DBPROPFLAGS_READ, VARIANT_TRUE, 0) PROPERTY_INFO_ENTRY_EX(OTHERINSERT, VT_BOOL, DBPROPFLAGS_ROWSET | DBPROPFLAGS_READ, VARIANT_TRUE, 0) PROPERTY_INFO_ENTRY_EX(OTHERUPDATEDELETE, VT_BOOL, DBPROPFLAGS_ROWSET | DBPROPFLAGS_READ, VARIANT_TRUE, 0) PROPERTY_INFO_ENTRY_EX(REMOVEDELETED, VT_BOOL, DBPROPFLAGS_ROWSET | DBPROPFLAGS_READ, VARIANT_FALSE, 0)
È possibile trovare i valori usati in queste chiamate di macro esaminando in Atldb.h gli ID e i valori delle proprietà (se Atldb.h differisce dalla documentazione online, Atldb.h sostituisce la documentazione).
Nota
Molte delle
VARIANT_FALSE
impostazioni eVARIANT_TRUE
sono richieste dai modelli OLE DB. La specifica OLE DB indica che possono essere di lettura/scrittura, ma i modelli OLE DB possono supportare un solo valore.Se si implementa IRowsetChangeImpl
Se si implementa
IRowsetChangeImpl
, è necessario impostare le proprietà seguenti nel provider. Queste proprietà vengono usate principalmente per richiedere interfacce tramiteICommandProperties::SetProperties
.DBPROP_IRowsetChange
: l'impostazione di questa impostazione impostaDBPROP_IRowsetChange
automaticamente .DBPROP_UPDATABILITY
: maschera di bit che specifica i metodi supportati inIRowsetChange
:SetData
,DeleteRows
oInsertRow
.DBPROP_CHANGEINSERTEDROWS
: il consumer può chiamareIRowsetChange::DeleteRows
oSetData
per le righe appena inserite.DBPROP_IMMOBILEROWS
: il set di righe non riordina le righe inserite o aggiornate.
Se si implementa IRowsetUpdateImpl
Se si implementa
IRowsetUpdateImpl
, è necessario impostare le proprietà seguenti nel provider, oltre a impostare tutte le proprietà perIRowsetChangeImpl
elencate in precedenza:DBPROP_IRowsetUpdate
.DBPROP_OWNINSERT
: deve essere READ_ONLY AND VARIANT_TRUE.DBPROP_OWNUPDATEDELETE
: deve essere READ_ONLY AND VARIANT_TRUE.DBPROP_OTHERINSERT
: deve essere READ_ONLY AND VARIANT_TRUE.DBPROP_OTHERUPDATEDELETE
: deve essere READ_ONLY AND VARIANT_TRUE.DBPROP_REMOVEDELETED
: deve essere READ_ONLY AND VARIANT_TRUE.DBPROP_MAXPENDINGROWS
.
Nota
Se si supportano le notifiche, potrebbero anche essere presenti altre proprietà; vedere la sezione su
IRowsetNotifyCP
per questo elenco.
Scrittura nell'origine dati
Per leggere dall'origine dati, chiamare la Execute
funzione . Per scrivere nell'origine dati, chiamare la FlushData
funzione . In generale, flush significa salvare le modifiche apportate a una tabella o un indice su disco.
FlushData(HROW, HACCESSOR);
Gli argomenti handle di riga (HROW) e handle di accesso (HACCESSOR) consentono di specificare l'area da scrivere. In genere, si scrive un singolo campo dati alla volta.
Il FlushData
metodo scrive i dati nel formato in cui è stato originariamente archiviato. Se non si esegue l'override di questa funzione, il provider funzionerà correttamente, ma le modifiche non verranno scaricate nell'archivio dati.
Quando scaricare
I modelli di provider chiamano FlushData ogni volta che i dati devono essere scritti nell'archivio dati; questo in genere (ma non sempre) si verifica come risultato di chiamate alle funzioni seguenti:
IRowsetChange::DeleteRows
IRowsetChange::SetData
IRowsetChange::InsertRows
(se sono presenti nuovi dati da inserire nella riga)IRowsetUpdate::Update
Come funziona
Il consumer effettua una chiamata che richiede uno scaricamento (ad esempio Update) e questa chiamata viene passata al provider, che esegue sempre le operazioni seguenti:
Chiama
SetDBStatus
ogni volta che è associato un valore di stato.Controlla i flag di colonna.
Chiama
IsUpdateAllowed
.
Questi tre passaggi consentono di garantire la sicurezza. Il provider chiama FlushData
quindi .
Come implementare FlushData
Per implementare FlushData
, è necessario tenere conto di diversi problemi:
Assicurarsi che l'archivio dati possa gestire le modifiche.
Gestione dei valori NULL.
Gestione dei valori predefiniti.
Per implementare un metodo personalizzato FlushData
, è necessario:
Passare alla classe del set di righe.
Nella classe rowset inserire la dichiarazione di:
HRESULT FlushData(HROW, HACCESSOR) { // Insert your implementation here and return an HRESULT. }
Fornire un'implementazione di
FlushData
.
Una buona implementazione di FlushData
archivia solo le righe e le colonne effettivamente aggiornate. È possibile usare i parametri HROW e HACCESSOR per determinare la riga e la colonna corrente archiviata per l'ottimizzazione.
In genere, la sfida principale consiste nell'usare un archivio dati nativo. Se possibile, provare a:
Mantenere il metodo di scrittura nell'archivio dati il più semplice possibile.
Gestire i valori NULL (facoltativo ma consigliato).
Gestire i valori predefiniti (facoltativo ma consigliato).
La cosa migliore da fare è avere valori specificati effettivi nell'archivio dati per valori NULL e predefiniti. È preferibile estrapolare questi dati. In caso contrario, è consigliabile non consentire valori NULL e predefiniti.
L'esempio seguente illustra come FlushData
viene implementato nella RUpdateRowset
classe nell'esempio UpdatePV
(vedere Rowset.h nel codice di esempio):
///////////////////////////////////////////////////////////////////////////
// class RUpdateRowset (in rowset.h)
...
HRESULT FlushData(HROW, HACCESSOR)
{
ATLTRACE2(atlTraceDBProvider, 0, "RUpdateRowset::FlushData\n");
USES_CONVERSION;
enum {
sizeOfString = 256,
sizeOfFileName = MAX_PATH
};
FILE* pFile = NULL;
TCHAR szString[sizeOfString];
TCHAR szFile[sizeOfFileName];
errcode err = 0;
ObjectLock lock(this);
// From a filename, passed in as a command text,
// scan the file placing data in the data array.
if (m_strCommandText == (BSTR)NULL)
{
ATLTRACE( "RRowsetUpdate::FlushData -- "
"No filename specified\n");
return E_FAIL;
}
// Open the file
_tcscpy_s(szFile, sizeOfFileName, OLE2T(m_strCommandText));
if ((szFile[0] == _T('\0')) ||
((err = _tfopen_s(&pFile, &szFile[0], _T("w"))) != 0))
{
ATLTRACE("RUpdateRowset::FlushData -- Could not open file\n");
return DB_E_NOTABLE;
}
// Iterate through the row data and store it.
for (long l=0; l<m_rgRowData.GetSize(); l++)
{
CAgentMan am = m_rgRowData[l];
_putw((int)am.dwFixed, pFile);
if (_tcscmp(&am.szCommand[0], _T("")) != 0)
_stprintf_s(&szString[0], _T("%s\n"), am.szCommand);
else
_stprintf_s(&szString[0], _T("%s\n"), _T("NULL"));
_fputts(szString, pFile);
if (_tcscmp(&am.szText[0], _T("")) != 0)
_stprintf_s(&szString[0], _T("%s\n"), am.szText);
else
_stprintf_s(&szString[0], _T("%s\n"), _T("NULL"));
_fputts(szString, pFile);
if (_tcscmp(&am.szCommand2[0], _T("")) != 0)
_stprintf_s(&szString[0], _T("%s\n"), am.szCommand2);
else
_stprintf_s(&szString[0], _T("%s\n"), _T("NULL"));
_fputts(szString, pFile);
if (_tcscmp(&am.szText2[0], _T("")) != 0)
_stprintf_s(&szString[0], _T("%s\n"), am.szText2);
else
_stprintf_s(&szString[0], _T("%s\n"), _T("NULL"));
_fputts(szString, pFile);
}
if (fflush(pFile) == EOF || fclose(pFile) == EOF)
{
ATLTRACE("RRowsetUpdate::FlushData -- "
"Couldn't flush or close file\n");
}
return S_OK;
}
Gestione delle modifiche
Affinché il provider gestisca le modifiche, è prima necessario assicurarsi che l'archivio dati (ad esempio un file di testo o un file video) disponga di funzionalità che consentono di apportare modifiche. In caso contrario, è necessario creare il codice separatamente dal progetto del provider.
Gestione dei dati NULL
È possibile che un utente finale invii dati NULL. Quando si scrivono valori NULL nei campi nell'origine dati, possono verificarsi potenziali problemi. Si immagini un'applicazione che accetta valori per la città e il codice postale; potrebbe accettare uno o entrambi i valori, ma non nessuno dei due, perché in questo caso il recapito sarebbe impossibile. È quindi necessario limitare determinate combinazioni di valori NULL nei campi che hanno senso per l'applicazione.
In qualità di sviluppatore del provider, è necessario considerare come archiviare tali dati, come leggerli dall'archivio dati e come specificarlo all'utente. In particolare, è necessario considerare come modificare lo stato dei dati dei set di righe nell'origine dati, ad esempio DataStatus = NULL. Si decide quale valore restituire quando un consumer accede a un campo contenente un valore NULL.
Esaminare il codice nell'esempio UpdatePV; illustra come un provider può gestire i dati NULL. In UpdatePV il provider archivia i dati NULL scrivendo la stringa "NULL" nell'archivio dati. Quando legge i dati NULL dall'archivio dati, viene visualizzata tale stringa e quindi svuota il buffer, creando una stringa NULL. Ha anche un override di IRowsetImpl::GetDBStatus
in cui restituisce DBSTATUS_S_ISNULL se tale valore di dati è vuoto.
Contrassegno di colonne nullable
Se si implementano anche set di righe dello schema (vedere IDBSchemaRowsetImpl
), l'implementazione deve specificare nel set di righe DBSCHEMA_COLUMNS (in genere contrassegnato nel provider da CxxxSchemaColSchemaRowset) che la colonna è nullable.
È anche necessario specificare che tutte le colonne nullable contengano il valore DBCOLUMNFLAGS_ISNULLABLE nella versione di GetColumnInfo
.
Nell'implementazione dei modelli OLE DB, se non si riesce a contrassegnare le colonne come nullable, il provider presuppone che contenga un valore e non consentirà al consumer di inviarlo valori Null.
L'esempio seguente mostra come la CommonGetColInfo
funzione viene implementata in CUpdateCommand (vedere UpProvRS.cpp) in UpdatePV. Si noti che le colonne hanno questo DBCOLUMNFLAGS_ISNULLABLE per le colonne nullable.
/////////////////////////////////////////////////////////////////////////////
// CUpdateCommand (in UpProvRS.cpp)
ATLCOLUMNINFO* CommonGetColInfo(IUnknown* pPropsUnk, ULONG* pcCols, bool bBookmark)
{
static ATLCOLUMNINFO _rgColumns[6];
ULONG ulCols = 0;
if (bBookmark)
{
ADD_COLUMN_ENTRY_EX(ulCols, OLESTR("Bookmark"), 0,
sizeof(DWORD), DBTYPE_BYTES,
0, 0, GUID_NULL, CAgentMan, dwBookmark,
DBCOLUMNFLAGS_ISBOOKMARK)
ulCols++;
}
// Next set the other columns up.
// Add a fixed length entry for OLE DB conformance testing purposes
ADD_COLUMN_ENTRY_EX(ulCols, OLESTR("Fixed"), 1, 4, DBTYPE_UI4,
10, 255, GUID_NULL, CAgentMan, dwFixed,
DBCOLUMNFLAGS_WRITE |
DBCOLUMNFLAGS_ISFIXEDLENGTH)
ulCols++;
ADD_COLUMN_ENTRY_EX(ulCols, OLESTR("Command"), 2, 16, DBTYPE_STR,
255, 255, GUID_NULL, CAgentMan, szCommand,
DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE)
ulCols++;
ADD_COLUMN_ENTRY_EX(ulCols, OLESTR("Text"), 3, 16, DBTYPE_STR,
255, 255, GUID_NULL, CAgentMan, szText,
DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE)
ulCols++;
ADD_COLUMN_ENTRY_EX(ulCols, OLESTR("Command2"), 4, 16, DBTYPE_STR,
255, 255, GUID_NULL, CAgentMan, szCommand2,
DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE)
ulCols++;
ADD_COLUMN_ENTRY_EX(ulCols, OLESTR("Text2"), 5, 16, DBTYPE_STR,
255, 255, GUID_NULL, CAgentMan, szText2,
DBCOLUMNFLAGS_WRITE | DBCOLUMNFLAGS_ISNULLABLE)
ulCols++;
if (pcCols != NULL)
{
*pcCols = ulCols;
}
return _rgColumns;
}
Valori predefiniti
Come per i dati NULL, si ha la responsabilità di gestire la modifica dei valori predefiniti.
Il valore predefinito di FlushData
e Execute
deve restituire S_OK. Pertanto, se non si esegue l'override di questa funzione, le modifiche sembrano avere esito positivo (S_OK verranno restituite), ma non verranno trasmesse all'archivio dati.
Nell'esempio UpdatePV
(in Rowset.h), il SetDBStatus
metodo gestisce i valori predefiniti come indicato di seguito:
virtual HRESULT SetDBStatus(DBSTATUS* pdbStatus, CSimpleRow* pRow,
ATLCOLUMNINFO* pColInfo)
{
ATLASSERT(pRow != NULL && pColInfo != NULL && pdbStatus != NULL);
void* pData = NULL;
char* pDefaultData = NULL;
DWORD* pFixedData = NULL;
switch (*pdbStatus)
{
case DBSTATUS_S_DEFAULT:
pData = (void*)&m_rgRowData[pRow->m_iRowset];
if (pColInfo->wType == DBTYPE_STR)
{
pDefaultData = (char*)pData + pColInfo->cbOffset;
strcpy_s(pDefaultData, "Default");
}
else
{
pFixedData = (DWORD*)((BYTE*)pData +
pColInfo->cbOffset);
*pFixedData = 0;
return S_OK;
}
break;
case DBSTATUS_S_ISNULL:
default:
break;
}
return S_OK;
}
Flag di colonna
Se si supportano i valori predefiniti nelle colonne, è necessario impostarlo usando i metadati nella <classe del provider SchemaRowset classe>. Impostare m_bColumnHasDefault = VARIANT_TRUE
.
È anche responsabilità dell'utente impostare i flag di colonna, specificati usando il tipo enumerato DBCOLUMNFLAGS. I flag di colonna descrivono le caratteristiche delle colonne.
Ad esempio, nella CUpdateSessionColSchemaRowset
classe in UpdatePV
(in Session.h), la prima colonna viene configurata in questo modo:
// Set up column 1
trData[0].m_ulOrdinalPosition = 1;
trData[0].m_bIsNullable = VARIANT_FALSE;
trData[0].m_bColumnHasDefault = VARIANT_TRUE;
trData[0].m_nDataType = DBTYPE_UI4;
trData[0].m_nNumericPrecision = 10;
trData[0].m_ulColumnFlags = DBCOLUMNFLAGS_WRITE |
DBCOLUMNFLAGS_ISFIXEDLENGTH;
lstrcpyW(trData[0].m_szColumnDefault, OLESTR("0"));
m_rgRowData.Add(trData[0]);
Questo codice specifica, tra le altre cose, che la colonna supporta un valore predefinito pari a 0, che sia scrivibile e che tutti i dati nella colonna abbiano la stessa lunghezza. Se si desidera che i dati in una colonna abbiano una lunghezza variabile, non impostare questo flag.