Brug af store parametre til Microsoft SQL-lagret procedure med DAO
Som mange af jer allerede ved, har SQL Server-teamet annonceret udfasning af OLEDB til SQL Server-databasemotor (Læs:vi kan ikke bruge ADO, fordi ADO bruger OLEDB). Derudover understøtter SQL Azure ikke officielt ADO, selvom man stadig kan slippe af sted med det ved at bruge SQL Server Native Client. Den nye 13.1 ODBC-driver kommer dog med en række funktioner, som ikke vil være tilgængelige i SQL Server Native Client, og der kan komme flere.
Den nederste linje:vi skal arbejde med ren DAO. Der er allerede flere brugerstemmeelementer, der berører emnet Access / ODBC eller Access / SQL Server... for eksempel:
Datakonnektor SQL Server
Bedre integration med SQL Server
Bedre integration med SQL Azure
Gør venligst Access i stand til at håndtere flere datatyper, som det er almindeligt anvendt i serverdatabaser
Gør Access til en bedre ODBC-klient
(Hvis du ikke har stemt eller besøgt access.uservoice.com, så gå derhen og stem, hvis du vil have Access-teamet til at implementere din yndlingsfunktion)
Men selvom Microsoft forbedrer DAO i den næste version, er vi stadig nødt til at håndtere vores kundes eksisterende applikationer. Vi overvejede at bruge ODBC over OLEDB-udbyderen (MSDASQL), men vi følte, at det var beslægtet med at skræve en pony på en døende hest. Det virker måske, men det dør måske bare et kort stykke tid ned.
For det meste vil en passthrough-forespørgsel gøre det, vi skal gøre, og det er nemt at sammensætte en funktion for at efterligne ADO's funktionalitet ved hjælp af en DAO pass-through-forespørgsel. Men der er et væsentligt hul, som ikke let kan afhjælpes - store parametre for lagrede procedurer. Som jeg skrev tidligere, bruger vi nogle gange XML-parameteren som en måde at videregive store mængder data på, hvilket er meget hurtigere end at få Access til at indsætte alle dataene én efter én. En DAO-forespørgsel er dog begrænset til omkring 64K tegn for SQL-kommandoen og kan i praksis være endnu mindre. Vi havde brug for en måde at videregive parametre, der kunne være større end 64K tegn, så vi var nødt til at tænke på en løsning.
Indtast tabellen tblExecuteStoredProcedure
Den tilgang, vi valgte, var at bruge en tabel, fordi når vi bruger nyere ODBC-drivere eller SQL Server Native Client, er DAO nemt i stand til at håndtere store mængder tekst (aka Memo) ved at indsætte direkte i tabellen. Derfor, for at udføre en stor XML-parameter, vil vi skrive proceduren, der skal udføres, og dens parameter til tabellen, og derefter lade triggeren samle den op. Her er scriptet til oprettelse af tabel:
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
);
Selvfølgelig har vi ikke til hensigt at bruge dette som et rigtigt bord. Vi indstiller også vilkårligt 10 parametre, selvom en lagret procedure kan have mange flere. Men efter vores erfaring er det ret sjældent at have meget mere end 10, især når vi har at gøre med XML-parametre. I sig selv ville bordet ikke være særlig nyttigt. Vi har brug for en 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;
–Behandle kun en enkelt post, som skal være den sidst indsatte
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);
VÆLG
@ProcedureSchema =p.ProcedureSchema,
@ProcedureName =p.ProcedureName,
@FullyQualifiedProcedureName =CONCAT(QUOTENAME(p.ProcedureSchema), N'.', QUOTENAME(p.ProcedureName) ),
@Parameter1 =p.Parameter1,
@Parameter2 =p.Parameter2
FRA indsat SOM p
HVOR p.RV =(
SELECT MAX(x. RV)
FRA indsat AS x
);
SET @Params =TING((
SELECT
CONCAT(
N',',
p.navn,
N' =',
s. navn
)
FRA 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 =TING((
VÆLG
KONCAT(
N',',
p.navn,
N' ',
t.navn ,
CASE
NÅR t.name LIKE N'%char%' ELLER t.name LIKE '%binary%'
SÅ CONCAT(N'(', IIF(p.max_length =- 1, N'MAX', CAST(p.max_length AS nvarchar(11))), N')')
NÅR t.name ='decimal' ELLER t.name ='numerisk'
SÅ CONCAT(N'(', p.præcision, N',', p.scale, N')')
ELSE N”
END
)
FRA 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 =(
VÆLG COUNT(*)
FRA 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'@ 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';');
–Forhindrer eventuelle resultatsæt i at blive returneret fra en trigger (som er forældet)
–Hvis en lagret procedure returnerer nogen, vil triggeren ende med en fejl
UDFØR sys.sp_executesql @Sql, @ParamList, @ Parameter1, @Parameter2, @Parameter3, @Parameter4, @Parameter5, @Parameter6, @Parameter7, @Parameter8, @Parameter9, @Parameter10
MED INGEN RESULTATSÆTTER;
SLET FRA dbo.tblExecuteStoredProcedure
WHERE EXISTS (
SELECT NULL
FROM inserted
WHERE inserted.ExecuteID =tblExecuteStoredProcedure.ExecuteID
END; ); P>
En ganske mundfuld, den udløser. Grundlæggende tager det en enkelt indsættelse, og finder derefter ud af, hvordan man konverterer parametrene fra deres nvarchar(MAX) som defineret i tabellen tblExecuteStoredProcedure til den faktiske type, der kræves af den lagrede procedure. Implicitte konverteringer bruges, og da det er pakket ind i en sys.sp_executesql, fungerer det godt for en række forskellige datatyper, så længe selve parameterværdierne er gyldige. Bemærk, at vi kræver, at den lagrede procedure IKKE returnerer nogen resultatsæt. Microsoft tillader triggere at returnere resultatsæt, men som nævnt er det ikke-standard og er blevet forældet. Så for at undgå problemer med fremtidige versioner af SQL Server, blokerer vi denne mulighed. Til sidst rydder vi bordet, så det altid er tomt. Vi misbruger jo bordet; vi gemmer ingen data.
Jeg valgte at bruge en trigger, fordi den reducerer antallet af rundrejser mellem Access og SQL Server. Havde jeg brugt en lagret procedure til at behandle T-SQL'en fra udløserens krop, ville det have betydet, at jeg skulle kalde den, efter jeg har indsat den i tabellen og også håndtere potentielle bivirkninger såsom to brugere, der indsætter på samme tid eller en fejl ved at efterlade en post og så videre.
OK, men hvordan bruger vi "tabellen" og dens udløser? Det er her, vi har brug for lidt VBA-kode for at opsætte hele arrangementet...
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
Indstil db =CurrentDb
Indstil rs =db.OpenRecordset(“SELECT * FROM tblExecuteStoredProcedure;”, dbOpenDynaset, dbAppendOnly Eller dbSeeChanges)
rs.AddNew
rs.Fields(“ProcedureSchema”).Value =ProcedureSchema
rs.Fields(“ProcedureName”).Value =ProcedureName
l =LBound(Parameters)
u =UBound(Parameters)
For i =l Til u
rs.Fields(“Parameter” &i).Value =Parameters(i)
Næste
rs.Opdater
Afslut under
Bemærk, at vi bruger ParamArray, som giver os mulighed for at specificere så mange parametre, som vi faktisk har brug for til en lagret procedure. Hvis du ville gå amok og have 20 parametre mere, kunne du bare tilføje flere felter til tabellen og opdatere triggeren, og VBA-koden ville stadig fungere. Du ville være i stand til at gøre noget som dette:
ExecuteWithLargeParameters "dbo", "uspMyStoredProcedure", dteStartDate, dteEndDate, strSomeBigXMLDocument
Forhåbentlig vil løsningen ikke være nødvendig i lang tid (især hvis du går til Access UserVoice og opstemmer forskellige elementer relateret til Access + SQL / ODBC), men vi håber, du finder det nyttigt, hvis du skulle befinde dig i den situation, vi er i. Vi vil også gerne høre om forbedringer, du måtte have til denne løsning eller en bedre tilgang!