O que o Access está fazendo quando um usuário faz alterações nos dados em uma tabela vinculada ODBC?
Nossa série de rastreamento ODBC continua e neste quarto artigo explicaremos como inserir e atualizar um registro de dados em um conjunto de registros, bem como o processo de exclusão de um registro. No artigo anterior, aprendemos como o Access lida com o preenchimento de dados de fontes ODBC. Vimos que o tipo de conjunto de registros tem um efeito importante em como o Access formulará as consultas à fonte de dados ODBC. Mais importante, descobrimos que com um conjunto de registros do tipo dynaset, o Access realiza trabalho adicional para ter todas as informações necessárias para poder selecionar uma única linha usando uma chave. Isso será aplicado neste artigo, onde exploramos como as modificações de dados são tratadas. Começaremos com inserções, que é a operação mais complicada, depois passaremos para atualizações e, finalmente, exclusões.
Inserindo um registro em um conjunto de registros
O comportamento de inserção de um conjunto de registros do tipo dynaset dependerá de como o Access percebe as chaves da tabela subjacente. Haverá 3 comportamentos distintos. Os dois primeiros lidam com o manuseio de chaves primárias que são geradas automaticamente pelo servidor de alguma forma. O segundo é um caso especial do primeiro comportamento que é aplicável apenas com o back-end do SQL Server usando um
IDENTITY
coluna. O último trata do caso em que as chaves são fornecidas pelo usuário (por exemplo, chaves naturais como parte da entrada de dados). Começaremos com o caso mais geral de chaves geradas pelo servidor. Inserindo um registro; uma tabela com chave primária gerada pelo servidor
Quando inserimos um conjunto de registros (novamente, como fazemos isso, via Access UI ou VBA não importa), o Access deve fazer coisas para adicionar a nova linha ao cache local.
O importante a ser observado é que o Access tem diferentes comportamentos de inserção dependendo de como a chave está configurada. Neste caso, as
Cities
a tabela não tem uma IDENTITY
atributo, mas usa um SEQUENCE
objeto para gerar uma nova chave. Aqui está o SQL rastreado formatado: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)Observe que o Access enviará apenas colunas que foram realmente modificadas pelo usuário. Embora a consulta em si inclua mais colunas, editamos apenas 4 colunas, portanto, o Access incluirá apenas essas. Isso garante que o Access não interfira no comportamento padrão definido para as outras colunas que o usuário não modificou, pois o Access não tem conhecimento específico sobre como a fonte de dados tratará essas colunas. Além disso, a instrução insert é praticamente o que esperaríamos.
A segunda afirmação, no entanto, é um pouco estranha. Seleciona para
WHERE "CityID" IS NULL
. Isso parece impossível, pois já sabemos que o CityID
coluna é uma chave primária e, por definição, não pode ser nula. No entanto, se você observar a captura de tela, nunca modificamos o CityID
coluna. Do ponto de vista do Access, é NULL
. Muito provavelmente, o Access adota uma abordagem pessimista e não assume que a fonte de dados irá de fato aderir ao padrão SQL. Como vimos na seção discutindo como o Access seleciona um índice a ser usado para identificar exclusivamente uma linha, pode não ser uma chave primária, mas apenas um UNIQUE
índice que pode permitir NULL
. Para esse caso de borda improvável, ele faz uma consulta apenas para garantir que a fonte de dados não tenha realmente criado um novo registro com esse valor. Depois de verificar que não houve retorno de dados, ele tenta localizar o registro novamente com o seguinte filtro:WHERE "CityName" = ? AND "StateProvinceID" = ? AND "LatestRecordedPopulation" = ? AND "LastEditedBy" = ?que eram as mesmas 4 colunas que o usuário realmente modificou. Como havia apenas uma cidade chamada “Zeke”, obtivemos apenas um registro de volta e, portanto, o Access pode preencher o cache local com o novo registro com os mesmos dados que a fonte de dados o possui. Ele irá incorporar quaisquer alterações em outras colunas, pois o
SELECT
lista inclui apenas o CityID
key, que ele usará em sua instrução já preparada para preencher a linha inteira usando o CityID
chave. Inserindo um registro; uma tabela com chave primária de autoincremento
No entanto, e se a tabela vier de um banco de dados SQL Server e tiver uma coluna de incremento automático, como
IDENTITY
atributo? O acesso se comporta de maneira diferente. Então, vamos criar uma cópia do Cities
mas edite para que o CityID
coluna agora é uma IDENTITY
coluna. Vamos ver como o Access lida com isso:
SQLExecDirect: INSERT INTO "Application"."Cities" ( "CityName" ,"StateProvinceID" ,"LatestRecordedPopulation" ,"LastEditedBy" ,"ValidFrom" ,"ValidTo" ) VALUES ( ? ,? ,? ,? ,? ,?) SQLExecDirect: SELECT @@IDENTITY SQLExecute: (GOTO BOOKMARK) SQLExecute: (GOTO BOOKMARK)Há significativamente menos conversas; nós simplesmente fazemos um
SELECT @@IDENTITY
para encontrar a identidade recém-inserida. Infelizmente, este não é um comportamento geral. Por exemplo, o MySQL suporta a habilidade de fazer um SELECT @@IDENTITY
, no entanto, o Access não fornecerá esse comportamento. O driver ODBC do PostgreSQL tem um modo para emular o SQL Server para enganar o Access para enviar o @@IDENTITY
para o PostgreSQL para que ele possa mapear para o equivalente serial
tipo de dados. Inserindo um registro com um valor explícito para chave primária
Vamos fazer um terceiro experimento usando uma tabela com um
int
normal coluna, sem uma IDENTITY
atributo. Embora ainda seja uma chave primária na mesa, queremos ver como ela se comporta quando inserimos explicitamente a chave nós mesmos. SQLExecDirect: INSERT INTO "Application"."Cities" ( "CityID" ,"CityName" ,"StateProvinceID" ,"LatestRecordedPopulation" ,"LastEditedBy" ,"ValidFrom" ,"ValidTo" ) VALUES ( ? ,? ,? ,? ,? ,? ,? ) SQLExecute: (GOTO BOOKMARK) SQLExecute: (MULTI-ROW FETCH)Desta vez, não há ginástica extra; como já fornecemos o valor da chave primária, o Access sabe que não precisa tentar encontrar a linha novamente; ele apenas executa a instrução preparada para ressincronizar a linha inserida. Voltando ao design original onde as
Cities
tabela usou uma SEQUENCE
objeto para gerar uma nova chave, podemos adicionar uma função VBA para buscar o novo número usando NEXT VALUE FOR
e, assim, preencher a chave proativamente para obter esse comportamento. Isso se aproxima mais de como o mecanismo de banco de dados do Access funciona; assim que sujamos um registro, ele busca uma nova chave do AutoNumber
tipo de dados, em vez de esperar até que o registro tenha sido realmente inserido. Assim, se seu banco de dados usa SEQUENCE
ou outras formas de criar chaves, pode valer a pena fornecer um mecanismo de busca da chave proativamente para ajudar a eliminar as suposições que vimos o Access fazendo no primeiro exemplo. Atualizando um registro em um conjunto de registros
Ao contrário das inserções na seção anterior, as atualizações são relativamente mais fáceis porque já temos a chave presente. Assim, o Access costuma se comportar de forma mais direta quando se trata de atualização. Há dois comportamentos principais que precisamos considerar ao atualizar um registro que depende da presença de uma coluna rowversion.
Atualizando um registro sem uma coluna rowversion
Suponha que modifiquemos apenas uma coluna. Isso é o que vemos no 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 é o problema com todas aquelas colunas extras que não modificamos? Bem, novamente, o Access tem que adotar uma visão pessimista. É preciso supor que alguém possivelmente poderia ter alterado os dados enquanto o usuário estava lentamente se atrapalhando com as edições. Mas como o Access saberia que outra pessoa alterou os dados no servidor? Bem, logicamente, se todas as colunas são exatamente iguais, deveria ter atualizado apenas uma linha, certo? É isso que o Access procura quando compara todas as colunas; para garantir que a atualização afetará apenas uma linha. Se descobrir que atualizou mais de uma linha ou nenhuma linha, ele reverte a atualização e retorna um erro ou
#Deleted
ao usuário. Mas... isso é meio ineficiente, não é? Além disso, isso pode trazer problemas se houver lógica do lado do servidor que possa alterar os valores inseridos pelo usuário. Para ilustrar, suponha que adicionamos um gatilho bobo que altera o nome da cidade (não recomendamos isso, é claro):
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;Portanto, se tentarmos atualizar uma linha alterando o nome da cidade, ela parecerá ter sido bem-sucedida.
Mas se tentarmos editá-lo novamente, recebemos uma mensagem de erro com uma mensagem atualizada:
Esta é a saída de
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 observar que o segundo
GOTO BOOKMARK
e o subsequente MULTI-ROW FETCH
es não aconteceu até que recebemos a mensagem de erro e a descartamos. A razão é que, à medida que sujamos um registro, o Access executa um GOTO BOOKMARK
, percebemos que os dados retornados não correspondem mais aos que estão no cache, o que nos faz receber a mensagem “Os dados foram alterados”. Isso nos impede de perder tempo editando um registro que está fadado ao fracasso porque já está obsoleto. Observe que o Access também descobriria a alteração se dermos tempo suficiente para atualizar os dados. Nesse caso, não haveria mensagem de erro; a folha de dados seria simplesmente atualizada para mostrar os dados corretos. Nesses casos, porém, o Access tinha a chave certa para que não houvesse problemas para descobrir os novos dados. Mas se é a chave que é frágil? Se o gatilho tivesse alterado a chave primária ou a fonte de dados ODBC não representasse o valor exatamente como o Access pensava, isso faria com que o Access pintasse o registro como
#Deleted
uma vez que não pode saber se foi editado pelo servidor ou por outra pessoa versus se foi legitimamente excluído por outra pessoa. Atualizando um registro com a coluna rowversion
De qualquer forma, recebendo uma mensagem de erro ou
#Deleted
pode ser bastante irritante. Mas existe uma maneira de evitar que o Access compare todas as colunas. Vamos remover o gatilho e adicionar uma nova coluna:ALTER TABLE Application.Cities ADD RV rowversion NOT NULL;Adicionamos uma
rowversion
que tem a propriedade de ser exposto ao ODBC como tendo SQLSpecialColumns(SQL_ROWVER)
, que é o que o Access precisa saber para que possa ser usado como uma forma de versionar a linha. Vejamos como as atualizações funcionam com essa alteração. SQLExecDirect: UPDATE "Application"."Cities" SET "CityName"=? WHERE "CityID" = ? AND "RV" = ? SQLExecute: (GOTO BOOKMARK)Ao contrário do exemplo anterior em que o Access comparou o valor em cada coluna, tenha o usuário editado ou não, apenas atualizamos o registro usando o
RV
como critério de filtro. O raciocínio é que se o RV
ainda tem o mesmo valor que o Access passou, então o Access pode ter certeza de que esta linha não foi editada por mais ninguém porque se foi, então o RV
o valor de 's teria mudado. Isso também significa que, se um gatilho alterou os dados ou se o SQL Server e o Access não representaram um valor exatamente da mesma maneira (por exemplo, números flutuantes), o Access não hesitará quando selecionar novamente a linha atualizada e retornar com valores em outras colunas que os usuários não editaram.
OBSERVAÇÃO :Nem todos os produtos DBMS usarão os mesmos termos. Como exemplo, o
timestamp
do MySQL pode ser usado como uma versão de linha para fins de ODBC. Você precisará consultar a documentação do produto para ver se eles suportam o recurso rowversion para que você possa aproveitar esse comportamento com o Access. Visualizações e versão de linha
As visualizações também são afetadas pela presença ou ausência de uma versão de linha. Suponha que criamos uma view no SQL Server com a definição:
CREATE VIEW dbo.vwCities AS SELECT CityID, CityName FROM Application.Cities;A atualização de um registro na exibição reverteria para a comparação coluna por coluna, como se a coluna rowversion não existisse na tabela:
SQLExecDirect: UPDATE "dbo"."vwCities" SET "CityName"=? WHERE "CityID" = ? AND "CityName" = ?Portanto, se você precisar do comportamento de atualização com base na versão de linha, deverá tomar cuidado para garantir que as colunas de versão de linha sejam incluídas nas exibições. No caso de uma exibição que contém várias tabelas em junções, é melhor incluir pelo menos as colunas rowversion da(s) tabela(s) onde você pretende atualizar. Como normalmente apenas uma tabela pode ser atualizada, incluir apenas uma versão de linha pode ser suficiente como regra geral.
Excluindo um registro em um conjunto de registros
A exclusão de um registro se comporta de maneira semelhante às atualizações e também usará rowversion, se disponível. Em uma tabela sem uma versão de linha, obtemos:
SQLExecDirect: DELETE FROM "Application"."Cities" WHERE "CityID" = ? AND "CityName" = ? AND "StateProvinceID" = ? AND "Location" IS NULL AND "LatestRecordedPopulation" = ? AND "LastEditedBy" = ? AND "ValidFrom" = ? AND "ValidTo" = ?Em uma tabela com uma versão de linha, obtemos:
SQLExecDirect: DELETE FROM "Application"."Cities" WHERE "CityID" = ? AND "RV" = ?Novamente, o Access deve ser pessimista em relação à exclusão, pois trata-se de atualização; ele não deseja excluir uma linha que foi alterada por outra pessoa. Assim, ele usa o mesmo comportamento que vimos com a atualização para se proteger contra vários usuários alterando os mesmos registros.
Conclusões
Aprendemos como o Access lida com modificações de dados e mantém seu cache local sincronizado com a fonte de dados ODBC. Vimos como o Access era pessimista, impulsionado pela necessidade de oferecer suporte ao maior número possível de fontes de dados ODBC sem depender de suposições ou expectativas específicas de que essas fontes de dados ODBC darão suporte a um determinado recurso. Por esse motivo, vimos que o Access se comportará de maneira diferente dependendo de como a chave é definida para uma determinada tabela vinculada ODBC. Se pudéssemos inserir explicitamente uma nova chave, isso exigiria o mínimo de trabalho do Access para ressincronizar o cache local para o registro recém-inserido. No entanto, se permitirmos que o servidor preencha a chave, o Access terá que fazer trabalho adicional em segundo plano para ressincronizar.
Também vimos que ter uma coluna na tabela que pode ser usada como uma versão de linha pode ajudar a reduzir as conversas entre o Access e a fonte de dados ODBC em uma atualização. Você precisaria consultar a documentação do driver ODBC para determinar se ele oferece suporte à versão de linha na camada ODBC e, em caso afirmativo, incluir essa coluna nas tabelas ou exibições antes de vincular ao Access para obter os benefícios das atualizações baseadas em versão de linha.
Agora sabemos que para quaisquer atualizações ou exclusões, o Access sempre tentará verificar se a linha não foi alterada desde que foi buscada pela última vez pelo Access, para evitar que os usuários façam alterações inesperadas. No entanto, precisamos considerar os efeitos decorrentes de fazer alterações em outros locais (por exemplo, gatilho do lado do servidor, executando uma consulta diferente em outra conexão) que podem fazer com que o Access conclua que a linha foi alterada e, portanto, não permita a alteração. Essas informações nos ajudarão a analisar e evitar a criação de uma sequência de modificações de dados que podem contrariar as expectativas do Access ao ressincronizar o cache local.
No próximo artigo, veremos os efeitos da aplicação de filtros em um conjunto de registros.
Obtenha ajuda de nossos especialistas em acesso hoje. Ligue para nossa equipe em 773-809-5456 ou envie-nos um e-mail para [email protected].