Archivo de la Categoría 'MySQL'

Report in .NET using Crystal Reports and MySQL database.

This is just the first of some English posts that I’ll publish by translating most popular posts in this blog. Original version (in Spanish) is here.

First of all, I must asume that creating reports is one of that things I like worst in programming. But it’s quite obvious that few serious applications don’t need them, and the one I’m developing now isn’t an exception to this rule. So I’ve been creating some reports recently and I’ve discovered a new way to do it. And that’s what I explain in this post.

As I’ve said sometimes before in previous posts, I develop with VisualBasic.NET and MySQL database. And I use Crystal Reports to create reports, as this tool is integrated in VisualStudio.NET. Since now, I used to use an ODBC connection configured in each PC to connect to MySQL server. But I didn’t like this system much, because actually I’m not working just with one database but with some with different names. They have the same structure, tables and data, but just one is the good one, as the rest are just for developing purposes. It’s really easy to use one or other connection string to make the application connect with one or other database, but with reports it isn’t so easy as they take data using that ODBC connection (and it just can connect with one database).

But now I’ve discovered how to create reports with just a DataTable and an XML schema, needing nothing else. Actually it’s possible to use a DataSet instead of a DataTable as well. So I’m going to explain it with an easy example and some images.

I’ll work with two tables in my MySQL database where I’ll keep information about bills. First table is the one with information about headers and has this data:

+---------+------------+-------------------------+
| blh_num | blh_dat    | blh_cus                 |
+---------+------------+-------------------------+
|       1 | 2008-07-30 | CERAMICAS PEPE, S.A.    |
|       2 | 2008-07-30 | TALLERES GOMEZ, S.L.    |
|       3 | 2008-07-31 | DEPORTES DAMIAN, S.L.   |
|       4 | 2008-07-31 | SOFTWARE ALBERTMATA.NET |
+---------+------------+-------------------------+

Second table is the one with information about positions and has this rows:

+---------+---------+------------------------+---------+---------+
| blp_num | blp_pos | blp_art                | blp_pri | blp_qty |
+---------+---------+------------------------+---------+---------+
|       1 |       1 | RATON LOGITECH         |   15.95 |       1 |
|       2 |       1 | MONITOR LG 19 PULGADAS |   210.5 |       1 |
|       3 |       1 | ROUTER DLINK           |      56 |       1 |
|       4 |       1 | RATON LOGITECH         |   15.95 |       2 |
|       4 |       2 | TECLADO LOGITECH       |   12.95 |       1 |
|       4 |       3 | RECEPTOR GPS ZAPPA     |   59.95 |       1 |
|       4 |       4 | PAQUETE 500 FOLIOS     |     3.7 |       4 |
+---------+---------+------------------------+---------+---------+

It’s something really simple and not normalized, but will be enough for this example, as we’re going to create a report that will be the inovice for purchase number 4 (the one with customer SOFTWARE ALBERTMATA.NET). Obviously, we’ll need information about both tables but I just want to work with one DataTable, so first of all I’m going to create a MySQL view with this sentence:

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

So, from now on the report will be created using this zbl_bill2print view. Let’s go with the .NET part.

Step 1. Creating XML file containing table/view structure.

Along this post we’ll work with these three things:

1) a Windows form (frmMain) where we’ll have the report viewer object.
2) a class (clsReportCreator) we’re going to create right now.
3) a report (rptBill) that will be the invoice we want to print.

So let’s start creating clsReportCreator class. It’ll have only one attribute (the name of the table or view), one constructor method, one method to load DataTable object and one last method to generate the XML file. Here is the full code for this class:

'--------------------------------------------------------------------
' Author:      Albert Mata (www.albertmata.net)
' Date:        20080731
' Needs:       MySQL.Data reference.
' Description: Class to create a report using just an XML file. 
'--------------------------------------------------------------------
Imports MySql.Data.MySqlClient

Public Class clsReportCreator

    '----------------------------------------------------------------
    ' Attributes.
    '----------------------------------------------------------------
    Private TableOrView As String

    '----------------------------------------------------------------
    ' Constructor method.
    '----------------------------------------------------------------
    Public Sub New(ByVal TableOrView As String)
        Me.TableOrView = TableOrView
    End Sub

    '----------------------------------------------------------------
    ' Returns DataTable corresponding to TableOrView.
    '----------------------------------------------------------------
    Public Function GetDataTable() As DataTable
        Dim DA As MySqlDataAdapter
        Dim DS As New DataSet
        Dim DT As DataTable
        Dim ConnectionString As String
        Dim SQL As String

        'Setting connection string to connect to MySQL database.
        ConnectionString = "Database = blog; " _
                         & "Data Source = localhost; " _
                         & "User ID = root; " _
                         & "Password = mypassword"

        'Setting SQL string.
        SQL = "SELECT * FROM " & Me.TableOrView

        'Getting data and filling DataSet and DataTable.
        DA = New MySqlDataAdapter(SQL, ConnectionString)
        DA.Fill(DS, Me.TableOrView)
        DT = DS.Tables(Me.TableOrView)

        'Returning DataTable.
        Return DT
    End Function

    '----------------------------------------------------------------
    ' Creates XML file in desired path.
    '----------------------------------------------------------------
    Public Sub CreateXMLFile(ByVal FilePath As String)
        Dim DT As DataTable

        'Creating DataTable.
        DT = Me.GetDataTable()

        'Writting XML file in desired path.
        DT.WriteXmlSchema(FilePath & Me.TableOrView & ".xml")
    End Sub

End Class

And we also create frmMain form, which only code by the moment will be this:

'--------------------------------------------------------------------
' Author:      Albert Mata (www.albertmata.net)
' Date:        20080731
' Description: Form to show how to create a report using just an XML
'              file. 
'--------------------------------------------------------------------
Public Class frmMain

    '----------------------------------------------------------------
    ' As a first step, creates XML file.
    '----------------------------------------------------------------
    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:\")
    End Sub

End Class

Right now we have a first application. If we execute it we’ll get C:\zbl_bill2print.xml file with the structure of zbl_bill2print view. So we run it and get that file.

Step 2. Creating report and loading data source.

First, we add a report to our project and give it a name like rptBill.rpt. We create it choosing empty report option, so desestimating any templates.

Now we go to Fields explorer menu and right-click the first option (Database fields). In new contextual menu we click on Database assistant option.

After this we get the Available data source menu, where we choose Create new connection and after that ADO.NET option.

Making this, we’ll see a new form where we’ll be asked about File’s path. In this point we have to find XML file we’ve created before (in my example C:\zbl_bill2print.xml) and then press Finish. We have NewDataSet option including our just added zbl_bill2print in Available data source menu now.

So we select it and press button to move it to Selected tables menu. Done this, it’s time to click on Accept.

With all this stuff we’ve gotten that zbl_bill2print structure available in Fields explorer menu with all its fields, as shown in image below:

Step 3. Designing report.

Nothing special to say here. Just adding fields from Fields explorer menu, inserting text objects where needed, sums, text formats, images and so on…

I’ve just created a very simple design like this:

Step 4. Last actions to get the invoice.

Finally we’re going to create the bill. To do that, we add a CrystalReportViewer object in frmMain form. I call it crvBill. After that it’s necessary to modify frmMain source code to make it look like this:

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

Public Class frmMain

    '----------------------------------------------------------------
    ' 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
    End Sub

End Class

It’s important to note that the line where the XML file is created is commented now, as we just need to create this file once to use it to create the source data, but from now on we don’t need to generate it every time.

What we’re mainly doing in this code is:

1) creating a report object same kind we’ve designed in step 3,
2) getting a DataTable with data we want to show (in this example and according to the way we’ve defined MySQL view, we want to show invoice number 4),
3) setting this DataTable as the report’s source data,
4) asking CrystalReportViewer to show this report.

We execute the application again and get desired invoice:

Of course there should be quite more information, images and legal texts in a real invoice, but this is just an easy example of how to do the report itself.

So we’ve seen how to create a report in VisualBasic.NET just using an XML file. Of course there are plenty of things to improve, as optimizing how database connection is done, or avoiding WHERE condition directly in MySQL view and so on… but what I was looking for with this example was just a very minimum guide to show the process.

PS. Some menu and option names can be different as I develope in VisualStudio Spanish version and I’ve just translated them as I’ve thought they could appear in English version. Sorry about that!

Update.

There is a second part for this post explaining how to pass parameters from form to report, but it’s still only in Spanish.

Estadística de visitas (200811).

Este pasado mes de noviembre ha sido un mes de parón, tanto en número de visitantes como en posts publicados. He tenido mucho -demasiado- trabajo, he estado de mudanza, he andado liado con la facultad (aunque al final he tenido que renunciar a un par de asignaturas matriculadas porque no llegaba)… y en general he tenido poco tiempo para postear. Espero este mes de diciembre coger de nuevo el ritmo y publicar algunas de las cosas que tengo por ahí pendientes.

De todos modos, y en tanto que utilizo mi propio blog como archivo donde consultar cosas más adelante, hay un par de posts de Pedro Cambra sobre MySQL que descubrí durante este mes y que no quisiera perder de vista y aprovecho para recomendar:

1. Equivalente del ROWNUM de Oracle en MySQL

Como él mismo comenta, no es posible aplicar este método en vistas, pero aún así nos puede ser muy útil en determinados momentos.

2. Mysql - Cómo averiguar el último registro insertado en una tabla

Muy interesante, al menos a mí me lo ha parecido, el hecho de que sea un valor que depende no de usuario ni de base de datos, sino de sesión. Si dependiera de base de datos y fuera una base de datos con muchos usuarios conectados a la vez, sería arriesgadísimo su uso. Si dependiera de usuario y tuviéramos muchas instancias de un mismo programa que se conecta a la base de datos utilizando un único usuario, tampoco podríamos fiarnos de utilizar este sistema. No obstante, al depender de sesión de usuario, en muchas ocasiones es utilísimo.

Y ahora ya paso a las estadísticas del mes.

Noviembre 2008.

Número de visitas totales:       3521     (+  5,45%)
Páginas individuales vistas:     4348     (+  3,16%)
Promedio de tiempo en el sitio:  00:01:00 (-  9,09%)
Porcentaje de rebote:            85,88%   (+  0,76%)
Porcentaje de visitas nuevas:    86,74%   (+  4,32%)

El día con más visitas del mes ha sido el 26 de noviembre con un total de 167.

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

1. Informe en .NET con Crystal Reports y base de datos MySQL. (9,31%)
2. Pasando parámetros al informe en .NET con Crystal Reports. (6,60%)
3. Crear librería .dll en .NET… ¡y utilizarla! (4,65%)

De paso, aprovecho para mencionar la nueva política de comentarios que aplico, ya que desde hace unas semanas vengo siendo algo más estricto en lo que a su publicación se refiere. Me he dado cuenta que en poco tiempo han comenzado a llegarme demasiadas preguntas técnicas sobre aspectos unas veces relacionados con el post en cuestión y otras no. Y, aunque lo siento, no dispongo ni del tiempo ni del conocimiento para responder la mayoría de esas preguntas. Es por ello que a partir de ahora (y lo he aplicado con efectos retroactivos borrando muchos comentarios, lo siento) aplico estos criterios:

1. Si tu comentario es realmente eso -un comentario- y no resulta ofensivo, se publicará.

2. Si tu comentario es en realidad una pregunta, lo más probable es que no se publique. Debes pensar que aquí solo la leeré y podré contestar yo, mientras que si expones tus dudas en algún foro o grupo podrás recibir respuestas de mucha más gente y mucho más cualificada que yo.

3. Si pese a todo, quieres enviarme una pregunta, pueden pasar cuatro cosas:

i) Que sepa la respuesta y sea rapidita de contestar. En ese caso publicaré pregunta y respuesta para aumentar el valor del post.

ii) Que sepa la respuesta pero sea largo de contestar. Sintiéndolo mucho pasará al buzón de tareas pendientes hasta que tenga tiempo para ponerme con ello, pero la experiencia me dice que eso en realidad será nunca, con lo cual la pregunta se quedará sin contestar. En realidad como ya sé que lo que ocurriría sería eso, lo que realmente pasará será que directamente eliminaré el comentario.

iii) Que no sepa la respuesta (escenario más probable). En este caso no publicaré el comentario porque como he dicho antes, foros y grupos son sitios más adecuados para formular preguntas sobre cualquier duda técnica.

iv) Que no sepa la respuesta pero sea un tema que me intrigue/motive lo suficiente como para indagarlo. En este caso lo más probable es que tu comentario termine generando un nuevo post, cosa que te agradeceré.

4. Como es evidente, HOYGANs y similares son implacablemente eliminados.

Con estas sencillas normas, confío invertir menos tiempo de ahora en adelante con el tema de los comentarios y sentirme menos culpable cuando borro o dejo de dar respuesta a alguno. No perdamos de vista que los comentarios son justamente eso, comentarios. Pequeños añadidos a un post, reflexiones, opiniones, cosas así.

Por último, han sido bastantes los que me han solicitado que publique el control Matchcode que en su día mencioné. Lo tengo en mente y lo haré tan pronto como pueda. Ocurre que la versión que utilizo en mi aplicación está demasiado personalizada para ella y quisiera poder publicar una versión algo más genérica. Lo haré, sin duda, solo que un poquito más adelante.

MySQL no admite TRANSFORM PIVOT (pero se pueden obtener resultados parecidos).

Estos últimas días he estado mirando un tema que me preocupaba porque iba a necesitarlo en la aplicación que ando desarrollando… y tras no haber encontrado nada, finalmente me he tenido que poner a fondo con ello hasta sacarlo de una u otra manera. El tema consiste en hacer una consulta de referencias cruzadas en MySQL y, como digo, tras bastante investigar he descubierto que en realidad MySQL no admite esta clase de consultas. Para explicar mejor lo que pretendía hacer voy a basarme en un ejemplo consistente en la siguiente tabla sfc_salefrcast:

+---------+---------+------------+---------+
| sfc_cus | sfc_mat | sfc_mth    | sfc_qty |
+---------+---------+------------+---------+
| 12345   |       1 | 2008-11-01 |    1200 |
| 12345   |       3 | 2008-11-01 |    1500 |
| 54321   |       2 | 2008-11-01 |    2500 |
| 54321   |       3 | 2008-12-01 |    3500 |
| 54321   |       3 | 2009-03-01 |    4500 |
| 54321   |       3 | 2009-07-01 |    4500 |
| 99999   |       4 | 2009-02-01 |    2000 |
| 99999   |       4 | 2009-04-01 |    4000 |
| 99999   |       4 | 2009-06-01 |    6000 |
+---------+---------+------------+---------+

Es una tabla de previsiones de ventas en donde sfc_cus es el código del cliente, sfc_mat el código del material, sfc_mth el mes representado por su primer día y sfc_qty la cantidad que prevemos que vamos a venderle de dicho material a dicho cliente dicho mes (y dichosos nosotros si acertamos).

Bien, pues a partir de esta tabla pretendía conseguir una tabla (o vista… un conjunto de registros, vamos) en la que en cada registro se me mostrara una combinación de cliente y material, y a partir de aquí una columna para cada mes con la cantidad correspondiente. Algo del estilo de lo siguiente…

+---------+---------+------+------+------+------+------+------+
| sfc_cus | sfc_mat | M0   | M1   | M2   | M3   | M4   | M5   |
+---------+---------+------+------+------+------+------+------+
| 12345   |       1 | 1200 |    0 |    0 |    0 |    0 |    0 |
| 12345   |       3 | 1500 |    0 |    0 |    0 |    0 |    0 |
| 54321   |       2 | 2500 |    0 |    0 |    0 |    0 |    0 |
| 54321   |       3 |    0 | 3500 |    0 |    0 | 4500 |    0 |
| 99999   |       4 |    0 |    0 |    0 | 2000 |    0 | 4000 |
+---------+---------+------+------+------+------+------+------+

…en donde M0 es el mes actual (2008-11-01), M1 es el mes próximo, M2 el siguiente, etc.

Pues bien, si estuviéramos trabajando en Microsoft Access podríamos hacer algo simple como…

TRANSFORM SUM(sfc_qty) 
    SELECT sfc_cus, sfc_mat
    FROM sfc_salefrcast
    GROUP BY sfc_cus, sfc_mat
PIVOT sfc_mth;

…y obtendríamos algo muy parecido al resultado deseado. Simplemente no nos mostraría las columnas donde no haya ningún valor que mostrar (como M2), pero sería un problema muy menor que solventaríamos sin dificultad. En Microsoft Excel también sería sencillísimo obtener esos resultados a través de una tabla dinámica.

No obstante, trabajo con MySQL. Y por desgracia MySQL no admite TRANSFORM-PIVOT. De hecho no he encontrado ninguna alternativa atractiva para resolver mi problema, de modo que he tenido que rascar un poquito y crear una a medida, que me ha quedado así:

SELECT
    sfc_cus,
    sfc_mat,
    SUM(IF(MID(sfc_mth, 1, 7) = MID(NOW(), 1, 7), sfc_qty, 0)) AS M0,
    SUM(IF(MID(sfc_mth, 1, 7) = 
           MID(DATE_ADD(NOW(),INTERVAL 1 MONTH), 1, 7), 
           sfc_qty, 0))                                        AS M1,
    SUM(IF(MID(sfc_mth, 1, 7) = 
           MID(DATE_ADD(NOW(),INTERVAL 2 MONTH), 1, 7), 
           sfc_qty, 0))                                        AS M2,
    SUM(IF(MID(sfc_mth, 1, 7) = 
           MID(DATE_ADD(NOW(),INTERVAL 3 MONTH), 1, 7), 
           sfc_qty, 0))                                        AS M3,
    SUM(IF(MID(sfc_mth, 1, 7) = 
           MID(DATE_ADD(NOW(),INTERVAL 4 MONTH), 1, 7), 
           sfc_qty, 0))                                        AS M4,
    SUM(IF(MID(sfc_mth, 1, 7) = 
           MID(DATE_ADD(NOW(),INTERVAL 5 MONTH), 1, 7), 
           sfc_qty, 0))                                        AS M5
FROM
    sfc_salefrcast
GROUP BY 
    sfc_cus, sfc_mat;

(Ver actualización 1.)

Al no utilizar directamente fechas sino estar jugando con NOW() y la función DATE_ADD, obtenemos siempre resultados para el mes corriente y los siguientes N meses, independientemente de cuando solicitemos esa información a la base de datos.

Ignoro si hay alguna manera más directa de hacerlo (y si la hay y la conoces, estaré encantadísimo de leerla), pero por lo pronto lo he solucionado así…

Actualización.

1. Siguiendo el consejo -uno de ellos- de Luís Medel en los comentarios, he cambiado la versión original que había publicado para dejar esta más simplificada al realizar en un solo paso el sumatorio y el filtrado. Además, Luís propone utilizar CASE WHEN THEN ELSE END en lugar de IF.

2. Pedro Cambra facilita en los comentarios un link a la página de Roland Bouman donde se expone un método para hacerlo de forma dinámica utilizando procedimientos almacenados.

3. Pedro Cambra comenta también que MySQL dispone de la función EXTRACT con el parámetro YEAR_MONTH que efectivamente se podría utilizar para mejorar el apartado del MID(fecha,1,7). Más información en el manual de referencia de MySQL.

Correspondencia entre tipos de datos en MySQL, en VisualBasic.NET y en .NET Framework.

Estos últimos días he estado liado programando una clase que tira intensamente de reflexión (me refiero a System.Reflection, no a que haya estado reflexionando intensamente, que también) para relacionar un objeto de una clase determinada (la que sea) con una tabla en la base de datos. Así, utilizando esa clase auxiliar podemos decirle que cargue un objeto a partir de un registro de una tabla o que lo guarde en ella. Y sirve para cualquier clase que tenga una tabla relacionada en la base de datos. Quizá otro día cuelgo esa clase… Pero el caso es que mientras lo desarrollaba he tenido algunos problemillas por no encajarme exactamente los tipos de datos que me devolvía MySQL y los que esperaba .NET, así que tras haber estado buscando cuáles son las equivalencias exactas, paso a exponer la tabla de correspondencias entre tipos de datos tanto en MySQL como en VisualBasic.NET como en el .NET Framework.

+--------------------+----------+----------------+
|       MYSQL        |  VB.NET  | .NET Framework |
+--------------------+----------+----------------+
| TINYINT            | SByte    | SByte          |
| TINYINT UNSIGNED   | Byte     | Byte           |
| SMALLINT           | Short    | Int16          |
| SMALLINT UNSIGNED  | UShort   | UInt16         |
| MEDIUMINT          | Integer  | Int32          |
| MEDIUMINT UNSIGNED | UInteger | UInt32         |
| INT                | Integer  | Int32          |
| INT UNSIGNED       | UInteger | UInt32         |
| BIGINT             | Long     | Int64          |
| BIGINT UNSIGNED    | ULong    | UInt64         |
+--------------------+----------+----------------+

Teniendo en cuenta estas relaciones, todo funciona a las mil maravillas… :-)

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.

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

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:

'----