Archivo Mensual de Octubre, 2008

Integer en VisualBasic.NET no es una clase.

Miniapunte que puede parecer absurdo pero que me ha hecho perder un ratito. El caso es que estaba intentando conseguir que un atributo de tipo Integer sin inicializar tuviera un valor nulo, para así poder insertarlo en un registro de una tabla de base de datos como NULL, ya que dada la naturaleza del atributo, un valor 0 (que es como un atributo de tipo Integer queda automáticamente inicializado) y un valor NULL significan cosas muy distintas. Pero resulta que no se puede hacer, ya que Integer no es una clase en VisualBasic.NET sino sólo un tipo de datos.

En el caso de un atributo de tipo String no hay problema, porque String sí puede ser una clase y por tanto sí acepta valores nulos, pero no así en el caso de Integer y Double, que son sólo tipos de datos y automáticamente quedan inicializados a 0. La solución -no óptima- puede pasar por utilizar la clase Object en su lugar o bien -algo más óptima en tanto que menos genérica- utilizar la clase Nullable y crear el atributo de tipo Nullable (Of Integer) o Nullable (Of Double).

Y la confusión me ha venido motivada porque en Java Integer sí es una clase…

En los grupos de discusión de Microsoft, Enrique Martínez ha dado una excelente explicación que copio literalmente:

«Tipos de datos» son todos: el tipo de dato Integer, el tipo de datos String, el tipo de datos Object, etc. Lo que ocurre es que mientras que los tipos de datos Object y String están definidos como «Class» (un tipo de datos por referencia), los tipos de datos como Short, Integer, Long, Single, Double, etc., se encuentran definidos como «Structure» (un tipo de datos por valor). Por este motivo, los valores de los tipos de datos «por referencia» (las clases), se inicializan a un valor «Nothing» en Visual Basic, mientras que los valores de los tipos de datos «por valor» (las estructuras), en principio se inicializan a 0.

Resumiendo, ante el código siguiente:

Dim n As Integer
Debug.Print(”El valor de n es: ” & n.ToString)
Debug.Print(”¿Es n nulo? ” & IsNothing(n))

Se obtiene la salida:

El valor de n es: 0
¿Es n nulo? False

Mientras que ante este otro código:

Dim n As Nullable(Of Integer)
Debug.Print(”El valor de n es: ” & n.ToString)
Debug.Print(”¿Es n nulo? ” & IsNothing(n))

Obtenemos esta otra salida:

El valor de n es:
¿Es n nulo? True

Ídem con Double, Long… ;-)

Pepecar o una mala opción para alquilar un coche.

pepecarComo cualquier autónomo, mis necesidades de desplazamiento son constantes y bastante ineludibles, así que cuando hace cosa de un par de meses mi coche dijo de golpe y sopetón hasta aquí hemos llegado, se me planteó un problema urgente que resolver. Me decidí a contratar un vehículo de renting a través de CaixaRenting (concretamente un Mazda2 que de momento va muy bien), pero como quiera que el plazo de entrega era de entre tres y cuatro semanas, opté también por alquilar un vehículo durante ese período para poder seguir acudiendo a las instalaciones del cliente principal para el cual trabajo.

Así que estuve sopesando varias alternativas para alquilar un coche para salir del apuro durante esas tres semanas. Y de entre todas las opciones y como soy gran defensor de las compañías de bajo coste en todos los sectores, me decidí por alquilar en Pepecar. Craso error. No le recomendaría a nadie hacerlo. Y paso a explicar mis motivos.

Alquilé un Smart Fortwo (el modelo más baratito que tenían disponible) para 22 días. En su página web mostraban un precio de lo más competitivo, algo inferior a las compañías de alquiler de vehículos tradicionales (Europcar, Hertz, Avis, etc). Como muestra, un simulacro que acabo de hacer ahora mismo para un alquiler de 22 días del mismo vehículo y en la misma sucursal:

O sea, unos 300 euros. Un buen precio, sí señor, unos 13 euros diarios, está bien. No obstante, a la que pasamos a la siguiente pantalla de contratación nos encontramos lo siguiente:

pepecar

No necesito silla de niño, no haré más de 500 kilómetros diarios y estoy seguro que no cancelaré. Puedo ahorrarme esos suplementos. La asistencia en carretera tiene un precio coherente y aunque es una práctica bastante triste que tengas que contratarla aparte, la marqué para no tener que andar empujando el Smart si se me quedaba tirado…

No obstante la cobertura extra de daños es exagerada. Prácticamente te dobla el precio del alquiler. O sea, que como reclamo muestran un precio muy interesante y después te dicen… paga el doble de lo que creías que ibas a pagar o hazte cargo de cualquier ralladita o golpecito que le des o te den al coche. No sé, esta clase de prácticas siempre me han parecido muy mediocres, la verdad. Pero el caso es que asumí el riego de tener que abonar los daños que pudiera causarle al coche y no marqué ese importe que casi me doblaba el importe reclamo con el que Pepecar me había llevado hasta ese punto.

Pues bien, durante esas tres semanas hubo un día que fui a recoger el coche donde lo había estacionado y me lo encontré con una rueda menos de las esperadas:

Vamos a ahorrarnos los calificativos que me merece quien se dedica a ir robando ruedecillas ajenas. El caso es que llamé a los Mossos d’Esquadra para presentar la correspondiente denuncia y me dijeron que enseguida venían para levantar acta de daños. Mientras tanto llamé a Pepecar. Aquí viene otra práctica a mi entender patética: su único teléfono de atención telefónica es un 807. O sea, que Pepephone vela por nuestras facturas de móvil pero después Pepecar nos penga un sablazo de padre y muy señor mío como les tengamos que preguntar cualquier cosita. Está bien, sí, sí, muy coherente.

Llegaron los Mossos y para mi sorpresa inicial me comentaron que no iban a levantar acta de daños alguna porque en realidad el coche no presentaba el más mínimo daño (lo cual de hecho era cierto) sino que simplemente había sufrido un robo. Pero que un robo y un daño son cosas distintas y que lo que sí tenía que hacer era ir a la comisaría más cercana a presentar denuncia por robo, pero que de acta de daños nanai.

A partir de aquí me salto las penurias de ese día (caminar arriba y abajo cargando los bártulos, acudir a la comisaría, acudir al taller, utilizar el transporte público -de un polígono industrial, o sea andar mucho-, no poder trabajar en toda la jornada, etc). Hasta que por la tarde me llaman desde Pepecar y me dicen que por la rueda me van a cargar 300 euros en mi tarjeta de crédito porque no contraté la cobertura extra de daños. Les explico que según mi entender (y el de los Mossos) lo que le ha pasado al coche no es un daño sino un robo, y que por tanto no aplica la cobertura de daños y que no soy yo sino la compañía con la que tengan contratado el seguro del coche la que debe hacer frente a la incidencia…

Como no nos ponemos de acuerdo contacto con la Agència Catalana de Consum en el 012 para consultarles el caso. Me hacen saber que Pepecar debe facilitarme una copia de la póliza de seguro que tiene contratada el Smart en cuestión porque así podremos ver si dicha póliza incluye el robo o no (prácticamente cualquier póliza a terceros suele incluir dicha contingencia), ya que si lo incluye es efectivamente la aseguradora y no yo quien debe hacerse cargo del importe de la rueda.

Informo de todo ello a Pepecar, pero me comentan que esa póliza está en la central de Madrid (cómo no) y que en la sucursal no disponen de copias (para qué). Quedo con ellos que para el día que tengo que devolver el coche me lo tengan por favor preparado… y hasta hoy.

Devolví el coche, no tenían la copia de la póliza, les di más días de margen para que por favor me la remitieran via correo electrónico, pero han pasado ya dos semanas desde entonces y no he recibido noticia alguna de Pepecar. Así que les acabo de enviar una hoja de reclamaciones (y archivos adjuntos varios) como primer paso para intentar recuperar esos 300 euros que sigo pensando no deberían haberme reclamado a mí sino a su compañía de seguros.

Pero en cualquier caso, mi recomendación personal para cualquiera que quiera alquilar un coche durante unos días es que no lo haga con Pepecar. Si contratas el seguro opcional te estarás moviendo en los mismos o mayores precios que otras compañías. Si no lo contratas te arriesgas a que cualquier mínimo incidente que tengas (ni hablemos de que tengas la mala fortuna de que te roben el coche) convierta el alquiler en una pequeña ruina. Entiendo que hay mejores alternativas, dadas las circunstancias. Y con suerte no tendrán un número 807 como único número de teléfono para atención telefónica.

PD1. Seguiré informando de cómo evoluciona este tema.

PD2. Todo el personal con el que traté de Pepecar, pese a nuestra manera dispar de entender lo que había pasado, educadísimo y muy amable en el trato, que una cosa no quita la otra.

Actualización.

12 de diciembre de 2008. Tal como comenté cuando escribí este post, tenía la intención de reclamar la devolución de los 300 euros y de seguir informando sobre como evolucionaba este caso. La segunda parte de este post ya esta disponible aquí.

UPDATE my_data SET my_age = 30;

Pues nada, ya de lleno en los treinta y con un montón de proyectos en mente, a ver qué sale de todo ello. De buenas a primeras con este blog llevo ya, sin darme cuenta, más de cinco meses compartiendo miniapuntes de programación (y de lo que va saliendo) y la experiencia está siendo de lo más positiva y enriquecedora. Pero andan también por ahí otros proyectos a los que habrá que ir dando forma cuando las horas no escaseen como estas últimas semanas… Lo que faltan no son ideas, es tiempo.

Primera vez con Amazon.

Minipost para comentar que ayer recibí mi primer pedido en Amazon (US). Todo estupendo. Llegó en 15 días clavados desde que hice el pedido (obvio seleccioné el transporte estándar más económico), así que es un período de lo más razonable. Los libros han llegado en perfectísimo estado, ni un rasguño, ni un toque, nada. Y el precio, como es bien sabido en Amazon, la mar de aceptable. Lástima del incremento que suponen los gastos de envío, a ver si pronto tenemos Amazon propio.

Los libros. El primero es el Getting Things Done, libro de productividad del que tengo referencias excelentes desde hace tiempo, la última de David Santos Orcero, y que quería agenciarme sí o sí. Ya está en mis manos, a ver cuando voy sacando ratos para comenzar a aplicar sus técnicas.

El segundo es el manual de certificación de MySQL. Para la próxima primavera tengo intención de sacarme dicha certificación, así que me ha parecido adecuado ir agenciándome ya el libro para comenzar a mirármelo con buenos ojos. Sobre este tema habla por ejemplo Pedro Cambra en su blog.

Software malintencionado de Windows.

Vaya por delante que en general me produce más simpatía como empresa Google que Microsoft. También es cierto que creo que en algunos puntos no andan tan lejos la una de la otra, y que si Microsoft hubiera hecho algunas de las cosas que Google ha llevado a cabo nos habríamos lanzado a su yugular, mientras que con Google somos mucho más permisivos y tolerantes. Pero bueno, se habrán ganado ese margen de crédito, no me parece mal.

El caso es que estos últimos días toquiteando cositas en mi PC me he encontrado con dos mensajes, uno de cada compañía, que me han resultado graciosos, con la salvedad que uno pretendía serlo y el otro no.

Voy primero con el de Google. En su día instalé el navegador Chrome, pero en tanto que nunca lo estaba utilizando y que esperaré a que esté en versiones más avanzadas y estables, decidí desinstalarlo. Al hacerlo me salió este mensaje:

Me hizo gracia ese ¿Es que hemos hecho algo mal? en tono victimista pero cómico. No sé, me pareció un guiño divertido con los usuarios.

Ahora el de Microsoft. Como sabréis, Windows Vista incorpora un gestor de actualizaciones bastante decente. Al menos si lo tienes bien configurado para que no tome el poder de tu máquina sin consultarte nada. Pues bien, en la última propuesta de actualizaciones me ha mostrado una bien curiosa:

¿Cómo que Herramienta de eliminacion de software malintencionado de Windows? ¿Me está diciendo que tengo en mi ordenador software malintencionado de Windows? ;-) No, no vale decir que eso es una redundancia… Pero cielos, a qué nivel de autocrítica han llegado los chicos de Microsoft. Admiten ya que existe tal cosa como software malintencionado de Windows. ¿Qué hará esa herramienta de eliminación de dicho software? ¿Un format C:?

PD. Lo sé, el ‘de Windows’ no se refiere realmente a ’software malintencionado’ sino a ‘Herramienta de eliminación’, pero igual les habría quedado mejor (¡aunque menos cómica!) una redacción como ‘Herramienta de Windows de eliminación de software malintencionado’…

Pinceladas de álgebra relacional.

Hoy algo de teoría. Como sabemos, el lenguaje estándar para realizar consultas en bases de datos es el SQL (Structured Query Language). Este lenguaje permite definir y manipular bases de datos relacionales, que son las bases de datos con las que solemos trabajar (como MySQL, Oracle, SQL Server y demás). Hay otro tipo de bases de datos que no son relacionales, pero su uso a día de hoy es tremendamente minoritario.

En una base de datos relacional la información está estructurada en forma de relaciones (ojo, no confundirse, en este ámbito ‘relación’ equivale a lo que habitualmente llamamos ‘tabla’) dando lugar a lo que se conoce como modelo relacional. En este modelo relacional las consultas se pueden llevar a cabo con lenguajes relacionales de dos tipos:

1) Lenguajes basados en álgebra relacional, que se inspira en la teoría de conjuntos para ir construyendo paso a paso el procedimiento a seguir para obtener los datos deseados. Estos lenguajes son lenguajes, pues, procedimentales.

2) Lenguajes basados en el cálculo relacional, que se basa en el cálculo de predicados de la lógica matemática. Estos lenguajes no describen un procedimiento y por tanto se les llama lenguajes declarativos (no procedimentales).

SQL es un lenguaje mixto, ya que incluye temas de álgebra relacional y otros de cálculo relacional, aunque predominan estos últimos, por lo que se considera un lenguaje declarativo. No obstante, hoy vamos a ver algunas pinceladas básicas de álgebra relacional.

Para especificar una consulta en álgebra relacional hay que ir siguiendo una serie de pasos que sirven para ir construyendo nuevas relaciones (recordemos: análogas a las tablas) a partir de las relaciones existentes. En estos pasos se utilizan las operaciones del álgebra relacional, que resumidas de un modo muy escueto son las siguientes.

0. Renombramiento.

Se trata de una operación que nos facilita trabajar con relaciones cambiándoles el nombre a ellas o a sus atributos. Así…

R (h) := RelacionConNombreOriginal (AtributoConNombreOriginal)

…renombra la relación a ‘R’ y uno de sus atributos a ‘h’ (del mismo modo que relaciones vienen a ser lo que habitualmente llamamos tablas, atributos vienen a ser lo que llamamos campos o columnas en esas tablas).

1. Unión.

Se trata de generar una nueva relación juntando todas las tuplas (tuplas = registros) de las dos relaciones que intervienen en la unión. Está claro que las dos relaciones deberán tener estructuras semejantes. Además, como en una relación (como en una tabla) no puede haber tuplas (registros) duplicadas, si al unirlas se da alguna duplicidad, se elimina directamente. Se denota con…

R := T U S

…donde ‘U’ representa el símbolo de unión y ‘T’ y ‘S’ son las relaciones de partida.

2. Intersección.

Se trata de generar una nueva relación con las tuplas que aparecen a la vez en las dos relaciones que intervienen en la intersección. También aquí las dos relaciones deberán tener estructuras semejantes. Se denota con…

R := T (U) S

…donde ‘(U)’ pretende ser el símbolo de intersección (’U’ invertida) que no sé cómo representar aquí en el blog y ‘T’ y ‘S’ son las relaciones de partida.

3. Diferencia.

Se trata de generar una nueva relación con las tuplas que aparecen en una primera relación pero no en una segunda. Nuevamente ambas relaciones deben tener estructuras semejantes. Se denota con…

R := T - S

…donde ‘T’ y ‘S’ son las relaciones de partida.

4. Producto cartesiano.

Se trata de generar una nueva relación combinando cada una de las tuplas de una primera relación con cada una de las tuplas de una segunda relación. Así pues la relación resultante tendrá tantos atributos como la suma de los atributos de cada una de las relaciones de partida. Y tendrá tantas tuplas como el producto de tuplas de ambas relaciones. Se denota con…

R := T x S

…donde ‘T’ y ‘S’ son las relaciones de partida.

5. Selección.

Se trata de generar una nueva relación con un subconjunto de las tuplas de la relación de partida. Para ello es preciso expresar alguna condición que las tuplas deberán cumplir para formar parte de la nueva relación. Se denota con…

R := T(C)

…donde ‘T’ es la relación de partida y ‘C’ la condición (puede ser compuesta) que deben cumplir las tuplas de ‘T’ para formar parte de ‘R’.

6. Proyección.

Se trata de generar una nueva relación con sólo algunos de los atributos de los que consta la relación de partida. Se denota con…

R := T [A1, A2, A3...]

…donde ‘T’ es la relación de partida y las ‘A’ representan los atributos que se quieren trasladar a la relación resultante ‘R’.

7. Combinación.

Se trata de generar una nueva relación a partir de un producto cartesiano entre dos relaciones de partida, con la salvedad que sólo formarán parte de la nueva relación las tuplas resultantes del producto que cumplan unas determinadas condiciones de igualdad entre atributos. Dios, suena fatal… pero es simplemente un JOIN. Se denota con…

R := T [B] S

…donde ‘T’ y ‘S’ son las relaciones de partida y ‘B’ es el conjunto de condiciones.

8. Combinación natural.

Igual que la combinación, sólo que no es necesario especificar las condiciones ‘B’ puesto que se asume que los atributos que se quieren igualar son los que se llaman igual en las dos relaciones de partida. Se denota con…

R := T * S

…donde ‘T’ y ‘S’ son las relaciones de partida.

Expresado así todo ello suena bastante complejo, la verdad. Pero si conocemos algo de SQL y hemos ido identificando las instrucciones veremos que es mucho más sencillo de lo que parece. Los renombramientos vienen a ser alias, las uniones vienen a ser UNIONs, las selecciones son SELECT * con
cláusulas WHERE, las proyecciones son SELECT con una selección de campos, etc.

Vamos a ver ahora un ejemplo interesante de cómo se construye una consulta con álgebra relacional. Imaginemos que tenemos una relación con las siguientes tuplas (voy a representarlo como tabla con registros para que resulte más familiar):

+-----------------+
|   biblioteca    |
+-------+---------+
| libro | paginas |
+-------+---------+
|   AAA |     125 |
|   BBB |     250 |
|   CCC |     300 |
|   DDD |     105 |
+-------+---------+

Si en SQL quisiéramos saber cuál es el menor número de páginas nos bastaría con hacer…

SELECT MIN(paginas) 
FROM biblioteca;

Y si quisiéramos saber cuál es el libro con ese menor número de páginas tendríamos que hacer…

SELECT libro 
FROM biblioteca 
WHERE paginas = (SELECT MIN(paginas) FROM biblioteca);

¿Pero cómo conseguimos encontrar esto con álgebra relacional donde NO tenemos una maravillosa función MIN()? Tenemos que hacerlo por pasos, más o menos así:

Paso 1.

Obtener una relación sólo con los números de páginas.

R1 := biblioteca [paginas]

+---------+
| paginas |
+---------+
|     125 |
|     250 |
|     300 |
|     105 |
+---------+
Paso 2.

Renombrar esta relación para poder llevar a cabo el siguiente paso. Podríamos haberlo hecho directamente en el paso 1, pero así queda más claro.

R2 (pags) := R1 (paginas)

+------+
| pags |
+------+
|  125 |
|  250 |
|  300 |
|  105 |
+------+
Paso 3.

Llevar a cabo una combinación entre ‘biblioteca’ y la recién obtenida ‘R2′ estableciendo como condición que ‘paginas’ sea mayor que ‘pags’.

R3 := biblioteca [paginas > pags] R2

+-------+---------+------+
| libro | paginas | pags |
+-------+---------+------+
|   AAA |     125 |  105 |
|   BBB |     250 |  125 |
|   BBB |     250 |  105 |
|   CCC |     300 |  125 |
|   CCC |     300 |  250 |
|   CCC |     300 |  105 |
+-------+---------+------+

Como vemos, hemos obtenido una relación donde aparecen todas las combinaciones en que ‘paginas’ tiene un valor superior a algún valor de ‘pags’. En esa relación tenemos pues todos los valores de ‘paginas’ que son mayores que algún otro valor de ‘pags’, y por tanto sólo faltan los valores de ‘paginas’ que no son mayores a ningún otro valor de ‘pags’ (o sea, los valores mínimos).

Paso 4.

Hacer una proyección para quedarnos sólo con los valores de ‘paginas’.

R4 := R3 [paginas]

+---------+
| paginas |
+---------+
|     125 |
|     250 |
|     300 |
+---------+
Paso 5.

Efectuar una diferencia entre los números de páginas de la relación original (lo tenemos en ‘R1′) y los que acabamos de obtener.

R5 := R4 - R1

+---------+
| paginas |
+---------+
|     105 |
+---------+

¡Bingo! Ya tenemos el valor mínimo de páginas… vamos ahora a terminar encontrando el título del libro con el menor número de páginas…

Paso 6.

Realizar una combinación natural entre la relación original y esta última.

R6 := biblioteca * R5

+-------+---------+
| libro | paginas |
+-------+---------+
|   DDD |     105 |
+-------+---------+
Paso 7.

Y para obtener el título del libro… una última proyección.

R7 := R6 [libro]

+-------+
| libro |
+-------+
|   DDD |
+-------+

Así pues, vemos que para llevar a cabo consultas con álgebra relacional debemos seguir una técnica procedimental en que vamos marcando los pasos que hay que ir siguiendo (haz esto y hazlo así). En cambio con SQL no necesitamos hacerlo de ese modo, ya que como hemos comentado al principio del post SQL es un lenguaje declarativo (haz esto, no me importa cómo lo hagas).

Nunca está de más tener algunas nociones de teoría de bases de datos, ¿no? ;-)

Valoración personal sobre el Barcelona PHP Conference.

Comentaba unos posts atrás mi intención de acudir al Barcelona PHP Conference que organizaban la gente del PHP Barcelona User Group. Pues bien, fui. Y disfruté. La verdad es que fue un evento excepcional, tal como ellos mismos comentan en sus conclusiones en su blog. Así que voy a limitarme a comentar algunos puntos que en mi opinión merecen mención especial:

1. Las instalaciones.

El Citilab de Cornellà es un centro absolutamente ideal para este tipo de actos. Mi más sincera enhorabuena para los responsables de haber llevado a cabo un proyecto como el Citilab. Tiene varias salas de conferencias absolutamente preparadas y habilitadas para llevar a cabo eventos de este tipo con toda comodidad. Dispone de facilidades tecnológicas varias y de un completo equipamiento que lo convierten en un marco ideal para actividades como estas ponencias. Genial, en serio.

2. La organización.

Estupenda. Desde el principio hasta el fin. La página web con información, la inscripción a la llegada, el respeto por los horarios, el asegurarse que nada falle, la contratación de un cátering exquisito… ni un pero se les puede poner. Incluso se molestaron en pasar a los asistentes una encuesta de satisfacción y mejora para próximas ediciones. Muy, muy bien.

3. El contenido de las conferencias.

Pude asistir a las de Marcus Bointon hablando de ‘Email in PHP’ (OK, understood, we won’t use mail() function!), Arno Schneider hablando de ‘Rasmus think again! Agile Framework == happy PHP Developer’ (YES! I’ll use a framework, I promise!), Pau Garcia-Milà hablando de ‘eyeOS: Open Source Web Desktop System in PHP’ (enlazo la web del proyecto eyeOS y no la suya personal porque creo que no la actualiza demasiado ;-) ) y finalmente Scott MacVicar hablando de ‘SQLite3′. Todas ellas estuvieron muy bien. Si tuviera que quedarme con alguna escogería las de Arno Schneider y Pau Garcia-Milà. La de Arno porque me pareció un brillantísimo ponente que domina contenidos y continentes, formas y fondos y que convierte una hora de ponencia en un rato fantástico. La de Pau porque siendo los dos del mismo pueblo todavía no había tenido oportunidad de escucharle en directo hablando de su proyecto, y la verdad es que me he marcado como asignatura pendiente el echarle un buen vistazo e intentar participar desarrollando algo en cuanto tenga algo más de tiempo.

Por desgracia algunas ponencias se solapaban y no pude asistir a otras que seguro también habrían resultado interesantes. No obstante, bajo mi criterio es un punto favorable lo de programar dos ponencias en paralelo para ofrecer a la gente varias alternativas en lugar de un único recorrido obligatorio.

4. Los patrocinadores (enlazados todos ellos aquí y aquí).

Siempre son necesarios en eventos así. Además en este caso tuvieron una presencia adecuada (no intrusiva), obsequiaron a los asistentes con algún detallito promocional y ayudaron a crear ambiente, así que genial.

Y nada más. Simplemente esperar la próxima edición confiando que no bajen el listón ni un centímetro y felicitar nuevamente a toda la organización por lo estupendo del resultado final.

Claves foráneas vs UNIQUE en MySQL.

Cuando diseñamos una tabla en MySQL (y en general en cualquier otro SGBD) solemos prestar mucha más importancia a las claves primarias que a las foráneas. Sin embargo las claves foráneas son también muy importantes principalmente por un doble motivo. Por un lado permiten mantener la integridad de la base de datos ante posibles modificaciones o eliminación de datos. Por otro lado aceleran enormemente determinadas combinaciones (joins) de tablas. No obstante, si estamos utilizando MySQL este segundo tema no depende estrictamente de las claves foráneas sino de la unicidad de las claves. Son temas parecidos pero no iguales, ya que para que una campo pueda ser referenciado por la clave foránea de otra tabla debe cumplir el criterio de unicidad, pero que lo cumpla no obliga a que alguna clave foránea tenga que referenciarlo. De esto es de lo que va a tratar este post.

Como el tema va a ser algo complicado de explicar sin unas tablas de datos a las que sujetarnos, voy a partir de tres tablas con las que desarrollaré toda la explicación. Estas tres tablas son las que paso a explicar a continuación.

Tabla de materiales vendidos.
mysql> DESCRIBE mat_materialsx;
+---------+----------------------+------+-----+---------+-------+
| Field   | Type                 | Null | Key | Default | Extra |
+---------+----------------------+------+-----+---------+-------+
| mat_ped | varchar(6)           | NO   | PRI |         |       |
| mat_pos | smallint(5) unsigned | NO   | PRI | 0       |       |
| mat_typ | varchar(6)           | YES  |     | NULL    |       |
| mat_fin | varchar(2)           | YES  |     | NULL    |       |
| mat_qty | double unsigned      | YES  |     | NULL    |       |
+---------+----------------------+------+-----+---------+-------+
5 rows in set (0.01 sec)

Es una tabla que recoge todos los materiales que se han vendido en algún momento a lo largo de la trayectoria de una empresa. Los campos mat_ped y mat_pos son la clave primaria e identifican el número de pedido de venta y la posición dentro de dicho pedido. No afectarán en nada a nuestro desarrollo, así que podemos olvidarnos de ellos. En cambio los restantes campos serán importantes: mat_typ indica el tipo de material, mat_fin indica el acabado de ese material y mat_qty indica el número de kilos vendidos en ese pedido y posición.

Quedará más claro viendo algunos datos que puede contener esta tabla:

mysql> SELECT * FROM mat_materialsx LIMIT 5;
+---------+---------+---------+---------+---------+
| mat_ped | mat_pos | mat_typ | mat_fin | mat_qty |
+---------+---------+---------+---------+---------+
| 100023  |       1 | 430     | XP      |     800 |
| A09085  |       1 | 430     | BA      |    3000 |
| A09086  |       1 | 430     | BA      |    7380 |
| A09138  |       1 | 409     | 2B      |     538 |
| A09139  |       1 | 409     | 2B      |     539 |
+---------+---------+---------+---------+---------+
5 rows in set (0.00 sec)

Lo he limitado a 5 resultados pero en realidad en esta tabla es donde estará el enorme volumen de datos, ya que la tabla entera contendrá…

mysql> SELECT COUNT(*) FROM mat_materialsx;
+----------+
| COUNT(*) |
+----------+
|   177047 |
+----------+
1 row in set (0.06 sec)

El objetivo final que querremos conseguir (para lo cual necesitaremos llevar a cabo una consulta) es una agrupación de esos 177.047 materiales vendidos. Para ello agruparemos tanto los tipos de material como los acabados a través de unas tablas de parametrización en las que le diremos qué tipo de material o qué acabado agrupado corresponden a cada tipo de material o acabado original. Estas dos tablas son las que ahora vemos. Primero la de tipos de material.

Tabla de agregación de tipos de material.
mysql> DESCRIBE typ_alltypesx;
+---------+------------+------+-----+---------+-------+
| Field   | Type       | Null | Key | Default | Extra |
+---------+------------+------+-----+---------+-------+
| typ_bas | varchar(6) | NO   | PRI |         |       |
| typ_agg | varchar(6) | NO   |     |         |       |
+---------+------------+------+-----+---------+-------+
2 rows in set (0.01 sec)

Es una tabla extremadamente simple donde sólo consta el tipo de material básico (el original, el que aparece en los pedidos) y el tipo de material agrupado que le corresponde a cada tipo de material básico.

Algunos datos de los que consta son los siguientes:

mysql> SELECT * FROM typ_alltypesx LIMIT 5;
+---------+---------+
| typ_bas | typ_agg |
+---------+---------+
| 14462   | 524     |
| 202     | 202     |
| 301PK   | 524     |
| 304     | 304     |
| 304/L   | 304     |
+---------+---------+
5 rows in set (0.00 sec)

Así, los materiales 14462 y 301PK quedarán agrupados como material 524. Los 304 y 304/L quedarán como 304. Y así para un total de unos 70 materiales.

Tabla de agregación de acabados.
mysql> DESCRIBE fin_allfinish;
+---------+------------+------+-----+---------+-------+
| Field   | Type       | Null | Key | Default | Extra |
+---------+------------+------+-----+---------+-------+
| fin_bas | varchar(2) | NO   | PRI |         |       |
| fin_agg | varchar(2) | NO   |     |         |       |
+---------+------------+------+-----+---------+-------+
2 rows in set (0.01 sec)

También es una tabla extremadamente simple donde sólo consta el acabado básico (el original, el que aparece en los pedidos) y el acabado agrupado que le corresponde a cada acabado básico.

Algunos datos de los que consta son los siguientes:

mysql> SELECT * FROM fin_allfinish LIMIT 5;
+---------+---------+
| fin_bas | fin_agg |
+---------+---------+
| 1       | 1       |
| 1D      | 1       |
| 1P      | K3      |
| 2B      | 2B      |
| 2C      | 2B      |
+---------+---------+
5 rows in set (0.00 sec)

Así, los acabados 1 y 1D quedarán agrupados como acabado 1. El 2B y el 2C quedarán agrupados como acabado 2B. Y así para un total de 94 acabados.

Primera consulta de agrupación.

Tenemos pues las tres tablas presentadas. Vamos a llevar a cabo una primera ejecución de la consulta de combinación y agrupación que necesitaremos para obtener el resultado deseado y evaluaremos cuánto tiempo ha tardado MySQL en devolvernos el resultado:

mysql> SELECT typ_agg AS type, 
    -> fin_agg AS finish, 
    -> SUM(mat_qty) AS quantity
    -> FROM 
    -> ((mat_materialsx LEFT JOIN typ_alltypesx ON mat_typ = typ_bas) 
    -> LEFT JOIN fin_allfinish ON mat_fin = fin_bas)
    -> GROUP BY typ_agg, fin_agg;
+-------+--------+-----------+
| type  | finish | quantity  |
+-------+--------+-----------+
| 202   | 2B     |      7217 |
| 202   | BA     |     31140 |
| 430   | 1      |    994298 |
| 430   | 2B     |  11621394 |
| 430   | BA     |  22678569 |
| 430   | K3     |    546858 |
| 441   | 2B     |   2862123 |
| 441   | K3     |     55160 |
| 524   | 1      |   4930442 |
| 524   | 2B     |  23379844 |
| 524   | BA     |   4499020 |
| 524   | K3     |    652620 |
+-------+--------+-----------+
12 rows in set (0.95 sec)

No está nada mal. En apenas un segundo ha hecho los correspondientes joins y las agregaciones de los 177.047 registros para devolvernos lo que deseábamos obtener. Entonces… ¿si estoy defendiendo la importancia de las claves foráneas y sobretodo de la unicidad, no es un contrasentido haber obtenido unos resultados tan buenos? No, no lo es. Tiene una explicación. Fijémonos que estamos haciendo las combinaciones con los cambos typ_bas y fin_bas que son claves principales en sus respectivas tablas y que por tanto son campos únicos en ellas. Dicho de otro modo, MySQL no necesita estrictamente que existan claves foráneas (en este caso serían mat_typ y mat_fin) referenciando a estas claves primarias para ejecutar la consulta rápidamente. Lo que sí necesita es que exista ese unicidad.

Bien, vamos a complicarlo y se entenderá mejor -espero-. Imaginemos que no estamos trabajando con datos de una sola empresa sino que formamos parte de un pequeño grupo de empresas que comparten base de datos. Así, en las tablas de parametrización de tipos de material y acabados, la clave principal no es únicamente el tipo de material y el acabado, sino que incorporamos también el código de la empresa en cuestión, ya que el material 304/L en una empresa se agrupará como 304 mientras que en otra se agrupará como 524 (es un decir).

De este modo las dos tablas quedarán ahora así.

Tabla de agregación de tipos de material.
mysql> describe typ_alltypesx;
+---------+------------+------+-----+---------+-------+
| Field   | Type       | Null | Key | Default | Extra |
+---------+------------+------+-----+---------+-------+
| typ_cpy | varchar(3) | NO   | PRI |         |       |
| typ_bas | varchar(6) | NO   | PRI |         |       |
| typ_agg | varchar(6) | NO   |     |         |       |
+---------+------------+------+-----+---------+-------+
3 rows in set (0.01 sec)

Con unos datos como estos (AMG es el código de una de las empresas, concretamente de la que nos interesará, ya que pondremos absolutamente todos los datos con este código de empresa):

mysql> SELECT * FROM typ_alltypesx LIMIT 5;
+---------+---------+---------+
| typ_cpy | typ_bas | typ_agg |
+---------+---------+---------+
| AMG     | 14462   | 524     |
| AMG     | 202     | 202     |
| AMG     | 301PK   | 524     |
| AMG     | 304     | 304     |
| AMG     | 304/L   | 304     |
+---------+---------+---------+
5 rows in set (0.00 sec)
Tabla de agregación de acabados.
mysql> describe fin_allfinish;
+---------+------------+------+-----+---------+-------+
| Field   | Type       | Null | Key | Default | Extra |
+---------+------------+------+-----+---------+-------+
| fin_cpy | varchar(3) | NO   | PRI |         |       |
| fin_bas | varchar(2) | NO   | PRI |         |       |
| fin_agg | varchar(2) | NO   |     |         |       |
+---------+------------+------+-----+---------+-------+
3 rows in set (0.01 sec)

Con unos datos como estos:

mysql> SELECT * FROM fin_allfinish LIMIT 5;
+---------+---------+---------+
| fin_cpy | fin_bas | fin_agg |
+---------+---------+---------+
| AMG     | 1       | 1       |
| AMG     | 1D      | 1       |
| AMG     | 1P      | K3      |
| AMG     | 2B      | 2B      |
| AMG     | 2C      | 2B      |
+---------+---------+---------+
5 rows in set (0.00 sec)
Segunda consulta de agrupación.

Tenemos pues de nuevo las tres tablas presentadas, sólo que ahora las dos últimas han sufrido un pequeño cambio, aunque aparentemente no demasiado importante puesto que sólo hemos añadido un campo adicional, pero no hemos tocado los que ya había y siguen siendo claves primarias los que lo eran anteriormente. Vamos a llevar a cabo la segunda ejecución de la consulta de combinación y agrupación y evaluaremos cuánto tiempo ha tardado esta vez MySQL en devolvernos el resultado:

mysql> SELECT typ_agg AS type, 
    -> fin_agg AS finish, 
    -> SUM(mat_qty) AS quantity
    -> FROM 
    -> ((mat_materialsx LEFT JOIN typ_alltypesx ON mat_typ = typ_bas) 
    -> LEFT JOIN fin_allfinish ON mat_fin = fin_bas)
    -> GROUP BY typ_agg, fin_agg;
+-------+--------+-----------+
| type  | finish | quantity  |
+-------+--------+-----------+
| 202   | 2B     |      7217 |
| 202   | BA     |     31140 |
| 430   | 1      |    994298 |
| 430   | 2B     |  11621394 |
| 430   | BA     |  22678569 |
| 430   | K3     |    546858 |
| 441   | 2B     |   2862123 |
| 441   | K3     |     55160 |
| 524   | 1      |   4930442 |
| 524   | 2B     |  23379844 |
| 524   | BA     |   4499020 |
| 524   | K3     |    652620 |
+-------+--------+-----------+
12 rows in set (21.02 sec)

Nada menos que 21 segundos. Una barbaridad. De acuerdo que tiene que combinar y agrupar 177.047 registros, pero tampoco son tantos… ¿qué pasará cuando sean millones? El problema que nos ha aparecido es ni más ni menos que la unicidad. Antes los campos typ_bas y fin_bas que se utilizan para los joins eran únicos. Ahora ya no lo son. Vamos a intentar solucionarlo.

Establecimiento de unicidad.

Para ello ejecutaremos las siguientes sentencias:

mysql> ALTER TABLE typ_alltypesx 
    -> CHANGE typ_bas typ_bas VARCHAR(6) UNIQUE;
Query OK, 70 rows affected (0.03 sec)
Records: 70  Duplicates: 0  Warnings: 0

mysql> DESCRIBE typ_alltypesx;
+---------+------------+------+-----+---------+-------+
| Field   | Type       | Null | Key | Default | Extra |
+---------+------------+------+-----+---------+-------+
| typ_cpy | varchar(3) | NO   | PRI |         |       |
| typ_bas | varchar(6) | NO   | PRI |         |       |
| typ_agg | varchar(6) | NO   |     |         |       |
+---------+------------+------+-----+---------+-------+
3 rows in set (0.01 sec)

mysql> ALTER TABLE fin_allfinish 
    -> CHANGE fin_bas fin_bas VARCHAR(2) UNIQUE;
Query OK, 94 rows affected (0.03 sec)
Records: 94  Duplicates: 0  Warnings: 0

mysql> DESCRIBE fin_allfinish;
+---------+------------+------+-----+---------+-------+
| Field   | Type       | Null | Key | Default | Extra |
+---------+------------+------+-----+---------+-------+
| fin_cpy | varchar(3) | NO   | PRI |         |       |
| fin_bas | varchar(2) | NO   | PRI |         |       |
| fin_agg | varchar(2) | NO   |     |         |       |
+---------+------------+------+-----+---------+-------+
3 rows in set (0.01 sec)

Aparentemente las tablas no han cambiado. Al menos no nos muestra cambios en el DESCRIBE. Sin embargo sí que lo han hecho, ya que le hemos establecido que los campos typ_bas y fin_bas además de formar parte de claves primarias, son en sí mismos únicos. Si quisiéramos que claves foráneas de otras tablas hicieran referencia a estos campos ahora podríamos hacerlo (antes no habríamos podido ya que MySQL nos habría devuelto un error), pero en realidad para evaluar la velocidad de la consulta no nos es estrictamente necesario. Ya he comentado antes que para la velocidad a la hora de combinar y agrupar MySQL requiere unicidad, no necesariamente claves foráneas. Vamos a verlo.

Tercera consulta de agrupación.

Respecto a la ejecución anterior sólo hemos cambiado que ahora las dos tablas de parametrización han visto como sus campos clave utilizados en los joins se han hecho únicos. Veamos cómo va la tercera ejecución de la consulta de combinación y agrupación y evaluaremos cuánto tiempo ha tardado ahora MySQL en devolvernos el resultado:

mysql> SELECT typ_agg AS type, 
    -> fin_agg AS finish, 
    -> SUM(mat_qty) AS quantity
    -> FROM 
    -> ((mat_materialsx LEFT JOIN typ_alltypesx ON mat_typ = typ_bas) 
    -> LEFT JOIN fin_allfinish ON mat_fin = fin_bas)
    -> GROUP BY typ_agg, fin_agg;
+-------+--------+-----------+
| type  | finish | quantity  |
+-------+--------+-----------+
| 202   | 2B     |      7217 |
| 202   | BA     |     31140 |
| 430   | 1      |    994298 |
| 430   | 2B     |  11621394 |
| 430   | BA     |  22678569 |
| 430   | K3     |    546858 |
| 441   | 2B     |   2862123 |
| 441   | K3     |     55160 |
| 524   | 1      |   4930442 |
| 524   | 2B     |  23379844 |
| 524   | BA     |   4499020 |
| 524   | K3     |    652620 |
+-------+--------+-----------+
12 rows in set (1.43 sec)

Genial. Hemos pasado de 21 segundos a apenas uno y medio. La mejora es espectacular… ¿Qué ocurriría ahora si aprovechamos que esos campos ya son únicos para hacer que también sean el destino de claves foráneas en la tabla principal? Veámoslo.

Establecimiento de claves foráneas.
mysql> ALTER TABLE mat_materialsx 
    -> ADD FOREIGN KEY(mat_typ) REFERENCES typ_alltypesx(typ_bas);
Query OK, 177047 rows affected (3.71 sec)
Records: 177047  Duplicates: 0  Warnings: 0

mysql> ALTER TABLE mat_materialsx 
    -> ADD FOREIGN KEY(mat_fin) REFERENCES fin_allfinish(fin_bas);
Query OK, 177047 rows affected (4.10 sec)
Records: 177047  Duplicates: 0  Warnings: 0

Cuarta consulta de agrupación.

¿Habremos mejorado algo al incorporar estas claves foráneas? Veamos, veamos…

mysql> SELECT typ_agg AS type, 
    -> fin_agg AS finish, 
    -> SUM(mat_qty) AS quantity
    -> FROM 
    -> ((mat_materialsx LEFT JOIN typ_alltypesx ON mat_typ = typ_bas) 
    -> LEFT JOIN fin_allfinish ON mat_fin = fin_bas)
    -> GROUP BY typ_agg, fin_agg;
+-------+--------+-----------+
| type  | finish | quantity  |
+-------+--------+-----------+
| 202   | 2B     |      7217 |
| 202   | BA     |     31140 |
| 430   | 1      |    994298 |
| 430   | 2B     |  11621394 |
| 430   | BA     |  22678569 |
| 430   | K3     |    546858 |
| 441   | 2B     |   2862123 |
| 441   | K3     |     55160 |
| 524   | 1      |   4930442 |
| 524   | 2B     |  23379844 |
| 524   | BA     |   4499020 |
| 524   | K3     |    652620 |
+-------+--------+-----------+
12 rows in set (1.45 sec)

En este caso la respuesta es que no. Nos hemos quedado exactamente igual. No obstante con todo esto no quiero decir que las claves foráneas no sean necesarias, ya que como he dicho al comenzamiento del post, nos sirven para mantener la integridad de la base de datos (¡aspecto fundamental!). Lo único que pretendo con este post es dejar claro que MySQL no las necesita estrictamente para combinar eficientemente distintas tablas, ya que es capaz de hacerlo igual de bien simplemente con que los campos que utilizamos para los joins sean únicos.

Nota importante.

A lo largo de todo el ejemplo es básico ir borrando la caché de consultas de MySQL para no obtener resultados distorsionados. Para ello he utilizado la sentencia:

mysql> RESET QUERY CACHE;
Query OK, 0 rows affected (0.00 sec)

De lo contrario nos encontraremos con tiempos de respuesta maravillosos de 0.00 sec… ;-)

Estadística de visitas (200809).

Como vengo haciendo siempre, paso a publicar las estadísticas de visitas a este blog del mes recién cerrado. Si alguien está interesado en realizar el seguimiento, puede hacerlo a través del tema habitual visitas, que aglutina todos estos posts. Además he cambiado un poco la información que muestro al respecto porque creo que ahora aparecen datos algo más relevantes.

Septiembre 2008.

Número de visitas totales:       1769     (+ 67,83%)
Páginas individuales vistas:     2293     (+ 62,97%)
Promedio de tiempo en el sitio:  00:01:12 (- 15,29%)
Porcentaje de rebote:            83,72%   (+  1,94%)
Porcentaje de visitas nuevas:    82,42%   (-  4,49%)

El día con más visitas del mes ha sido el 22 de septiembre con un total de 90.

Y los tres posts que durante el mes han sido más visitados son los siguientes:

1. Informe en .NET con Crystal Reports y base de datos MySQL. (13,08%)
2. Ambrosio o cómo crear un servicio de Windows obediente en .NET. (7,50%)
3. Crear librería .dll en .NET… ¡y utilizarla! (6,19%)

Por último y sintiéndolo mucho, esta vez no hay sección Premio Google del Mes. Lo cierto es que hay muchísimas búsquedas en la lista pero ninguna lo suficientemente extraña como para ser galardonada, así que en esta ocasión el premio queda desierto… :-(

Pasando parámetros al informe en .NET con Crystal Reports.

Hace unas semanas publiqué el post Informe en .NET con Crystal Reports y base de datos MySQL, que se convirtió rápidamente en el post más visitado de este blog. También en el que más comentarios ha recibido, y precisamente de uno de ellos ha surgido este apéndice a dicho post.

Recordemos que en él enseñábamos cómo crear un informe con Crystal Reports utilizando un archivo XML y un DataTable (también servía un DataSet). En éste vamos a ver cómo podemos pasar parámetros al informe creado, por ejemplo para enviar el valor de un TextBox (aunque en mi ejemplo utilizaré una constante) y mostrarlo en el informe o bien utilizarlo para filtrar qué registros se tienen que mostrar y cuáles no.

Vamos a meternos pues en faena.

Paso 1. Recopilatorio de lo que teníamos.

Partíamos de un par de tablas de MySQL y una vista que se había establecido así:

CREATE VIEW zbl_bill2print AS 
(
SELECT
    blh_num AS BILL_NUMBER,
    blh_dat AS BILL_DATE,
    blh_cus AS BILL_CUSTOMER,
    blp_pos AS LINE_NUMBER,
    blp_art AS LINE_ARTICLE,
    blp_pri AS LINE_UNITPRICE,
    blp_qty AS LINE_UNITS,
    blp_pri * blp_qty AS LINE_TOTALPRICE
FROM
    blh_billheader LEFT JOIN blp_billposits ON blh_num = blp_num
WHERE
    blh_num = 4
);

En la que por tanto filtrábamos los valores para obtener sólo los registros correspondientes a la factura número 4. Bien, vamos a cambiar eso para que ahora nuestro DataTable contenga todos los registros correspondientes a todas las facturas. La vista quedará pues ahora así:

CREATE VIEW zbl_bill2print AS 
(
SELECT
    blh_num AS BILL_NUMBER,
    blh_dat AS BILL_DATE,
    blh_cus AS BILL_CUSTOMER,
    blp_pos AS LINE_NUMBER,
    blp_art AS LINE_ARTICLE,
    blp_pri AS LINE_UNITPRICE,
    blp_qty AS LINE_UNITS,
    blp_pri * blp_qty AS LINE_TOTALPRICE
FROM
    blh_billheader LEFT JOIN blp_billposits ON blh_num = blp_num
);

El resto de objetos siguen de momento igual. Esto es, el formulario frmMain y el reporte rptBill. No es preciso volver a generar el archivo XML para los cambios que vamos a hacer.

Paso 2. Añadir parámetro en el informe rptBill.

En el Explorador de campos hacemos click derecho en la opción Campos de parámetro para crear un nuevo parámetro. Lo creamos dándole un nombre (BillNumber) y un tipo de valor (Número) y aceptamos.

Si y sólo si tenemos algún interés en que este parámetro que posteriormente le enviaremos al informe aparezca en algún sitio de este informe, lo añadiremos a su área de impresión. En mi caso y sólo para que todo quede más claro, lo añado tal como se muestra en la imagen siguiente (el parámetro es el campo ?BillNumber):

Pero insisto en que lo importante ha sido crearlo. Arrastrarlo hasta el área de impresión del informe es absolutamente opcional.

Paso 3. Establecer el filtro para el informe.

En este ejemplo partimos de un DataTable con varios registros pertenecientes a distintas facturas y desearemos filtrar el reporte para que nos muestre sólo un determinado número de factura. Para ello tenemos pues que establecer este filtro.

Para ello, hacemos click derecho en cualquier punto del área de impresión del report y seleccionamos la opción Report - Fórmula de selección - Registro. En el editor que nos aparece seleccionamos los campos correspondientes para terminar creando la siguiente fórmula:

{zbl_bill2print.BILL_NUMBER} = {?BillNumber}

Guardamos el filtro y cerramos el editor.

Paso 4. Enviar el valor del parámetro desde el formulario.

Ya hemos creado un parámetro en el informe y hemos definido también un filtro basado en ese parámetro. Lo último que nos queda es pues informar el valor que deseamos que coja ese parámetro. En este caso lo haremos enviándoselo desde el formulario a través de una constante, pero como he dicho antes podríamos hacerlo tomando el valor, por ejemplo, de un TextBox o un DataGridView.

Para enviar el valor simplemente debemos añadir esta línea al código del formulario:

RD.SetParameterValue(”BillNumber”, BILL_NUMBER)

Con lo cual el código íntegro del formulario queda como sigue:

'--------------------------------------------------------------------
' Author:      Albert Mata (www.albertmata.net)
' Date:        20081001
' Description: Form to show how to create a report using just an XML
'              file, and how to send parameters to it.
'--------------------------------------------------------------------
Imports CrystalDecisions.CrystalReports.Engine

Public Class frmMain

    '----------------------------------------------------------------
    ' Constants.
    '----------------------------------------------------------------
    Const BILL_NUMBER As Integer = 4

    '----------------------------------------------------------------
    ' Creates XML file (just once) and creates and loads a report.
    '----------------------------------------------------------------
    Private Sub frmMain_Load(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles MyBase.Load
        'Creating XML file.
        Dim RC As New clsReportCreator("zbl_bill2print")
        'RC.CreateXMLFile("C:\")

        'Creating report.
        Dim RD As ReportDocument = New rptBill()

        'Setting data source for report.
        Dim DT As DataTable = RC.GetDataTable()
        RD.SetDataSource(DT)

        'Setting data source for possible subreports.
        For Each SR As ReportDocument In RD.Subreports
            If SR.Database.Tables.Count > 0 Then
                SR.SetDataSource(DT)
            End If
        Next

        'Setting recently created report must be shown in viewer.
        Me.crvBill.ReportSource = RD

        'Sending parameter to the report.
        RD.SetParameterValue("BillNumber", BILL_NUMBER)

    End Sub

End Class

Y esto es todo. A partir de aquí si ejecutamos la aplicación obtendremos el informe correspondiente.

Concretamente tal como está el código obtendremos lo siguiente:

Es decir, la factura número 4 porque hemos establecido el valor de la constante BILL_NUMBER a 4. Si cambiamos ese valor por un 3 y volvemos a ejecutar, obtendremos esto otro:

O sea, la factura número 3.

Vemos además que tanto en una como en la otra aparecen esos 4,00 y 3,00 fruto de que en el paso opcional de añadir el parámetro al área de impresión del informe, yo sí lo hice.