Utilizzo di parametri di grandi dimensioni per la stored procedure Microsoft SQL con DAO
Come molti di voi già sanno, il team di SQL Server ha annunciato la deprecazione di OLEDB per il motore di database di SQL Server (leggi:non possiamo usare ADO perché ADO usa OLEDB). Inoltre, SQL Azure non supporta ufficialmente ADO, anche se è ancora possibile farla franca usando SQL Server Native Client. Tuttavia, il nuovo driver ODBC 13.1 include una serie di funzionalità che non saranno disponibili in SQL Server Native Client e potrebbero essercene altre in arrivo.
La conclusione:dobbiamo lavorare con DAO puro. Esistono già più voci dell'utente che toccano l'argomento Accesso / ODBC o Accesso / SQL Server... ad esempio:
Connettore dati SQL Server
Migliore integrazione con SQL Server
Migliore integrazione con SQL Azure
Rendi Access in grado di gestire più tipi di dati comunemente usati nei database dei server
Migliora Access Cliente ODBC
(Se non hai votato o visitato access.uservoice.com, vai lì e vota se vuoi che il team di Access implementi la tua funzionalità preferita)
Ma anche se Microsoft migliora DAO nella prossima versione, dobbiamo comunque fare i conti con le applicazioni esistenti dei nostri clienti. Abbiamo considerato l'utilizzo di ODBC su provider OLEDB (MSDASQL), ma abbiamo ritenuto che fosse come cavalcare un pony su un cavallo morente. Potrebbe funzionare, ma potrebbe morire per un breve periodo.
Per la maggior parte, una query pass-through farà ciò che dobbiamo fare ed è facile mettere insieme una funzione per imitare la funzionalità di ADO utilizzando una query pass-through DAO. Ma c'è una lacuna significativa a cui non è facile rimediare:parametri di grandi dimensioni per le procedure memorizzate. Come ho scritto in precedenza, a volte utilizziamo il parametro XML come un modo per passare grandi quantità di dati, il che è molto più veloce rispetto al fatto che Access inserisca effettivamente tutti i dati uno per uno. Tuttavia, una query DAO è limitata a circa 64K caratteri per il comando SQL e in pratica può essere anche inferiore. Avevamo bisogno di un modo per passare parametri che potessero essere più grandi di 64.000 caratteri, quindi abbiamo dovuto pensare a una soluzione alternativa.
Inserisci la tabella tblExecuteStoredProcedure
L'approccio che abbiamo scelto è stato quello di utilizzare una tabella perché quando utilizziamo driver ODBC più recenti o SQL Server Native Client, DAO è facilmente in grado di gestire grandi quantità di testo (ovvero Memo) inserendolo direttamente nella tabella. Pertanto, per eseguire un parametro XML di grandi dimensioni, scriveremo la procedura da eseguire e il relativo parametro nella tabella, quindi lasceremo che il trigger lo raccolga. Ecco lo script di creazione della tabella:
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
);
Naturalmente, in realtà non intendiamo usarlo come un vero tavolo. Inoltre, impostiamo arbitrariamente 10 parametri anche se una procedura memorizzata può averne molti di più. Tuttavia, nella nostra esperienza, è abbastanza raro averne molto più di 10, specialmente quando abbiamo a che fare con parametri XML. Di per sé, il tavolo non sarebbe molto utile. Abbiamo bisogno di un trigger:
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;
–Elabora solo record singolo che dovrebbe essere l'ultimo inserito
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 inserito AS p
WHERE p.RV =(
SELECT MAX(x. RV)
DA inserito 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%' OR 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 ='decimale' OR t.name ='numerico'
THEN CONCAT(N'(', p.precision, N',', p.scale, 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 '@Parameter1′),
(2, N'@Parameter2′),
(3, N'@Parameter3′),
(4, N'@Parameter4′),
/> (5, N'@Parameter5′),
(6, N'@Parameter6′),
(7, N'@Parameter7′),
(8, N'@ Parametro8′),
(9, N'@Parametro9′),
(10, N'@Parametro10′)
) AS p(ParameterID, ParameterName)
WHERE p. ParameterID> @ParamCount
FOR XML PATH(N”)
));
SET @Sql =CONCAT(N'EXEC ', @FullyQualifiedProcedureName, N' ', @Params, N';');
–Impedisci la restituzione di set di risultati da un trigger (che è deprecato)
–Se una stored procedure ne restituisce uno, il trigger terminerà con un errore
ESEGUI sys.sp_executesql @Sql, @ParamList, @ Parametro1, @Parametro2, @Parametro3, @Parametro4, @Parametro5, @Parametro6, @Parametro7, @Parametro8, @Parametro9, @Parametro10
CON RISULTATI NESSUNO;
ELIMINA DA dbo.tblExecuteStoredProcedure
DOVE ESISTE (
SELECT NULL
DA inserito
DOVE inserito.ExecuteID =tblExecuteStoredProcedure.ExecuteID
);
END;
Un bel boccone, quel grilletto. Fondamentalmente richiede un singolo inserimento, quindi scopre come convertire i parametri dal loro nvarchar(MAX) come definito nella tabella tblExecuteStoredProcedure nel tipo effettivo richiesto dalla stored procedure. Vengono utilizzate conversioni implicite e poiché è racchiuso in un sys.sp_executesql funziona bene per una varietà di tipi di dati purché i valori dei parametri stessi siano validi. Si noti che è necessario che la procedura memorizzata NON restituisca alcun set di risultati. Microsoft consente ai trigger di restituire set di risultati ma, come notato, non è standard ed è stato deprecato. Quindi, per evitare problemi con le versioni future di SQL Server, blocchiamo questa possibilità. Infine, svuotiamo il tavolo, quindi è sempre vuoto. Dopotutto, stiamo abusando del tavolo; non memorizziamo alcun dato.
Ho scelto di utilizzare un trigger perché riduce il numero di round trip tra Access e SQL Server. Se avessi usato una procedura memorizzata per elaborare il T-SQL dal corpo del trigger, ciò avrebbe significato che avrei dovuto chiamarlo dopo averlo inserito nella tabella e gestire anche potenziali effetti collaterali come l'inserimento di due utenti contemporaneamente o un errore che lascia un record dietro e così via.
OK, ma come utilizziamo la "tabella" e il suo trigger? È qui che abbiamo bisogno di un po' di codice VBA per impostare l'intero arrangiamento...
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 i As Long
Dim u As Long
Imposta db =CurrentDb
Imposta rs =db.OpenRecordset("SELECT * FROM tblExecuteStoredProcedure;", dbOpenDynaset, dbAppendOnly o dbSeeChanges)
rs.AddNew
rs.Fields(“ProcedureSchema”).Value =ProcedureSchema
rs.Fields(“ProcedureName”).Value =ProcedureName
l =LBound(Parameters)
u =UBound(Parameters)
For i =l To u
rs.Fields("Parameter" &i).Value =Parameters(i)
Avanti
rs.Update
End Sub
Si noti che utilizziamo ParamArray che ci consente di specificare tutti i parametri di cui abbiamo effettivamente bisogno per una procedura memorizzata. Se vuoi impazzire e avere 20 parametri in più, puoi semplicemente aggiungere più campi alla tabella e aggiornare il trigger e il codice VBA funzionerebbe comunque. Potresti fare qualcosa del genere:
ExecuteWithLargeParameters "dbo", "uspMyStoredProcedure", dteStartDate, dteEndDate, strSomeBigXMLDocument
Si spera che la soluzione alternativa non sia necessaria per molto tempo (soprattutto se vai su Access UserVoice e voti vari elementi relativi ad Access + SQL / ODBC), ma speriamo che lo trovi utile se ti trovi nella situazione in cui siamo in. Ci piacerebbe anche conoscere i miglioramenti che potresti avere per questa soluzione o un approccio migliore!