sql >> Base de Datos >  >> RDS >> Access

Uso de parámetros grandes para el procedimiento almacenado de Microsoft SQL con DAO

Uso de parámetros grandes para procedimientos almacenados de Microsoft SQL con DAO

Como muchos de ustedes ya saben, el equipo de SQL Server ha anunciado la desaprobación de OLEDB para el motor de base de datos de SQL Server (Lea:no podemos usar ADO porque ADO usa OLEDB). Además, SQL Azure no admite oficialmente ADO, aunque todavía se puede salirse con la suya con SQL Server Native Client. Sin embargo, el nuevo controlador ODBC 13.1 viene con una serie de funciones que no estarán disponibles en SQL Server Native Client, y es posible que haya más por venir.

El resultado final:necesitamos trabajar con DAO puro. Ya hay varios elementos de voz de usuario que tocan el tema de Access / ODBC o Access / SQL Server... por ejemplo:

Conector de datos SQL Server
Mejor integración con SQL Server
Mejor integración con SQL Azure
Haga que Access pueda manejar más tipos de datos como los que se usan comúnmente en las bases de datos del servidor
Haga que Access sea un mejor Cliente ODBC

(Si no ha votado o visitado access.uservoice.com, vaya allí y vote si desea que el equipo de Access implemente su característica favorita)

Pero incluso si Microsoft mejora DAO en la próxima versión, todavía tenemos que lidiar con las aplicaciones existentes de nuestros clientes. Consideramos usar ODBC sobre el proveedor OLEDB (MSDASQL), pero sentimos que era similar a montar un pony sobre un caballo moribundo. Podría funcionar, pero podría morir un poco más abajo.

En su mayor parte, una consulta de paso hará lo que necesitamos hacer y es fácil crear una función para imitar la funcionalidad de ADO usando una consulta de paso de DAO. Pero hay una brecha importante que no se soluciona fácilmente:parámetros grandes para procedimientos almacenados. Como escribí anteriormente, a veces usamos el parámetro XML como una forma de pasar una gran cantidad de datos, que es mucho más rápido que hacer que Access inserte realmente todos los datos uno por uno. Sin embargo, una consulta DAO está limitada a unos 64 000 caracteres para el comando SQL y, en la práctica, puede ser incluso menos. Necesitábamos una forma de pasar parámetros que pudieran tener más de 64 000 caracteres, por lo que tuvimos que pensar en una solución alternativa.

Ingrese la tabla tblExecuteStoredProcedure

El enfoque que elegimos fue usar una tabla porque cuando usamos controladores ODBC más nuevos o SQL Server Native Client, DAO puede manejar fácilmente una gran cantidad de texto (también conocido como Memo) al insertarlo directamente en la tabla. Por lo tanto, para ejecutar un parámetro XML grande, escribiremos el procedimiento a ejecutar y su parámetro en la tabla, luego dejaremos que el gatillo lo recoja. Aquí está el script de creación de tablas:

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
);

Por supuesto, en realidad no tenemos la intención de usar esto como una tabla real. También establecemos arbitrariamente 10 parámetros a pesar de que un procedimiento almacenado puede tener muchos más. Sin embargo, según nuestra experiencia, es bastante raro tener mucho más de 10, especialmente cuando se trata de parámetros XML. Por sí sola, la mesa no sería muy útil. Necesitamos un disparador:

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;

–Procesar solo un registro que debe ser el último insertado
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 insertado COMO p
WHERE p.RV =(
SELECT MAX(x. RV)
DESDE insertado COMO x
);

SET @Params =STUFF((
SELECT
CONCAT(
N',',
p.name,
N' =',
p. name
)
DESDE sys.parameters AS p
INNER JOIN sys.types AS t
ON p.user_type_id =t.user_type_id
DONDE p.object_id =OBJECT_ID( @FullyQualifiedProcedureName)
FOR XML PATH(N”)
), 1, 1, N”);

SET @ParamList =STUFF((
SELECT
CONCAT(
N',',
p.nombre,
N' ',
t.nombre ,
CASE
CUANDO t.name LIKE N'%char%' O t.name LIKE '%binary%'
THEN CONCAT(N'(', IIF(p.max_length =- 1, N'MAX', CAST(p.max_length AS nvarchar(11))), N')')
CUANDO t.name ='decimal' O t.name ='numeric'
ENTONCES CONCAT(N'(', p.precisión, 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 '@Parameter1′),
(2, N'@Parameter2′),
(3, N'@Parameter3′),
(4, N'@Parameter4′),
(5, N'@Parámetro5′),
(6, N'@Parámetro6′),
(7, N'@Parámetro7′),
(8, N'@ Parámetro8′),
(9, N'@Parameter9′),
(10, N'@Parameter10′)
) AS p(ParameterID, ParameterName)
WHERE p. ID de parámetro> @ParamCount
FOR XML PATH(N”)
));

SET @Sql =CONCAT(N'EXEC ', @FullyQualifiedProcedureName, N' ', @Params, N';');

–Evitar que un desencadenador devuelva conjuntos de resultados (que está en desuso)
–Si un procedimiento almacenado devuelve alguno, el desencadenador terminará en un error
EJECUTAR sys.sp_executesql @Sql, @ParamList, @ Parámetro 1, @ Parámetro 2, @ Parámetro 3, @ Parámetro 4, @ Parámetro 5, @ Parámetro 6, @ Parámetro 7, @ Parámetro 8, @ Parámetro 9, @ Parámetro 10
CON RESULTADOS NINGUNO;

ELIMINAR DESDE dbo.tblExecuteStoredProcedure
DONDE EXISTE (
SELECCIONAR NULL
DESDE insertado
DONDE insertado.ExecuteID =tblExecuteStoredProcedure.ExecuteID
);
FIN;

Todo un bocado, ese disparador. Básicamente, se necesita una sola inserción, luego descubre cómo convertir los parámetros de su nvarchar (MAX) como se define en la tabla tblExecuteStoredProcedure al tipo real requerido por el procedimiento almacenado. Se utilizan conversiones implícitas y, dado que está envuelto en un sys.sp_executesql, funciona bien para una variedad de tipos de datos siempre que los valores de los parámetros sean válidos. Tenga en cuenta que requerimos que el procedimiento almacenado NO devuelva ningún conjunto de resultados. Microsoft permite que los disparadores devuelvan conjuntos de resultados pero, como se indicó, no es estándar y ha quedado obsoleto. Entonces, para evitar problemas con futuras versiones de SQL Server, bloqueamos esa posibilidad. Finalmente, limpiamos la mesa, para que siempre esté vacía. Después de todo, estamos abusando de la mesa; no estamos almacenando ningún dato.

Elegí usar un activador porque reduce la cantidad de viajes de ida y vuelta entre Access y SQL Server. Si hubiera usado un procedimiento almacenado para procesar el T-SQL desde el cuerpo del disparador, eso habría significado que tendría que llamarlo después de insertarlo en la tabla y también lidiar con los posibles efectos secundarios, como dos usuarios insertando al mismo tiempo o un error dejando un registro atrás y así sucesivamente.

Bien, pero ¿cómo usamos la "tabla" y su disparador? Ahí es donde necesitamos un poco de código VBA para configurar todo el arreglo...

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

Establecer db =CurrentDb
Establecer rs =db.OpenRecordset(“SELECT * FROM tblExecuteStoredProcedure;”, dbOpenDynaset, dbAppendOnly o dbSeeChanges)

rs.AddNew
rs.Fields(“ProcedureSchema”).Value =ProcedureSchema
rs.Fields(“ProcedureName”).Value =ProcedimientoNombre

l =LBound(Parámetros)
u =UBound(Parámetros)
For i =l To u
rs.Fields(“Parámetro” &i).Valor =Parámetros(i)
Siguiente

rs.Actualizar
Finalizar sub

Tenga en cuenta que usamos ParamArray, que nos permite especificar tantos parámetros como realmente necesitamos para un procedimiento almacenado. Si quisiera volverse loco y tener 20 parámetros más, simplemente podría agregar más campos a la tabla y actualizar el disparador y el código VBA aún funcionaría. Podrías hacer algo como esto:

ExecuteWithLargeParameters "dbo", "uspMyStoredProcedure", dteStartDate, dteEndDate, strSomeBigXMLDocument

Con suerte, la solución alternativa no será necesaria durante mucho tiempo (especialmente si va a Access UserVoice y vota a favor de varios elementos relacionados con Access + SQL / ODBC), pero esperamos que lo encuentre útil si se encuentra en la situación en la que estamos. in. ¡También nos encantaría conocer las mejoras que podría tener para esta solución o un mejor enfoque!