LINQ to SQL (II): Relaciones


Ahora que ya sabemos cómo mapear una base de datos relacional en un conjunto de objetos, es momento de profundizar en algunos conceptos y procesos relacionados con esta transformación. El primero de ellos tiene que ver con la forma de tratar las relaciones entre las tablas.

Generalmente, una relación entre dos tablas se establece a través de un campo de la clase que referencia, al que denominamos clave foránea, que “apunta” (toma el valor) de la clave primaria (que identifica cada registro de forma única) de otra tabla.

Así, en el siguiente esquema, el campo IdCliente identifica de forma única a cada cliente, mientras que el campo IdPedido hace lo propio con cada pedido. Sin embargo, la tabla Pedido posee también un campo IdCliente, cuyo valor coincidirá con el campo IdCliente de la tabla Cliente perteneciente al registro que quiere referenciar.

Es decir. Si tenemos los siguientes registros:

Vemos que el campo IdCliente de la tabla Pedido “referencia” al campo IdCliente de la tabla Cliente indicando cuál es el cliente que ha realizado el pedido. A través de operaciones como joins, es posible, a partir del Id del cliente almacenado en el pedido, recuperar los datos del cliente, como su nombre o fecha de nacimiento.

El modelo objetual, sin embargo, no se basa en el mismo principio. A ojos de un programador, lo que tenemos es un cliente que posee n pedidos asociados, por lo que desde el punto de vista de la programación orientada a objetos, lo que deberíamos tener es una clase Cliente que contenga, además de los campos propios de la clase (IdCliente, Nombre, FechaNacimiento), un listado de pedidos asociados.

Si echamos un vistazo al código que generamos en el artículo anterior mediante SqlMetal, vemos que la herramienta ya lo ha realizado por nosotros:


	[global::System.Data.Linq.Mapping.TableAttribute(Name="dbo.Cliente")]
	public partial class Cliente : INotifyPropertyChanging, INotifyPropertyChanged
	{
		
		private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty);
		
		private int _IdCliente;
		
		private string _Nombre;
		
		private System.DateTime _FechaNacimiento;
		
		private EntitySet<Pedido> _Pedido;
              // ...
		[global::System.Data.Linq.Mapping.AssociationAttribute(Name="Cliente_Pedido", Storage="_Pedido", ThisKey="IdCliente", OtherKey="IdCliente")]
		public EntitySet<Pedido> Pedido
		{
			get
			{
				return this._Pedido;
			}
			set
			{
				this._Pedido.Assign(value);
			}
		}
	}


El atributo _Pedido, de tipo EntitySet<Pedido> se expone mediante una propiedad pública que se encuentra adornada con el atributo AssociationAttribute, encargado de realizar el mapeo entre el mundo relacional y objetual. El parámetro ThisKey simbolizará, por lo tanto, la clave primaria de Cliente, mientras que el parámetro OtherKey indicará el nombre del campo que se corresponde con la clave foránea en la otra tabla.

Este tipo de modelado hace que pensemos en objetos, de forma que no sea necesario pensar en realizar operaciones como joins o similares. Bastará realizar un drill-down para acceder a los datos que solicitemos. Además, si nos fijamos en la entidad Pedido, veremos que también poseerá un listado de los clientes asociados a los pedidos, por lo que será posible acceder a los clientes desde los pedidos y a los pedidos desde los clientes.

La diferencia se encuentra en el tipo de objeto: mientras que el cliente tiene un EntitySet de pedidos, el pedido tiene un EntityRef de clientes, debido al tipo de relación (1:n). El lado “1” poseerá un Set, mientras que el lado “n” poseerá un Ref. Este tipo de objetos los veremos en otra ocasión.


	[global::System.Data.Linq.Mapping.TableAttribute(Name="dbo.Pedido")]
	public partial class Pedido : INotifyPropertyChanging, INotifyPropertyChanged
	{
		
		private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty);
		
		private int _IdPedido;
		
		private int _IdCliente;
		
		private System.DateTime _FechaPedido;
		
		private EntitySet<LineaPedido> _LineaPedido;
		
		private EntityRef<Cliente> _Cliente;

Así, una consulta SQL o LINQ to Objects que quiera recuperar aquellos pedidos de Ana Lopez Diaz realizados después del 20/11/2013, sería así:


            var consulta = from pedido in dbContext.Pedido
                           join cliente in dbContext.Cliente
                             on pedido.IdCliente equals cliente.IdCliente
                           where ((pedido.FechaPedido >= new DateTime(2013, 11, 20)) &&
                                 (cliente.Nombre.Equals("Ana Lopez Diaz")))
                           select pedido;

            foreach (var pedido in consulta)
            {
                Console.WriteLine(string.Format("PEDIDO: {0}\tCLIENTE: {1}\tFECHA: {2}",
                    pedido.IdPedido, pedido.IdCliente, pedido.FechaPedido));
            }
            Console.WriteLine("--------------------------------------------------\n");

La consulta equivalente en LINQ to SQL prescinde completamente de las relaciones entre las tablas, ya que son objetos. Referenciando adecuadamente los objetos dentro de un listado puede accederse a los elementos relacionados de forma natural, sin necesidad de conocer nada acerca de claves foráneas, claves primarias ni relaciones entre entidades: el propio Intellisense de Visual Studio nos proporcionará esa información de forma automática:


            var consultaObj = from pedido in dbContext.Pedido
                              where ((pedido.FechaPedido >= new DateTime(2013, 11, 20)) &&
                                    (pedido.Cliente.Nombre.Equals("Ana Lopez Diaz")))
                              select pedido;

            foreach (var pedido in consulta)
            {
                Console.WriteLine(string.Format("PEDIDO: {0}\tCLIENTE: {1}\tFECHA: {2}",
                    pedido.IdPedido, pedido.IdCliente, pedido.FechaPedido));
            }

 

El resultado es, a todas luces, el mismo:

DataLoadOptions

Este comportamiento, sin embargo, puede hacer que nos formulemos una pregunta: entonces, ¿cada vez que estoy recuperando un cliente estoy recperando por extensión todos sus pedidos? La respuesta es no. Como vimos en artículos previos, LINQ hace uso de la ejecución diferida, por lo que los pedidos asociados al cliente sólo se recuperarán en el momento en el que éstos se vayan a utilizar. Sin embargo, esto puede plantear un problema en ciertas ocasiones. Imaginemos que queremos mostrar los datos de un cliente y, a continuación, el de todos sus pedidos. Algo como lo siguiente:


            var consulta = from cliente in dbContext.Cliente
                           select cliente;

            foreach (var cliente in consulta)
            {
                Console.WriteLine(string.Format("CLIENTE: {0}", cliente.Nombre));
                foreach (var pedido in cliente.Pedido)
                {
                    Console.WriteLine(string.Format("PEDIDO: {0} - {1}", pedido.IdCliente, pedido.FechaPedido));
                }
            }

Este bucle haría lo siguiente:

  • Recuperar los elementos de la tabla Cliente
  • Por cada cliente
    • Recuperar los pedidos asociados al cliente actual

Por lo tanto, ante una estructura de este tipo, el número de consultas lanzada a la base de datos será (número de clientes + 1), ya que se ejecutará una SELECT para recuperar los clientes más otra SELECT por cada uno de los clientes (para recuperar los pedidos asociados a ese cliente).

En términos de rendimiento y dependiendo de la estructura de la base de datos, puede que este modelo no sea óptimo del todo, ya que estamos realizando un gran número de consultas que recuperan muy poca información de cada vez. ¿No sería mejor recuperar en una sola consulta la información relacionada con los clientes y todos sus pedidos? Seguramente, pero ¿cómo hacemos esto?

A la hora de utilizar un DataContext por primera vez, es posible asignarle un método de carga específico, en el que le indicaremos este tipo de preferencias. Así, si quisiéramos que cada vez que se cargue un objeto Cliente se rellene también automáticamente su listado de Pedido, haríamos lo siguiente:


            //Obtenemos la cadena de conexión de App.config o Web.config
            string cadenaConexion = ConfigurationManager.ConnectionStrings["TestDb"].ConnectionString;

            // Instanciamos el DataContext a partir del fichero dbml
            Testdb dbContext = new Testdb(cadenaConexion);

            DataLoadOptions opcionesDeCarga = new DataLoadOptions();
            opcionesDeCarga.LoadWith<Cliente>(cliente => cliente.Pedido);
            dbContext.LoadOptions = opcionesDeCarga;

Básicamente, lo que estamos diciéndole a nuestro DataContext es que cargue con cada cliente (LoadWith<Cliente>) los elementos del listado Pedido (cliente.Pedido). Esto redundará en que se realizará una única consulta con mayor carga de datos. Como podemos imaginar, este tipo de comportamiento deberá valorarse dependiendo de cada caso.

Para mejorar el rendimiento en casos de cargas muy costosas, DataLoadOptions nos permite una opción adicional: filtrar qué registros del listado queremos recuperar con el objeto. Aplicado a nuestro ejemplo: es posible decirle a LINQ to SQL que cuando recupere un cliente, recupere también sus pedidos, pero sólo aquellos que cumplen una determinada condición. El resto de registros, si son necesarios posteriormente, se recuperarán de forma lazy tal y como se recuperarían normalmente si no se especificara ninguna configuración específica de carga. Por lo tanto, lo que hará esta opción es seleccionar los registros que se van a cargar en memoria durante la carga del objeto padre.

El método AssociateWith tiene una sintaxis similar a LoadWith, y se usa en conjunción con ésta. Primero se indica qué se quiere cargar junto al objeto principal (LoadWith) y luego, opcionalmente, se indica una condición de carga eager (inmediata). Así, si quisiésemos que la consulta que recupera los clientes cargue también los pedidos realizados en los últimos cinco días, haríamos algo como lo siguiente:


            DataLoadOptions opcionesDeCarga = new DataLoadOptions();
            opcionesDeCarga.LoadWith<Cliente>(cliente => cliente.Pedido);
            opcionesDeCarga.AssociateWith<Cliente>(cliente => cliente.Pedido
                                                            .Where(pedido => (pedido.FechaPedido >= DateTime.Now.AddDays(-5))));
            dbContext.LoadOptions = opcionesDeCarga;

Es importantísimo no confundir esta operación, realizada mediante el método AssociateWith, con una sentencia Where.

  • AssociateWith provoca un “adelanto” en la obtención de los datos que coinciden con los filtros. Los que no coinciden serán recuperados en caso de que sean necesarios.
  • Where ignora los datos que no cumplan con el filtro. Estos datos nunca llegarán a recuperarse.
Anuncios

One comment

  1. Excelente explicación, estuve horas sumergido en la documentación y viendo tutoriales y ejemplos sobre joins debido a que al pasar mi consutla a la vista con el ViewBag e intentar acceder a las propiedades me devolvía el error “object’ no contiene una definición para ‘ID’.
    Luego de leer tu explicación me entendí como funciona LinQ realmente y que puedo acceder a las propiedades de las tablas relacionadas a través de los EntitySet y EntityRef. Muchisimas gracias me ha quedado muy claro todo.
    Saludos, me suscribo a tu blog.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s