sábado, 11 de abril de 2015

Joins




Los sistemas de bases de datos relacionales tradicionales, tienen la habilidad de poder fundir múltiples conjuntos de datos (normalmente tablas) dentro del resultado de una consulta. Para realizar estas uniones, se tienen en cuenta los datos en común entre unos y otros conjuntos, realizándose generalmente entre claves primarias y claves ajenas (PK y FK).

 Si nos ceñimos al caso de LinQ y al trabajo con datos en memoria mediante la programación orientada a objetos (OOP), el uso de joins en sí, es mucho menos frecuente, pero para quien se sienta cómodo con este tipo de acciones, Microsoft nos proporciona el operador Join, dentro de su conjunto de métodos extensores.


También abordaremos el caso del operador GroupJoin, que nos ofrece la capacidad de relacionar 2 conjuntos de datos, con una tipología mucho más cercana al programador, creando propiedades de tipo colección dentro de las propiedades de la secuencia de resultado, eliminando de esta manera los campos con datos redundantes.








Recuerda que aquí tienes el indice de todos los posts del Curso de LinQ.



Join


El uso de este operador de LinQ, es básicamente igual al de su homónimo en SQL. Este operador tiene una sintaxis mucho más clara si realizamos su escritura mediante expresión de consulta, ya que en su definición de Lambda, se hace un poco más complicado de entender y de visualizar. Nosotros explicaremos las dos formas:



























Señalar el operador select para crear la secuencia de resultado, que en la expresión de consulta se ve claramente y la expresión lambda está oculto dentro de Colec_Resultado.

Vamos a dejar un poco de lado el pseudocódigo para meternos con un ejemplo más real. Para ello utilizaremos dos clases, Autor y Publicacion:

Antes de nada me gustaría pedir disculpas si los ejemplos se ven un poco largos, ya que en cada uno de ellos repito por ejemplo la instanciación de las listas, pero el fin es que cualquier persona que lo lea pueda fácilmente hacer un copy/paste y lanzar el ejemplo en su Visual Studio sin tener que ponerse a pensar que parte de código tiene que añadir de cada ejemplo.


public class Autor
{
    public int      ID_Autor        { get; set; }
    public string   Nombre          { get; set; }
    public DateTime FechaNacimento  { get; set; }
    public string   LugarNacimiento { get; set; }
}
public class Publicacion
{
    public int       ID_Publi         { get; set; }
    public string    Titulo           { get; set; }
    public int       ID_Autor         { get; set; }
    public DateTime  Fecha            { get; set; }
    public string    LugarPublicacion { get; set; }
}


Estas clases nos denotan, que podría realizarse un enlace entre ellas por el campo ID_Autor, y que un autor puede tener varias publicaciones realizadas.


Vamos con el ejemplo:


static void Main(string[] args)
    {
        var autores = new List<Autor>()
        {
            new Autor { 
                        ID_Autor        = 1, 
                        Nombre          = "Federico García Lorca", 
                        FechaNacimento  = new DateTime(1898, 6, 5), 
                        LugarNacimiento = "España" 
                        },
            new Autor { 
                        ID_Autor        = 2, 
                        Nombre          = "William Blake", 
                        FechaNacimento  = new DateTime(1757,12, 8), 
                        LugarNacimiento = "Inglaterra" 
                        },
            new Autor { 
                        ID_Autor        = 3, 
                        Nombre          = "Antonio Machado Ruiz", 
                        FechaNacimento  = new DateTime(1875, 7, 26), 
                        LugarNacimiento = "España" 
                        }
        };
 
 
        var publicaciones = new List<Publicacion>()
        {
            new Publicacion
            {
                ID_Publi         = 1,
                Titulo           = "Songs of Innocence",
                Fecha            = new DateTime(1789, 1, 1),
                ID_Autor         = 2,
                LugarPublicacion = "Inglaterra"
            },
            new Publicacion
            {
                ID_Publi         = 2,
                Titulo           = "Soledades: poesías",
                Fecha            = new DateTime(1903, 1, 1),
                ID_Autor         = 3,
                LugarPublicacion = "España"
            },
            new Publicacion
            {
                ID_Publi         = 3,
                Titulo           = "El Romancero Gitano",
                Fecha            = new DateTime(1918, 1, 1),
                ID_Autor         = 1,
                LugarPublicacion = "España"
            },
            new Publicacion
            {
                ID_Publi         = 4,
                Titulo           = "Poeta en Nueva York",
                Fecha            = new DateTime(1930, 1, 1),
                ID_Autor         = 1,
                LugarPublicacion = "España"
            },
            new Publicacion
            {
                ID_Publi         = 5,
                Titulo           = "Páginas escogidas",
                Fecha            = new DateTime(1917, 1, 1),
                ID_Autor         = 3,
                LugarPublicacion = "España"
            }
        };
 
 
        var joinConsulta = from a in autores
                            join p in publicaciones
                            on a.ID_Autor equals p.ID_Autor
                            select new
                                {
                                    Autor            = a.Nombre,
                                    Titulo           = p.Titulo,
                                    FechaPublicacion = p.Fecha
                                };
 
        var joinLambda = autores.Join                       // Colección 1
            (
                publicaciones,                              // Colección 2
                a => a.ID_Autor,                            // Clave Colec 1
                p => p.ID_Autor,                            // Clave Colec 2
                (a, p) => new {                             // Colección de resultado
                                    Autor            = a.Nombre,
                                    Titulo           = p.Titulo,
                                    FechaPublicacion = p.Fecha
                                }
            );
 
        Console.WriteLine("Expresión de Consulta:");
 
        foreach (var j in joinConsulta) 
        {
            Console.WriteLine("{0,-21} - {1,-19} - {2:d}", j.Autor, j.Titulo, j.FechaPublicacion);
        }
 
        Console.WriteLine();
        Console.WriteLine("Expresión Lambda:");
 
        foreach (var j in joinLambda) 
        {
            Console.WriteLine("{0,-21} - {1,-19} - {2:d}", j.Autor, j.Titulo, j.FechaPublicacion);
        }
 
        Console.Read();
    }


Generamos 2 colecciones de cada uno de nuestros tipos y realizamos ambos joins para cada una de las tendencias, con el siguiente resultado:


















Join por campos compuestos


Estamos en la misma coyuntura que con la cláusula GroupBy cuando agrupábamos por más de un campo, y haremos exactamente lo mismo, y es utilizar un tipo anónimo formado por los mismos casos y propiedades del mismo tipo para cada una de las colecciones. En nuestro ejemplo este sería el caso:


var joinConsulta = from a in autores
                    join p in publicaciones
                    on new { Id = a.ID_Autor, Lugar = a.LugarNacimiento } equals new { Id = p.ID_Autor, Lugar = p.LugarPublicacion }
                    select new
                        {
                            Autor     = a.Nombre,
                            Titulo     = p.Titulo,
                            FechaPublicacion = p.Fecha
                        };
 
var joinLambda = autores.Join                                       // Colección 1
    (
        publicaciones,                                              // Colección 2
        a => new { Id = a.ID_Autor, Lugar = a.LugarNacimiento  },   // Clave Colec 1
        p => new { Id = p.ID_Autor, Lugar = p.LugarPublicacion },   // Clave Colec 2
        (a, p) => new {                                             // Colección de resultado
                            Autor            = a.Nombre,
                            Titulo           = p.Titulo,
                            FechaPublicacion = p.Fecha
                        }
    );


Nota: Es importante comentar que es imprescindible el alias en los tipos anónimos, ya que si no, nos dará un error de compilación.

El resultado, exactamente el mismo. 

Si queremos observar la diferencia del enlace, podríamos jugar con los lugares de entrada. Simplemente con cambiar la letra mayúscula por minúscula del país, podremos comprobar que los datos no se cruzan y no aparecen como resultado.




Join con IEqualityComparer

Al igual que en el anterior post, podemos encontrarnos con una situación en que tenemos que utilizar propiedades de cruce entre colecciones (pseudos PK y FK) que no son de tipo simple (string o estructuras), sino que pertenecen a tipos definidos por nosotros o por cualquier otro desarrollador o compañía, en definitiva tipos por referencia (class).

Supongamos que tenemos las siguientes clases:


public class PersonaReducida
{
    public string    ID              { get; set; }
    public DateTime  FechaNacimiento { get; set; }
    public Direccion Domicilio       { get; set; }  
}
 
public class Alquiler
{
    public int       IdAlquiler        { get; set; }
    public DateTime  FechaAlquiler     { get; set; }
    public decimal   Precio            { get; set; }
    public Direccion DireccionInmueble { get; set; }
}

 
public class Direccion
{
    public string Calle     { get; set; }
    public int    Numero    { get; set; }
    public string Provincia { get; set; }
}



Si por nuestras reglas de negocio, necesitáramos cruzar nuestras colecciones de PersonaReducida y Alquiler por el campo Domicilio y DirecciónInmueble de tipo Direccion, al ser este un tipo definido por nosotros, una clase, tendríamos que indicarle cual es la manera en la que queremos nosotros que se diferencien dos objetos de tipos Direccion para nuestra ocurrencia, podríamos crear varias clases IEqualityComparer para distintos sucesos. Utilizaremos la misma que para el ejemplo GroupBy.


public class DireccionEqualityComparer : IEqualityComparer<Direccion>
{
 
    #region IEqualityComparer Members
 
    public bool Equals(Direccion x, Direccion y)
    {
        return  x.Calle     == y.Calle  && 
                x.Numero    == y.Numero && 
                x.Provincia == y.Provincia;
    }
 
    public int GetHashCode(Direccion obj)
    {
        return  obj.Calle    .GetHashCode() + 
                objNumero   .GetHashCode() + 
                obj.Provincia.GetHashCode();
    }
 
    #endregion
}

Añadimos la condición del enlace y el uso de la cláusula join con el IEqualityComparer<Direccion>:


static void Main(string[] args)
{
    List<PersonaReducida> personas = new List<PersonaReducida>();
    //personas.Add(...)
    //personas.Add(...)
    List<Alquiler> alquileres = new List<Alquiler>();
    //alquileres.Add(...)
    //alquileres.Add(...)
 
    var datos = personas.Join
        (
            alquileres,
            p => p.Domicilio,
            a => a.DireccionInmueble,
            (p, a) => new { PersonaID = p.ID,  FechaAlquiler = a.FechaAlquiler, PrecioAlquiler = a.Precio },
            new DireccionEqualityComparer()
        );
 
    Console.Read();
}


Dentro del código nos encontramos el cruce tanto en la colección principal con p => p.Domicilio, como en la secundaria a => a.DireccionInmueble, y para quitarle las dudas al compilador, de cómo debe diferenciar estos tipos, para la tesitura que nosotros abordamos, añadimos una nueva instancia de DireccionEqualityComparer new DirecciónEqualityComparer() como último parámetro del método Join.


Recordar que en caso de no facilitarle esta clase derivada de IEqualityComparer el compilador tomaría el comportamiento por defecto de comparación, que en el caso de las clases es la referencia a la que apunta.



Join mediante Where

Hay veces, cuando trabajamos con sentencias SQL y con Joins, muchos, entre los que me incluyo, realizamos la unión de tablas mediante la conjunción de un producto cartesiano y una clausula where, como muestro en el ejemplo:


Select Camposssss ....
From Tabla1, Tabla2
where Tabla1.Campo1 = Tabla2.Campo1
...


Esto, no sé por qué, pero se te mete en la retina y te hace inaceptable el utilizar Inner Joins, en definitiva un importante error a la hora de aprovechar el rendimiento, que se agrava en LinQ.

Como vemos en el ejemplo, tomado de los anteriores, un simple conjunto de 2 colecciones de 3 y 5 elementos respectivamente, tiene una pérdida de rendimiento, tan espectacular como que el tiempo de ejecución de la claúsula Join es 8 veces inferior al del uso del where.

static void Main(string[] args)
{
    var autores = new List<Autor>()
    {
        new Autor { 
                    ID_Autor        = 1, 
                    Nombre          = "Federico García Lorca", 
                    FechaNacimento  = new DateTime(1898, 6, 5), 
                    LugarNacimiento = "España" 
                    },
        new Autor { 
                    ID_Autor        = 2, 
                    Nombre          = "William Blake", 
                    FechaNacimento  = new DateTime(1757,12, 8), 
                    LugarNacimiento = "Inglaterra" 
                    },
        new Autor { 
                    ID_Autor        = 3, 
                    Nombre          = "Antonio Machado Ruiz", 
                    FechaNacimento  = new DateTime(1875, 7, 26), 
                    LugarNacimiento = "españa" 
                    }
    };
 
 
    var publicaciones = new List<Publicacion>()
    {
        new Publicacion
        {
            ID_Publi         = 1,
            Titulo           = "Songs of Innocence",
            Fecha            = new DateTime(1789, 1, 1),
            ID_Autor         = 2,
            LugarPublicacion = "Inglaterra"
        },
        new Publicacion
        {
            ID_Publi         = 2,
            Titulo           = "Soledades: poesías",
            Fecha            = new DateTime(1903, 1, 1),
            ID_Autor         = 3,
            LugarPublicacion = "España"
        },
        new Publicacion
        {
            ID_Publi         = 3,
            Titulo           = "El Romancero Gitano",
            Fecha            = new DateTime(1918, 1, 1),
            ID_Autor         = 1,
            LugarPublicacion = "España"
        },
        new Publicacion
        {
            ID_Publi         = 4,
            Titulo           = "Poeta en Nueva York",
            Fecha            = new DateTime(1930, 1, 1),
            ID_Autor         = 1,
            LugarPublicacion = "España"
        },
        new Publicacion
        {
            ID_Publi         = 5,
            Titulo           = "Páginas escogidas",
            Fecha            = new DateTime(1917, 1, 1),
            ID_Autor         = 3,
            LugarPublicacion = "España"
        }
    };
 
    System.DiagnosticsStopwatch reloj = new System.DiagnosticsStopwatch();
    reloj.Start();
 
    var joinConsulta = from a in autores
                        join p in publicaciones
                        on new { Id = a.ID_Autor, Lugar = a.LugarNacimiento } equals new { Id = p.ID_Autor, Lugar = p.LugarPublicacion }
                        select new
                            {
                                Autor = a.Nombre,
                                Titulo = p.Titulo,
                                FechaPublicacion = p.Fecha
                            };
 
    Console.WriteLine("El join normal ha tardado  {0}", reloj.ElapsedTicks);
 
    reloj.Restart();
 
    var joinWhere = from a in autores
                    from p in publicaciones
                    where a.ID_Autor == p.ID_Autor && a.LugarNacimiento == p.LugarPublicacion
                    select new
                    {
                        Autor            = a.Nombre,
                        Titulo           = p.Titulo,
                        FechaPublicacion = p.Fecha
                    };
 
 
    Console.WriteLine("El join where  ha tardado {0}", reloj.ElapsedTicks);
 
    reloj.Stop();

    Console.Read();
}


Resultado:






















Así que ya sabéis, cuando queráis unir colecciones join, nada de where.



GroupJoin

Esta es la solución más inteligente y más utilizada a la hora de unir 2 conjuntos de datos, si nos ponemos las gafas de desarrollador y dejamos de lado esas otras de aprendiz de DBA.

Con el operador GroupJoin, crearemos jerarquías de datos, como hacíamos con GroupBy, pero mediante la relación de dos colecciones.

Vamos con nuestro ejemplo, pero con este enfoque de GroupJoin:


static void Main(string[] args)
{
    var autores = new List<Autor>()
    {
        new Autor { 
                    ID_Autor        = 1, 
                    Nombre          = "Federico García Lorca", 
                    FechaNacimento  = new DateTime(1898, 6, 5), 
                    LugarNacimiento = "España" 
                    },
        new Autor { 
                    ID_Autor        = 2, 
                    Nombre          = "William Blake", 
                    FechaNacimento  = new DateTime(1757,12, 8), 
                    LugarNacimiento = "Inglaterra" 
                    },
        new Autor { 
                    ID_Autor        = 3, 
                    Nombre          = "Antonio Machado Ruiz", 
                    FechaNacimento  = new DateTime(1875, 7, 26), 
                    LugarNacimiento = "españa" 
                    }
    };
 
 
    var publicaciones = new List<Publicacion>()
    {
        new Publicacion
        {
            ID_Publi         = 1,
            Titulo           = "Songs of Innocence",
            Fecha            = new DateTime(1789, 1, 1),
            ID_Autor         = 2,
            LugarPublicacion = "Inglaterra"
        },
        new Publicacion
        {
            ID_Publi         = 2,
            Titulo           = "Soledades: poesías",
            Fecha            = new DateTime(1903, 1, 1),
            ID_Autor         = 3,
            LugarPublicacion = "España"
        },
        new Publicacion
        {
            ID_Publi         = 3,
            Titulo           = "El Romancero Gitano",
            Fecha            = new DateTime(1918, 1, 1),
            ID_Autor         = 1,
            LugarPublicacion = "España"
        },
        new Publicacion
        {
            ID_Publi         = 4,
            Titulo           = "Poeta en Nueva York",
            Fecha            = new DateTime(1930, 1, 1),
            ID_Autor         = 1,
            LugarPublicacion = "España"
        },
        new Publicacion
        {
            ID_Publi         = 5,
            Titulo           = "Páginas escogidas",
            Fecha            = new DateTime(1917, 1, 1),
            ID_Autor         = 3,
            LugarPublicacion = "España"
        }
    };
 
    var groupJoinConsulta = from a in autores
                            join p in publicaciones
                            on a.ID_Autor equals p.ID_Autor into agrupados
                            select new
                            {
                                Autor = a,
                                Publicaciones = agrupados
                            };
 
 
 
 
 
    var groupJoinLambda = autores.GroupJoin                         // Colección 1
        (
            publicaciones,                                          // Colección 2
            a => a.ID_Autor,                                        // Clave Colec 1
            p => p.ID_Autor,                                        // Clave Colec 2
            (padre, hijos) => new {                                 // Colección de resultado
                                        Autor         = padre, 
                                        Publicaciones = hijos 
                                    }
        );
 
 
    Console.WriteLine("Expresión de Consulta:");
 
    foreach (var grupo in groupJoinConsulta)
    {
        Console.WriteLine("***  {0}  ***", grupo.Autor);
 
        foreach (var publicacion in grupo.Publicaciones)
        {
            Console.WriteLine("    {0,-21} - {1,-19} - {2:d}", publicacion.Titulo, publicacion.LugarPublicacion, publicacion.Fecha);
        }
    }
 
    Console.WriteLine();
    Console.WriteLine("Expresión Lambda:");
 
    foreach (var grupo in groupJoinLambda)
    {
        Console.WriteLine("***  {0}  ***", grupo.Autor);
 
        foreach (var publicacion in grupo.Publicaciones)
        {
            Console.WriteLine("    {0,-21} - {1,-19} - {2:d}", publicacion.Titulo, publicacion.LugarPublicacion, publicacion.Fecha);
        }
    }
    Console.Read();
}

Apuntar que en la expresión de consulta, simplemente se añade la claúsula into para generar una variable con los registros de cada grupo.

Resultado:






















La utilización es muy similar pero el resultado es mucho más práctico desde el punto de vista del desarrollador, ya que tenemos una colección de objetos, q a su vez cada uno de ellos contiene otra colección con los objetos relacionados.

Así es como realiza Entity Framework sus relaciones entre entidades (DBSet, ObjectSet).


Left Join, Right Join, Full Outer Join … etc

Si nos ponemos de nuevo las gafas para volver a ver con el prisma de becario de DBA, hay algunos operadores de unión que nos faltan, y son todos esos que pongo en el título y algunos más. Para dar respuesta a todo eso, dejo un enlace a un artículo que escribí hace unos años para CodeProject, en el que explico todo esto, y muestro un ejemplo de cada uno de ellos, construyendo una librería de extensión para poder utilizarlos.



Lo más importante de todo es que la moraleja del viene a decir que lo importante para el desarrollador es usar el GroupJoin.