Siempre que planteamos una arquitectura, se nos plantea la misma duda ¿cómo movemos los datos entre capas de la aplicación?. Pues bien, estoy liado con la arquitectura de dos aplicaciones en cierto modo similares y una vez más me enfrento a esta cuestión. Solo que esta vez, además, esta WCF de por medio, tecnología nueva en la que no cuento con la experiencia que atesoro con DCOM, Remoting o Web Servcies ‘normales’.
Así que esta vez es un poco más arduo encontrar la respuesta.
A mi siempre me a parecido una excelente opción para mover datos usar los objetos contenedores de datos que proporcionan los ‘primos’ de la familia ADO: ADO COM del de toda la vida, con su Recordset y ADO.Net con sus DataSets y sus DataTables. Son una solución comoda, rápida de implementar y simple para solucionar el problema de mover datos entre capas de una aplicación. Los Recordset del viejo ADO y su ‘serialización’ en DCOM tenian un excelente redimiento además.
Los más puristas os estareís llevando las manos a la cabeza, diciendo, ¡pero que aberración, donde estén las colecciones de objetos!. Sin duda es una opción mucho más OOP y mucho más ‘cool’, pero ¿son la solución más comoda y simple?. En mi opinión no. Tienen un problema que hacen que no sean, en la mayoría de los casos, mi opción preferida: cada vez que añades un campo, tienes que modificar varias capas de código, no vale con simplemente cambiar la SELECT. Además las colecciones no se vinculan tan bien a componentes de interfaz de usuario (aunque esto a mejorado mucho). Las ventajas son código más elegante y un pelín más simple y autodescriptivo. Los DataSet tipados son una aproximación intermedia, pero basicamente, tener que crear un nuevo contendor de datos siempre que variamos los campos que devolvemos puede llegar a ser extremadamente tedioso y poco práctico.
Con WCF los partidarios de las colecciones tienen además otro argumento a su favor: Devolver o recibir DataSets viola el principio de SOA que dice que ‘los servicios comparten esquema y contrato, no clases’. Es evidente que cuando devolvemos o recibimos DataSet estamos obligando a que los clientes de nuestro servicio sepan que es un DataSet. Pero la reglas están para romperlas, cuando hay un motivo de peso para ello. No quiero decir que este principio no sea importante, pero si estamos seguros de que nuestros servicios solo serán consumidos desde clientes .Net, devolver DataSet puede ser muy comodo.
Ahora se presentan una serie de incognitas:
¿Mejor DataSet o DataTables?
En principio, la lógica dice, que un DataTable es un objeto más ligero que un DataSet, luego parece que una buena opción es devolver DataTables en lugar de DataSet. Pero ocurre una cosa curiosa, no podemos devolver un DataTable no tipado desde un servicio. Al menos yo no he logrado que se cree correctamente el proxy de lado cliente. No he encontrado la explicación a esta cuestión, no se porque ocurre esto. En principio el DataTable es tan serializable como el DataSet en la versión 2.0 del Framework pero no lo he logrado crear un cliente que reciba un DataTable no tipado. Especulando, he llegado a la conclusión de que el framework no tiene suficiente información para serilizar un DataTable. En concreto en este artículo dicen que debemos proporcionar un esquema para los DataTable no tipados, lo que no nos separa en nada de devolver un DataTable no tipado, cada vez que añadamos un campo tendremos que modificar el esquema devuelto.
Con los DataTables tipados no hay problema, la cuestión es que no hay ganancia ninguna, pues un DataTable tipado, se serializa exactamente igual que el DataSet tipado que lo contiene. Vamos, que los bytes que se mueven son los mismos. Para averiguar esto, simplemente he enchufado el Ethereal, un sniffer de red, para ver cuantos datos se movian.
¿Cómo son de eficientes moviendo datos los DataSet en WCF?
Se puede reformular esta pregunta a ¿Cómo de eficiente es la serialización de los DataSet en WCF?. La verdad es que no mucho. Este es el aspecto de un DataSet construido sobre la tabla Address de la base de datos AdventureWorks, cuando se devuelve desde un servicio que usa netTCPBinding, y por tanto el enconding binario.
…… net.tcp://pc065:8383/DataService…
.
…Q/http://tempuri.org/IDataService/GetTypedDataSet net.tcp://pc065:8383/DataServiceV…s…a.V.D
…..D..M…t.^J…>.5.aD,D*…D…….V…xsi)http://www.w3.org/2001/XMLSchema-instance..xsd http://www.w3.org/2001/XMLSchema@.GetTypedDataSet..http://tempuri.org/…
…..z7http://tempuri.org/IDataService/GetTypedDataSetResponse.GetTypedDataSetResponse.http://tempuri.org/.GetTypedDataSetResultV…s…a.V.D
…..D..M…t.^J…>.5.aD…….V.B.
.B.A.xs.schema..id..AddressDataSet..targetNamespace.%http://tempuri.org/AddressDataSet.xsd..attributeFormDefault..qualified..elementFormDefault..qualified..xs http://www.w3.org/2001/XMLSchema..mstns%http://tempuri.org/AddressDataSet.xsd.%http://tempuri.org/AddressDataSet.xsd..msdata$urn:schemas-microsoft-com:xml-msdataA.xs.element..name..AddressDataSet..msdata.IsDataSet..true..msdata.UseCurrentLocale..trueA.xs.complexTypeA.xs.choice..minOccurs…maxOccurs..unboundedA.xs.element..name..AddressA.xs.complexTypeA.xs.sequenceA.xs.element..name..AddressID..msdata.ReadOnly..true..msdata
AutoIncrement..true..type..xs:int.A.xs.element..name..AddressLine1A.xs
simpleTypeA.xs.restriction..base..xs:stringA.xs.maxLength..value..60….A.xs.element..name..AddressLine2..minOccurs.A.xs
simpleTypeA.xs.restriction..base..xs:stringA.xs.maxLength..value..
60….A.xs.element..name..CityA.xs
simpleTypeA.xs.restriction..base..xs:stringA.xs.maxLength..value..30….A.xs.element..name.
PostalCode..minOccurs.A.xs
simpleTypeA.xs.restriction..base..xs:stringA.xs.maxLength..value..15….A.xs.element..name..Phone..minOccurs.A.xs
simpleTypeA.xs.restriction..base..xs:stringA.xs.maxLength..value..25….A.xs.element..name..StateProvinceID..type..xs:int..minOccurs..A.xs.element..name..CountryRegionC
ode..minOccurs.A.xs
simpleTypeA.xs.restriction..base..xs:stringA.xs.maxLength..value..3….A.xs.element..name..ModifiedDate..type..xs:dateTime.A.xs.element..name..rowguid..msdata.DataType.XSystem.Guid, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089..type..xs:string……A.xs.unique..name..Constraint1..msdata
PrimaryKey..trueA.xs.selector..xpath…//mstns:Address.A.xs.field..xpath..mstns:AddressID….A.diffgr.diffgram..diffgr)urn:schemas-microsoft-com:xml-diffgram-v1..msdata$urn:schemas-microsoft-com:xml-msdata@.AddressDataSet.%http://tempuri.org/AddressDataSet.xsd@.Address..diffgr.id..Address1..msdata.rowOrder.@.AddressID.@.AddressLine1.
1970 Napa Ct.@.City..Bothell@
PostalCode..36903@.Phone..819-555-0175@.StateProvinceID..79@.CountryRegionCode..US @.ModifiedDate..2002-12-12T11:37:20.187+01:00@.rowguid.$9aadcb0d-36cf-483f-84d8-585c2d4ec6e9.@.Address..diffgr.id..Address2..msdata.rowOrder.@.AddressID..2@.AddressLine1..9833 Mt. Dias
….Resto de DataSet hasta 6,2 MB…..
Llama la atención que se incluye una gran cantidad de metadatos. Lo primero que se pasa por la cabeza es quitar esos metadatos, para disminuir el tamaño de los datos que se ponen en la red, estableciendo la propiedad SchemaSerializationMode del DataSet a ExclueSchema, pero esto para mi sorpresa ¡no tienen ningún efecto sobre la serialización del DataSet en WCF!. En Remoting y Web Services esta propiedad tenía un efecto radical sobre el rendimiento.
El problema se recrudece cuando usamos un binding basado en Http, entonces el enconding es XML y por lo tanto tenemos un tamaño de los datos serializados mucho mayor, unos 11Mb frente a los 6,2Mb que teniamos antes. Podeís ver como aparecen un motón de etiquetas XML con metadatos en la captura adjunta.
Resumiento los DataSet son muy comodos, pero no son lo más eficiente de mundo, sobre todo cuando se usa encoding XML. La situación es más complicada por la incapacidad de retirar los metadatos. En cualquier caso la comodidad y la sencilled pueden a veces tener más peso que el rendimiento sobre todo si este es aceptable. Devolver 20000 filas y cargarlas en un grid, tarda menos de 2 segundos en una red local.
De todos modos que casi 20000 filas, ocupen 6,2Mb en el mejor de los casos, solo deben hacernos pensar una cosa: es una pésima idea devolver 20000 registros desde un servicio, en un DataSet o en una colección de objetos o por SMS.
Por cierto, un artículo muy recomendable sobre la serialización en WCF: Serialization in Windows Communication Foundation