sábado, enero 05, 2008

UpdateBatchSize

MarshalingTodo programador que haya trabajado con SQL Server sabe lo importante que es empaquetar cuantas instrucciones sea posible dentro de una misma petición al servidor. El mismo programador, cuando comienza a trabajar con ADO.NET tropieza con la paradoja de que cada actualización de registros modificados en un dataset, debe viajar al servidor SQL completamente sola. Al menos, en la versión 1.1 era esto lo que ocurría...

Tinieblas

El problema, aclaro, no es grave. Típicamente, cuando se producen actualizaciones a través de modificaciones en un DataSet, estas modificaciones afectan a muy pocos registros, y el peaje a pagar es poco. Por el contrario, si usted se despierta un día actualizando centenares de registros a través de un data adapter, corra a visitar a su psicólogo de cabecera. Para actualizaciones masivas, es mejor usar directamente SQL, y si es posible, mediante procedimientos almacenados. Ese mítico escenario del feliz programador OOP que transparentemente modifica instancias de clases generadas por un ORM es una gran mentira: en una aplicación real, la mayoría de las modificaciones que no se realizan en el servidor mediante procedimientos almacenados, se generan a petición de controles enlazados a datos... a no ser que usted sienta fobia hacia el enlace de datos y los procedimientos almacenados.
De todos modos, las clases SqlDataAdapter y OracleDataAdapter de .NET 2.0 introdujeron una propiedad UpdateBatchSize, que permite agrupar varias actualizaciones en una misma petición. Si la propiedad vale 1, que es su valor por omisión, el comportamiento de los adaptadores es el de la versión anterior: una petición separada para cada registro actualizado. Un 0 en la propiedad provoca que todas las actualizaciones se sumen y se envíen en un único viaje de ida y vuelta. Y, finalmente, cualquier valor mayor que 1 sirve para crear grupos de peticiones del tamaño indicado.
Confieso, sin embargo, que hasta hace muy poco, nunca había usado esta propiedad. El motivo tiene que ver con la mecánica de las actualizaciones en ADO.NET. Suponga que tratamos con un una tabla cuya clave primaria es una columna autoincremental, y que tiene un campo de tipo rowversion/timestamp para el control optimista de concurrencia. Las inserciones en esta tabla se ejecutan mediante el siguiente grupo de instrucciones SQL:
insert into Clientes(...) values (...);
select IDCliente, TS from Clientes
where IDCliente = SCOPE_IDENTITY()
En otras palabras: cuando se inserta un registro, inmediatamente después se pide la última identidad creada al servidor, y el valor de la versión de fila para la nueva fila. Estos dos valores se devuelven al adaptador como un recordset de una fila, y el adaptador utiliza estos valores para poner al día las correspondientes columnas en el dataset.
¿Qué ocurriría si agrupásemos varias de estas instrucciones en un mismo batch o grupo de consultas? Evidentemente, el mecanismo saltaría por los aires. El servidor respondería enviando una colección de recordsets, pero el adaptador sería incapaz de distinguir cuál correspondería a cada fila. Es natural que el programador se abstenga de usar UpdateBatchSize, pues aparentemente obligaría a utilizar otro sistema para la puesta al día de la copia local de los datos tras una actualización.

Fiat lux

Pero, ¡pobre de mí!, no sospechaba cuán listos habían sido los que añadieron esta característica a los adaptadores. Hace poco, haciendo pruebas, me di cuenta de que un adaptador con UpdateBatchSize parecía seguir enviando consultas individuales al servidor SQL. Al menos, en el SQL Profiler se mostraban las inserciones como instrucciones individuales. Intrigado, busqué más información en Internet, y así supe que la labor de UpdateBatchSize no consiste en generar una única petición formada por una gigantesca instrucción. La técnica es más sofisticada, y según lo que sé, consiste en agrupar todas las peticiones en un único paquete RPC (Remote Procedure Call). Esto ahorra un ancho de banda considerable, y lo que es más importante, disminuye los problemas de latencia provocados por la red. No se optimiza la operación, considerada por separado, al máximo, pero se logra un mejor resultado, de todos modos... y abre el camino a la esperanza.
Volví entonces a la documentación de SqlDataAdapter y descubrí un pequeño detalle: cuando BatchUpdateSize es distinto de 1, los comandos del adaptador no pueden tener los valores Both o FirstReturnedRecord en su propiedad UpdateSource. Es decir: no se puede usar un recordset para poner al día la copia local. Pero esto también significa que se admite la opción OutputParameters. Si la actualización se realiza a través de procedimientos almacenados, ¡es posible usar UpdateBatchSize y seguir actualizando la copia local en una misma operación!
En estos casos, hay que tener un procedimiento almacenado en el servidor parecido a éste:
create procedure InsertCliente
@IdCliente int output,
@TS timestamp output,
@Nombre T_NOMBRE,
@Apellidos T_APELLIDOS as
insert into Clientes(Nombre, Apellidos)
values (@Nombre, @Apellidos);
select @IdCliente = IDCliente, @TS = TS
from Clientes
where IDCliente = SCOPE_IDENTITY()
Observe que esta vez no se utiliza un recordset, sino los parámetros de salida del procedimiento, para devolver los valores actualizados de las columnas modificadas en el servidor.
Evidentemente, el uso de procedimientos almacenados para estas operaciones requiere un poco más de planificación por parte del programador... pero es una tarea mecanizable, y seguramente podremos contar con la ayuda del DBA. Tenga en cuenta que el DBA no protestará si le pide que no permita actualizaciones en una tabla excepto a través de un procedimiento almacenado: estas cosas hacen feliz a un administrador de bases de datos.
Es poco probable que una aplicación en dos capas, para la que se presume poca carga, se beneficie de un mecanismo tan sofisticado de actualización. Pero en una aplicación multicapas con mucha carga, cada milisegundo (¡sí, hablo de milisegundos!) que se gane en una operación, significa más capacidad de concurrencia para la capa intermedia: el modelo de concurrencia de una capa intermedia con pooling no ofrece capacidad lineal respecto a los tiempos de respuesta. El truco de combinar UpdateBatchSize con procedimientos almacenados puede parecer excesivamente complicado a algunos, pero es bueno saber que disponemos de un arma como ésta en nuestro arsenal.

Actualización: He subido una pequeña actualización de Intuitive C#, con una nueva sección sobre métodos de extensión. Lo único interesante es que he añadido la sección al capítulo de interfaces.

Etiquetas: , ,

4 Comments:

Blogger jose luis said...

Ya que estamos en aplicaciones en n-capas, con acceso a datos a traves de procedimientos almacenados, etc, etc.
Que te parece CSLA. ?

lunes, enero 07, 2008 10:48:00 a. m.  
Blogger Ian Marteens said...

¿Otro ORM más? Por amor de Mefistófeles, no entiendo esa manía de tropezar quinientas veces con la misma piedra.

lunes, enero 07, 2008 6:22:00 p. m.  
Blogger PhiCode said...

Ian no hagas hígado, CSLA no es un ORM, es un framework para desarrollar aplicaciones distribuidas, se centra más en la capa de negocio de la aplicación que en la capa de datos, en la capa de datos podemos utilizar los tradicionales objetos de ADO.Net o alguno de los ORMs que más te agraden :)

http://www.lhotka.net/cslanet/Info.aspx

martes, enero 08, 2008 5:16:00 p. m.  
Blogger Ian Marteens said...

Entonces la cosa cambia...

Es que la página estaba caída y sólo pude leer un par de entrevistas con este señor que rozaban el tema de lejos.

Le echaré un vistazo. ADO.NET, efectivamente, necesitaría estandarizar algunas tareas que en Delphi/DataSnap venían implementadas de serie. Y el otro hueco donde un producto de ese tipo podría tener sentido, tiene que ver con las implementaciones de "remoting" o lo que se use en la capa intermedia: hay mucha confusión en las técnicas de implementación y en la documentación del comportamiento de esos sistemas. Si lo resuelve, bienvenido sea...

martes, enero 08, 2008 8:08:00 p. m.  

Publicar un comentario

<< Home