Usando parâmetros grandes para procedimento armazenado do Microsoft SQL com DAO
Como muitos de vocês já sabem, a equipe do SQL Server anunciou a descontinuação do OLEDB para o mecanismo de banco de dados SQL Server (Leia:não podemos usar o ADO porque o ADO usa o OLEDB). Além disso, o SQL Azure não oferece suporte oficial ao ADO, embora ainda seja possível usar o SQL Server Native Client. No entanto, o novo driver ODBC 13.1 vem com vários recursos que não estarão disponíveis no SQL Server Native Client, e pode haver mais por vir.
Conclusão:precisamos trabalhar com DAO puro. Já existem vários itens de voz do usuário tocando no assunto de Access / ODBC ou Access / SQL Server… por exemplo:
Conector de dados SQL Server
Melhor integração com o SQL Server
Melhor integração com o SQL Azure
Por favor, torne o Access capaz de lidar com mais tipos de dados, como são comumente usados em bancos de dados do servidor
Torne o acesso melhor cliente ODBC
(Se você não votou ou visitou access.uservoice.com, vá lá e vote se deseja que a equipe do Access implemente seu recurso favorito)
Mas mesmo que a Microsoft aprimore o DAO na próxima versão, ainda teremos que lidar com os aplicativos existentes de nossos clientes. Consideramos usar o provedor ODBC em vez de OLEDB (MSDASQL), mas sentimos que era semelhante a montar um pônei em um cavalo moribundo. Pode funcionar, mas pode morrer um pouco abaixo.
Na maioria das vezes, uma consulta de passagem fará o que precisamos fazer e é fácil reunir uma função para imitar a funcionalidade do ADO usando uma consulta de passagem DAO. Mas há uma lacuna significativa que não é facilmente corrigida – grandes parâmetros para procedimentos armazenados. Como escrevi anteriormente, às vezes usamos o parâmetro XML como uma maneira de passar uma grande quantidade de dados, o que é muito mais rápido do que o Access inserir todos os dados um por um. No entanto, uma consulta DAO é limitada a cerca de 64 K caracteres para o comando SQL e, na prática, pode ser ainda menor. Precisávamos de uma maneira de passar parâmetros que pudessem ter mais de 64 mil caracteres, então tivemos que pensar em uma solução alternativa.
Insira a tabela tblExecuteStoredProcedure
A abordagem que escolhemos foi usar uma tabela porque quando usamos drivers ODBC mais recentes ou SQL Server Native Client, o DAO é capaz de lidar facilmente com uma grande quantidade de texto (também conhecido como Memo) inserindo diretamente na tabela. Portanto, para executar um parâmetro XML grande, escreveremos o procedimento a ser executado e seu parâmetro na tabela e deixaremos o gatilho pegá-lo. Aqui está o script de criação da tabela:
CREATE TABLE dbo.tblExecuteStoredProcedure (
ExecuteID int NOT NULL IDENTITY
CONSTRAINT PK_tblExecuteStoredProcedure PRIMARY KEY CLUSTERED,
ProcedureSchema sysname NOT NULL
CONSTRAINT DF_tblExecuteStoredProcedure DEFAULT 'dbo',
ProcedureName sysname NOT NULL,
Parameter1 nvarchar(MAX) NULL,
Parameter2 nvarchar(MAX) NULL,
Parameter3 nvarchar(MAX) NULL,
Parameter4 nvarchar(MAX) NULL,
Parameter5 nvarchar(MAX) NULL,
Parameter6 nvarchar(MAX) NULL,
Parameter7 nvarchar(MAX) NULL,
Parameter8 nvarchar(MAX) NULL,
Parameter9 nvarchar(MAX) NULL,
Parameter10 nvarchar(MAX) NULL,
RV rowversion NOT NULL
);
Claro, nós não pretendemos usar isso como uma mesa real. Também definimos arbitrariamente 10 parâmetros, embora um procedimento armazenado possa ter muitos mais. No entanto, em nossa experiência, é muito raro ter muito mais do que 10, especialmente quando estamos lidando com parâmetros XML. Por si só, a tabela não seria muito útil. Precisamos de um gatilho:
CREATE TRIGGER dbo.tblExecuteStoredProcedureAfterInsert
ON dbo.tblExecuteStoredProcedure AFTER INSERT AS
BEGIN
--Throw if multiple inserts were performed
IF 1 < (
SELECT COUNT(*)
FROM inserted
)
BEGIN
ROLLBACK TRANSACTION;
THROW 50000, N'Cannot perform multiple-row inserts on the table `tblExecuteStoredProcedure`.', 1;
RETURN;
END;
–Processar apenas um único registro que deve ser o último inserido
DECLARE @ProcedureSchema sysname,
@ProcedureName sysname,
@FullyQualifiedProcedureName nvarchar(MAX),
@Parameter1 nvarchar(MAX),
@Parameter2 nvarchar(MAX),
@Parameter3 nvarchar(MAX),
@Parameter4 nvarchar(MAX),
@Parameter5 nvarchar(MAX),
@Parameter6 nvarchar(MAX),
@Parameter7 nvarchar(MAX),
@Parameter8 nvarchar(MAX),
@Parameter9 nvarchar(MAX),
@Parameter10 nvarchar(MAX),
@Params nvarchar(MAX),
@ParamCount int,
@ParamList nvarchar(MAX),
@Sql nvarchar(MAX);
SELECT
@ProcedureSchema =p.ProcedureSchema,
@ProcedureName =p.ProcedureName,
@FullyQualifiedProcedureName =CONCAT(QUOTENAME(p.ProcedureSchema), N'.', QUOTENAME(p.ProcedureName) ),
@Parameter1 =p.Parameter1,
@Parameter2 =p.Parameter2
FROM inserido AS p
WHERE p.RV =(
SELECT MAX(x. RV)
DE inserido AS x
);
SET @Params =STUFF((
SELECT
CONCAT(
N',',
p.name,
N' =',
p. name
)
FROM sys.parameters AS p
INNER JOIN sys.types AS t
ON p.user_type_id =t.user_type_id
WHERE p.object_id =OBJECT_ID( @FullyQualifiedProcedureName)
FOR XML PATH(N”)
), 1, 1, N”);
SET @ParamList =STUFF((
SELECT
CONCAT(
N',',
p.name,
N' ',
t.name ,
CASE
QUANDO t.name LIKE N'%char%' OU t.name LIKE '%binary%'
THEN CONCAT(N'(', IIF(p.max_length =- 1, N'MAX', CAST(p.max_length AS nvarchar(11))), N')')
QUANDO t.name ='decimal' OR t.name ='numeric'
THEN CONCAT(N'(', p.precisão, N',', p.escala, N')')
ELSE N”
END
)
FROM sys.parameters AS p
INNER JOIN sys.types AS t
ON p.user_type_id =t.user_type_id
WHERE p.object_id =OBJECT_ID(@FullyQualifiedProcedureName)
FOR XML PATH(N”)
), 1, 1, N”);
SET @ParamCount =(
SELECT COUNT(*)
FROM sys.parameters AS p
WHERE p.object_id =OBJECT_ID(@FullyQualifiedProcedureName)
);
SET @ParamList +=((
SELECT
CONCAT(N',', p.ParameterName, N' nvarchar(1)')
FROM (VALUES
(1, N '@Parâmetro1′),
(2, N'@Parâmetro2′),
(3, N'@Parâmetro3′),
(4, N'@Parâmetro4′),
(5, N'@Parâmetro5′),
(6, N'@Parâmetro6′),
(7, N'@Parâmetro7′),
(8, N'@ Parameter8′),
(9, N'@Parameter9′),
(10, N'@Parameter10′)
) AS p(ParameterID, ParameterName)
WHERE p. ParameterID> @ParamCount
FOR XML PATH(N”)
));
SET @Sql =CONCAT(N'EXEC', @FullyQualifiedProcedureName, N'', @Params, N';');
–Impedir que quaisquer conjuntos de resultados sejam retornados de um gatilho (que está obsoleto)
–Se um procedimento armazenado retornar algum, o gatilho terminará em um erro
EXECUTE sys.sp_executesql @Sql, @ParamList, @ Parâmetro1, @Parâmetro2, @Parâmetro3, @Parâmetro4, @Parâmetro5, @Parâmetro6, @Parâmetro7, @Parâmetro8, @Parâmetro9, @Parâmetro10
COM CONJUNTOS DE RESULTADOS NENHUMA;
DELETE FROM dbo.tblExecuteStoredProcedure
ONDE EXISTE (
SELECT NULL
FROM inserido
ONDE inserido.ExecuteID =tblExecuteStoredProcedure.ExecuteID
);
END;
Um bocado bastante, esse gatilho. Basicamente, é preciso uma única inserção e, em seguida, descobre como converter os parâmetros de seu nvarchar(MAX) conforme definido na tabela tblExecuteStoredProcedure para o tipo real exigido pelo procedimento armazenado. Conversões implícitas são usadas e, como estão envolvidas em um sys.sp_executesql, funcionam bem para vários tipos de dados, desde que os próprios valores de parâmetro sejam válidos. Observe que exigimos que o procedimento armazenado NÃO retorne nenhum conjunto de resultados. A Microsoft permite que os gatilhos retornem conjuntos de resultados, mas, conforme observado, não é padrão e foi preterido. Portanto, para evitar problemas com versões futuras do SQL Server, bloqueamos essa possibilidade. Por fim, limpamos a mesa, para que esteja sempre vazia. Afinal, estamos abusando da mesa; não estamos armazenando nenhum dado.
Optei por usar um gatilho porque reduz o número de viagens de ida e volta entre o Access e o SQL Server. Se eu tivesse usado um procedimento armazenado para processar o T-SQL do corpo do gatilho, isso significaria que eu precisaria chamá-lo depois de inserir na tabela e também lidar com possíveis efeitos colaterais, como dois usuários inserindo ao mesmo tempo ou um erro deixando um registro para trás e assim por diante.
OK, mas como usamos a “tabela” e seu gatilho? É aí que precisamos de um pouco de código VBA para configurar todo o arranjo…
Public Sub ExecuteWithLargeParameters( _
ProcedureSchema As String, _
ProcedureName As String, _
ParamArray Parameters() _
)
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim i As Long
Dim l As Long
Dim u As Long
Set db =CurrentDb
Set rs =db.OpenRecordset(“SELECT * FROM tblExecuteStoredProcedure;”, dbOpenDynaset, dbAppendOnly Ou dbSeeChanges)
rs.AddNew
rs.Fields(“ProcedureSchema”).Value =ProcedureSchema
rs.Fields(“ProcedureName”).Value =ProcedureName
l =LBound(Parâmetros)
u =UBound(Parâmetros)
Para i =l Para u
rs.Fields(“Parâmetro” &i).Value =Parâmetros(i)
Próximo
rs.Atualizar
End Sub
Observe que usamos ParamArray, que nos permite especificar quantos parâmetros realmente precisamos para um procedimento armazenado. Se você quisesse enlouquecer e ter mais 20 parâmetros, bastaria adicionar mais campos à tabela e atualizar o gatilho e o código VBA ainda funcionaria. Você seria capaz de fazer algo assim:
ExecuteWithLargeParameters "dbo", "uspMyStoredProcedure", dteStartDate, dteEndDate, strSomeBigXMLDocument
Espero que a solução não seja necessária por um longo tempo (especialmente se você acessar o Access UserVoice e votar em vários itens relacionados ao Access + SQL / ODBC), mas esperamos que você ache útil caso se encontre na situação em que estamos também gostaríamos de saber sobre as melhorias que você pode ter para esta solução ou uma abordagem melhor!