Cosa fa Access quando un utente apporta modifiche ai dati su una tabella collegata ODBC?
La nostra serie di analisi ODBC continua e in questo quarto articolo spiegheremo come inserire e aggiornare un record di dati in un recordset, nonché il processo di eliminazione di un record. Nell'articolo precedente, abbiamo appreso come Access gestisce il popolamento dei dati dalle origini ODBC. Abbiamo visto che il tipo di recordset ha un effetto importante su come Access formulerà le query all'origine dati ODBC. Ancora più importante, abbiamo riscontrato che con un recordset di tipo dynaset, Access esegue operazioni aggiuntive per disporre di tutte le informazioni necessarie per poter selezionare una singola riga utilizzando una chiave. Ciò si applicherà in questo articolo in cui esploreremo come vengono gestite le modifiche ai dati. Inizieremo con gli inserimenti, che è l'operazione più complicata, per poi passare agli aggiornamenti e infine alle eliminazioni.
Inserimento di un record in un recordset
Il comportamento di inserimento di un recordset di tipo dynaset dipenderà dal modo in cui Access percepisce le chiavi della tabella sottostante. Ci saranno 3 comportamenti distinti. I primi due riguardano la gestione delle chiavi primarie che vengono generate automaticamente dal server in qualche modo. Il secondo è un caso speciale del primo comportamento applicabile solo con il back-end di SQL Server che utilizza un IDENTITY
colonna. L'ultimo riguarda il caso in cui le chiavi vengono fornite dall'utente (es. chiavi naturali nell'ambito dell'inserimento dei dati). Inizieremo con il caso più generale delle chiavi generate dal server.
Inserimento di un record; una tabella con chiave primaria generata dal server
Quando inseriamo un recordset (di nuovo, come lo facciamo, tramite l'interfaccia utente di Access o VBA non importa), Access deve fare le cose per aggiungere la nuova riga alla cache locale.
La cosa importante da notare è che Access ha comportamenti di inserimento diversi a seconda di come è impostata la chiave. In questo caso, le Cities
la tabella non ha un IDENTITY
attributo ma utilizza invece una SEQUENCE
oggetto per generare una nuova chiave. Ecco l'SQL tracciato formattato:
SQLExecDirect: INSERT INTO "Application"."Cities" ( "CityName" ,"StateProvinceID" ,"LatestRecordedPopulation" ,"LastEditedBy" ) VALUES ( ? ,? ,? ,?) SQLPrepare: SELECT "CityID" ,"CityName" ,"StateProvinceID" ,"Location" ,"LatestRecordedPopulation" ,"LastEditedBy" ,"ValidFrom" ,"ValidTo" FROM "Application"."Cities" WHERE "CityID" IS NULL SQLExecute: (GOTO BOOKMARK) SQLExecDirect: SELECT "Application"."Cities"."CityID" FROM "Application"."Cities" WHERE "CityName" = ? AND "StateProvinceID" = ? AND "LatestRecordedPopulation" = ? AND "LastEditedBy" = ? SQLExecute: (GOTO BOOKMARK) SQLExecute: (MULTI-ROW FETCH)Tieni presente che Access invierà solo le colonne che sono state effettivamente modificate dall'utente. Anche se la query stessa includeva più colonne, abbiamo modificato solo 4 colonne, quindi Access includerà solo quelle. Ciò garantisce che Access non interferisca con il comportamento predefinito impostato per le altre colonne che l'utente non ha modificato, poiché Access non ha conoscenze specifiche su come l'origine dati gestirà tali colonne. Oltre a ciò, la dichiarazione di inserimento è praticamente ciò che ci aspetteremmo.
La seconda affermazione, tuttavia, è un po' strana. Seleziona per WHERE "CityID" IS NULL
. Sembra impossibile, poiché sappiamo già che il CityID
colonna è una chiave primaria e per definizione non può essere nulla. Tuttavia, se guardi lo screenshot, non abbiamo mai modificato il CityID
colonna. Dal POV di Access, è NULL
. Molto probabilmente, Access adotta un approccio pessimistico e non presuppone che l'origine dati aderisca effettivamente allo standard SQL. Come abbiamo visto nella sezione relativa a come Access seleziona un indice da utilizzare per identificare in modo univoco una riga, potrebbe non essere una chiave primaria ma semplicemente un UNIQUE
indice che può consentire NULL
. Per quel caso limite improbabile, esegue una query solo per assicurarsi che l'origine dati non abbia effettivamente creato un nuovo record con quel valore. Dopo aver verificato che non sono stati restituiti dati, tenta di individuare nuovamente il record con il seguente filtro:
WHERE "CityName" = ? AND "StateProvinceID" = ? AND "LatestRecordedPopulation" = ? AND "LastEditedBy" = ?che erano le stesse 4 colonne effettivamente modificate dall'utente. Poiché c'era solo una città chiamata "Zeke", abbiamo recuperato solo un record e quindi Access può quindi popolare la cache locale con il nuovo record con gli stessi dati dell'origine dati. Incorporerà tutte le modifiche alle altre colonne, dal momento che
SELECT
l'elenco include solo il CityID
chiave, che utilizzerà quindi nella sua istruzione già preparata per poi popolare l'intera riga utilizzando il CityID
chiave. Inserimento di un record; una tabella con chiave primaria autoincrementante
Tuttavia, cosa succede se la tabella proviene da un database SQL Server e dispone di una colonna a incremento automatico come IDENTITY
attributo? L'accesso si comporta in modo diverso. Quindi creiamo una copia di Cities
tabella ma modifica in modo che il CityID
la colonna è ora un IDENTITY
colonna.
Vediamo come Access gestisce questo:
SQLExecDirect: INSERT INTO "Application"."Cities" ( "CityName" ,"StateProvinceID" ,"LatestRecordedPopulation" ,"LastEditedBy" ,"ValidFrom" ,"ValidTo" ) VALUES ( ? ,? ,? ,? ,? ,?) SQLExecDirect: SELECT @@IDENTITY SQLExecute: (GOTO BOOKMARK) SQLExecute: (GOTO BOOKMARK)C'è significativamente meno chiacchiere; facciamo semplicemente un
SELECT @@IDENTITY
per trovare l'identità appena inserita. Sfortunatamente, questo non è un comportamento generale. Ad esempio, MySQL supporta la possibilità di eseguire un SELECT @@IDENTITY
, tuttavia, Access non fornirà questo comportamento. Il driver ODBC PostgreSQL ha una modalità per emulare SQL Server al fine di indurre Access a inviare il @@IDENTITY
a PostgreSQL in modo che possa essere mappato sull'equivalente serial
tipo di dati. Inserimento di un record con un valore esplicito per chiave primaria
Facciamo un terzo esperimento usando una tabella con un normale int
colonna, senza un IDENTITY
attributo. Anche se sarà ancora una chiave primaria sul tavolo, vorremo vedere come si comporta quando inseriamo esplicitamente la chiave noi stessi.
SQLExecDirect: INSERT INTO "Application"."Cities" ( "CityID" ,"CityName" ,"StateProvinceID" ,"LatestRecordedPopulation" ,"LastEditedBy" ,"ValidFrom" ,"ValidTo" ) VALUES ( ? ,? ,? ,? ,? ,? ,? ) SQLExecute: (GOTO BOOKMARK) SQLExecute: (MULTI-ROW FETCH)Questa volta, non c'è ginnastica extra; poiché abbiamo già fornito il valore per la chiave primaria, Access sa che non deve cercare di trovare nuovamente la riga; esegue semplicemente l'istruzione preparata per risincronizzare la riga inserita. Tornando al design originale in cui le
Cities
la tabella utilizzava una SEQUENCE
oggetto per generare una nuova chiave, possiamo aggiungere una funzione VBA per recuperare il nuovo numero usando NEXT VALUE FOR
e quindi popolare la chiave in modo proattivo per ottenere questo comportamento. Questo si avvicina più da vicino al funzionamento del motore di database di Access; non appena sporchiamo un record, recupera una nuova chiave da AutoNumber
tipo di dati, piuttosto che attendere che il record sia stato effettivamente inserito. Pertanto, se il tuo database utilizza SEQUENCE
o altri modi per creare chiavi, potrebbe essere utile fornire un meccanismo per recuperare la chiave in modo proattivo per eliminare le congetture che abbiamo visto fare in Access con il primo esempio. Aggiornamento di un record in un recordset
A differenza degli inserti nella sezione precedente, gli aggiornamenti sono relativamente più facili perché abbiamo già la chiave presente. Pertanto, Access di solito si comporta in modo più diretto quando si tratta di aggiornare. Ci sono due comportamenti principali che dobbiamo considerare durante l'aggiornamento di un record che dipende dalla presenza di una colonna rowversion.
Aggiornamento di un record senza una colonna rowversion
Supponiamo di modificare solo una colonna. Questo è ciò che vediamo in ODBC.
SQLExecute: (GOTO BOOKMARK) SQLExecDirect: UPDATE "Application"."Cities" SET "CityName"=? WHERE "CityID" = ? AND "CityName" = ? AND "StateProvinceID" = ? AND "Location" IS NULL AND "LatestRecordedPopulation" = ? AND "LastEditedBy" = ? AND "ValidFrom" = ? AND "ValidTo" = ?Hmm, qual è il problema con tutte quelle colonne extra che non abbiamo modificato? Bene, ancora una volta, Access deve adottare una prospettiva pessimistica. Si deve presumere che qualcuno potrebbe aver modificato i dati mentre l'utente stava lentamente armeggiando con le modifiche. Ma come farebbe Access a sapere che qualcun altro ha modificato i dati sul server? Bene, logicamente, se tutte le colonne sono esattamente le stesse, allora avrebbe dovuto aggiornare solo una riga, giusto? Questo è ciò che Access cerca quando confronta tutte le colonne; per garantire che solo l'aggiornamento influisca esattamente su una riga. Se rileva che ha aggiornato più di una riga o zero righe, esegue il rollback dell'aggiornamento e restituisce un errore o
#Deleted
all'utente. Ma... è un po' inefficiente, vero? Inoltre, ciò potrebbe causare problemi se esiste una logica lato server che potrebbe modificare i valori immessi dall'utente. Per illustrare, supponiamo di aggiungere un trigger stupido che cambia il nome della città (non lo consigliamo, ovviamente):
CREATE TRIGGER SillyTrigger ON Application.Cities AFTER UPDATE AS BEGIN UPDATE Application.Cities SET CityName = 'zzzzz' WHERE EXISTS ( SELECT NULL FROM inserted AS i WHERE Cities.CityID = i.CityID ); END;Quindi se poi proviamo ad aggiornare una riga cambiando il nome della città, sembrerà che ci sia riuscita.
Ma se poi proviamo a modificarlo di nuovo, riceviamo un messaggio di errore con il messaggio aggiornato:
Questo è l'output di sqlout.txt
:
SQLExecDirect: UPDATE "Application"."Cities" SET "CityName"=? WHERE "CityID" = ? AND "CityName" = ? AND "StateProvinceID" = ? AND "Location" IS NULL AND "LatestRecordedPopulation" = ? AND "LastEditedBy" = ? AND "ValidFrom" = ? AND "ValidTo" = ? SQLExecute: (GOTO BOOKMARK) SQLExecute: (GOTO BOOKMARK) SQLExecute: (MULTI-ROW FETCH) SQLExecute: (MULTI-ROW FETCH)È importante notare che il 2°
GOTO BOOKMARK
e il successivo MULTI-ROW FETCH
es non si è verificato fino a quando non abbiamo ricevuto il messaggio di errore e lo abbiamo respinto. Il motivo è che mentre sporchiamo un record, Access esegue un GOTO BOOKMARK
, ci rendiamo conto che i dati restituiti non corrispondono più a quelli presenti nella cache, il che ci fa ricevere il messaggio "I dati sono stati modificati". Ciò ci impedisce di perdere tempo a modificare un record destinato a fallire perché è già obsoleto. Si noti che Access alla fine scoprirebbe anche la modifica se gli dessimo tempo sufficiente per aggiornare i dati. In tal caso, non ci sarebbe alcun messaggio di errore; il foglio dati verrebbe semplicemente aggiornato per mostrare i dati corretti.
In quei casi, tuttavia, Access aveva la chiave giusta, quindi non ha avuto problemi a scoprire i nuovi dati. Ma se è la chiave a essere fragile? Se il trigger avesse modificato la chiave primaria o l'origine dati ODBC non rappresentasse il valore esattamente come pensava che sarebbe stato Access, ciò causerebbe la visualizzazione del record come #Deleted
poiché non può sapere se è stato modificato dal server o da qualcun altro rispetto a se è stato legittimamente eliminato da qualcun altro.
Aggiornamento di un record con la colonna rowversion
In entrambi i casi, viene visualizzato un messaggio di errore o #Deleted
può essere abbastanza fastidioso. Ma c'è un modo per evitare che Access confronti tutte le colonne. Rimuoviamo il trigger e aggiungiamo una nuova colonna:
ALTER TABLE Application.Cities ADD RV rowversion NOT NULL;Aggiungiamo una
rowversion
che ha la proprietà di essere esposto a ODBC come avente SQLSpecialColumns(SQL_ROWVER)
, che è ciò che Access deve sapere che può essere utilizzato come un modo per eseguire la versione della riga. Diamo un'occhiata a come funzionano gli aggiornamenti con questa modifica. SQLExecDirect: UPDATE "Application"."Cities" SET "CityName"=? WHERE "CityID" = ? AND "RV" = ? SQLExecute: (GOTO BOOKMARK)A differenza dell'esempio precedente in cui Access ha confrontato il valore in ciascuna colonna, indipendentemente dal fatto che l'utente lo abbia modificato o meno, aggiorniamo il record solo utilizzando il
RV
come criteri di filtro. Il ragionamento è che se il RV
ha ancora lo stesso valore di quello che Access ha passato, quindi Access può essere sicuro che questa riga non è stata modificata da nessun altro perché se lo fosse, allora il RV
il valore sarebbe cambiato. Significa anche che se un trigger ha alterato i dati o se SQL Server e Access non rappresentavano un valore esattamente allo stesso modo (ad es. numeri mobili), Access non si bloccherà quando riseleziona la riga aggiornata e torna con un valore diverso valori in altre colonne che gli utenti non hanno modificato.
NOTA :Non tutti i prodotti DBMS utilizzeranno gli stessi termini. Ad esempio, il timestamp
di MySQL può essere utilizzato come versione di riga per gli scopi di ODBC. Dovrai consultare la documentazione del prodotto per vedere se supportano la funzione rowversion in modo da poter sfruttare questo comportamento con Access.
Viste e versione di riga
Le visualizzazioni sono anche influenzate dalla presenza o dall'assenza di una versione di riga. Supponiamo di creare una vista in SQL Server con la definizione:
CREATE VIEW dbo.vwCities AS SELECT CityID, CityName FROM Application.Cities;L'aggiornamento di un record nella vista ripristina il confronto colonna per colonna come se la colonna rowversion non esistesse nella tabella:
SQLExecDirect: UPDATE "dbo"."vwCities" SET "CityName"=? WHERE "CityID" = ? AND "CityName" = ?Pertanto, se è necessario il comportamento di aggiornamento basato su rowversion, è necessario assicurarsi che le colonne rowversion siano incluse nelle viste. Nel caso di una vista che contiene più tabelle nei join, è meglio includere almeno le colonne rowversion dalle tabelle in cui intendi aggiornare. Poiché in genere è possibile aggiornare solo una tabella, includere solo una versione di riga può essere sufficiente come regola generale.
Eliminazione di un record in un recordset
L'eliminazione di un record si comporta in modo simile agli aggiornamenti e utilizzerà anche rowversion, se disponibile. In una tabella senza una versione di riga, otteniamo:
SQLExecDirect: DELETE FROM "Application"."Cities" WHERE "CityID" = ? AND "CityName" = ? AND "StateProvinceID" = ? AND "Location" IS NULL AND "LatestRecordedPopulation" = ? AND "LastEditedBy" = ? AND "ValidFrom" = ? AND "ValidTo" = ?In una tabella con una versione di riga, otteniamo:
SQLExecDirect: DELETE FROM "Application"."Cities" WHERE "CityID" = ? AND "RV" = ?Ancora una volta, Access deve essere pessimista sull'eliminazione in quanto riguarda l'aggiornamento; non vorrebbe eliminare una riga che è stata modificata da qualcun altro. Quindi utilizza lo stesso comportamento che abbiamo visto con l'aggiornamento per evitare che più utenti cambino gli stessi record.
Conclusioni
Abbiamo imparato come Access gestisce le modifiche ai dati e mantiene la sua cache locale sincronizzata con l'origine dati ODBC. Abbiamo visto quanto fosse pessimista Access, guidato dalla necessità di supportare il maggior numero possibile di origini dati ODBC senza fare affidamento su presupposti o aspettative specifiche che tali origini dati ODBC supporteranno una determinata funzionalità. Per questo motivo, abbiamo visto che Access si comporterà in modo diverso a seconda di come viene definita la chiave per una determinata tabella collegata ODBC. Se fossimo in grado di inserire in modo esplicito una nuova chiave, ciò richiedeva il lavoro minimo da parte di Access per risincronizzare la cache locale per il record appena inserito. Tuttavia, se consentiamo al server di popolare la chiave, Access dovrà eseguire un lavoro aggiuntivo in background per risincronizzarsi.
Abbiamo anche visto che avere una colonna sulla tabella che può essere usata come una versione di riga può aiutare a ridurre le chiacchiere tra Access e l'origine dati ODBC in un aggiornamento. Dovresti consultare la documentazione del driver ODBC per determinare se supporta la versione riga a livello ODBC e, in tal caso, includere tale colonna nelle tabelle o nelle viste prima di collegarti ad Access per sfruttare i vantaggi degli aggiornamenti basati sulla versione riga.
Ora sappiamo che per qualsiasi aggiornamento o eliminazione, Access tenterà sempre di verificare che la riga non sia stata modificata dall'ultima volta che è stata recuperata da Access, per impedire agli utenti di apportare modifiche che potrebbero essere impreviste. Tuttavia, dobbiamo considerare gli effetti derivanti dall'apportare modifiche in altri luoghi (ad es. trigger lato server, esecuzione di una query diversa in un'altra connessione) che possono far sì che Access concluda che la riga è stata modificata e quindi non consenta la modifica. Tali informazioni ci aiuteranno ad analizzare ed evitare di creare una sequenza di modifiche ai dati che potrebbero contraddire le aspettative di Access quando risincronizza la cache locale.
Nel prossimo articolo esamineremo gli effetti dell'applicazione di filtri su un recordset.
Chiedi aiuto ai nostri esperti di accesso oggi stesso. Chiama il nostro team al numero 773-809-5456 o inviaci un'e-mail a [email protected].