martes, 20 de julio de 2010

LINQ2Sql: Modificar dinámicamente el nombre de una tabla en un DataContext de LINQ.

Ya van varias veces que veo preguntas de cómo hacer para cambiar dinámicamente el nombre de una tabla de un mapeo de LINQ.

Algunas aplicaciones crean nuevas tablas dinámicamente a medida que, por algún motivo, las van necesitando. Estas nuevas tablas se crean a partir de una tabla molde o "base".
Dicho de otra manera: Se tiene una base de datos en la que n tablas tienen el mismo esquema y sólo difieren en su nombre y, se desea poder mapear dichas tablas con un único DataContext que pueda ser instanciado dinámicamente según la tabla que se quiera consultar.

La misma idea se puede aplicar para el nombre de una base de datos o cualquier otro objeto de SQL.

NOTA: Los ejemplos de esta entrada están en inglés porque inicialmente fue la respuesta de este thread de MS

EL PROBLEMA:

Por defecto, el mapeo de LINQ se lleva a cabo mediante atributos y, como tales, no pueden ser modificados en tiempo de ejecución.
Pero por suerte el constructor de la clase DataContext de LINQ tiene una sobrecarga con el parámetro mappingSource, que nos permite obtener el DataContext a partir de un mapeo externo (que puede ser un archivo .xml).

CASO DE EJEMPLO:

Se tiene la tabla base Person, y otras dos tablas más llamadas Person_1 y Person_2, todas con idéntica estructura.

A continuación se lista el script que crea estas tablas:


Use [DB_Test]
GO
Create Table dbo.Person
(
ID int PRIMARY KEY,
Name nvarchar(MAX)
)
Create Table dbo.Person_1
(
ID int PRIMARY KEY,
Name nvarchar(MAX)
)
Create Table dbo.Person_2
(
ID int PRIMARY KEY,
Name nvarchar(MAX)
)
Insert Into dbo.Person Values (1, 'Base Table')
Insert Into dbo.Person_1 Values (1, 'Some data in Table_1')
Insert Into dbo.Person_2 Values (1, 'Some data in Table_2')

SOLUCIÓN:

La solución propuesta es mapear una única vez el esquema de la tabla y al momento de obtener el DataContext, especificar el nombre de la tabla que se quiere consultar.

Lo primero que necesitamos es generar un archivo .dbml con el mapeo de la tabla "base" de la forma habitual (Add New Item -> LINQ to SQL classes).
Como nombre le pondremos Person.dbml, por lo cual la clase generada se llamará PersonDataContext.
El diagrama quedará así:
dbml

Una vez generado el mapeo de la tabla base, procedemos a generar a partir de éste, un archivo .xml que también contendrá el mapeo.
Para esto utilizaremos la herramienta sqlmetal.
En línea de comandos de Visual Studio (Visual Studio Tools -> Visual Studio 2008 Command Prompt), ejecutar desde la carpeta donde se encuentra el archivo person.dbml:
SqlMetal /map:person.xml /code person.dbml
Esto generará el archivo person.xml cuyo contenido será algo como:


<?xml version="1.0" encoding="utf-8"?>
<Database Name="DB_Test" xmlns="http://schemas.microsoft.com/linqtosql/mapping/2007">
<Table Name="dbo.Person" Member="Persons">
<Type Name="Person">
<Column Name="ID" Member="ID" Storage="_ID" DbType="Int NOT NULL" IsPrimaryKey="true" />
<Column Name="Name" Member="Name" Storage="_Name" DbType="NVarChar(MAX)" />
</Type>
</Table>
</Database>


A continuación, es recomendable incluir el archivo person.xml en nuestro proyecto como recurso embebido, con las propiedades Build Action=Enbedded Resource y Copy to Output Directory=Do not copy, como se muestra a continuación:
add

Ahora necesitamos generar un método que retorne el DataContext apuntando a una tabla en específico.
Lo más cómodo es codificarlo en la misma clase (PersonDataContext).
La forma más directa es parándonos sobre Person.dbml en el solution explorer y haciendo click en el botón View Code. Esto generará el archivo Person.cs con la clase PersonDataContext a la que le agregamos el método y queda así:


namespace DynamicDataContext
{
 using System.Linq;
 using System.Xml;
 using System.Xml.Linq;
 using System.Data.Linq.Mapping;
 using System.IO;
 using System.Reflection;
 partial class PersonDataContext
 {
  private const string CNN_STRING = "Data Source=.;Initial Catalog=DB_Test;Integrated Security=True";
  public static PersonDataContext GetDataContext(string tableName)
  {
   // Get the .xml file into memory 
   Stream ioSt = Assembly.GetExecutingAssembly().GetManifestResourceStream("DynamicDataContext.person.xml");
   XElement xe = XElement.Load(XmlReader.Create(ioSt));
   // Replace the table name value in memory
   var tableElements = xe.Elements().AsQueryable().Where(e => e.Name.LocalName.Equals("Table"));
   foreach (var t in tableElements)
   {
    var nameAttribute = t.Attributes().Where(a => a.Name.LocalName.Equals("Name"));
    foreach (var a in nameAttribute)
    {
     if (a.Value.Equals("dbo.Person"))
     {
      a.Value = a.Value.Replace("Person", tableName);
     }
    }
   }
   // Obtain and return the dynamic DataContext
   XmlMappingSource source = XmlMappingSource.FromXml(xe.ToString());
   return new PersonDataContext(CNN_STRING, source);
  }
 }
}
Y eso es todo…
Cada vez que necesitemos el datacontext, llamaremos al método GetDataContext recién creado pasándole el nombre de la tabla que nos interesa en ese momento:


static void Main()
{
 // Query table Person_2 
 using (PersonDataContext dc = PersonDataContext.GetDataContext("Person_2"))
 {
  Console.WriteLine(dc.Persons.First().Name);
 }
 // Query table Person_1
 using (PersonDataContext dc = PersonDataContext.GetDataContext("Person_1"))
 {
  Console.WriteLine(dc.Persons.First().Name);
 }
 // Query table Person
 using (PersonDataContext dc = PersonDataContext.GetDataContext("Person"))
 {
  Console.WriteLine(dc.Persons.First().Name);
 }
 // Query default table (Person in this case)
 using (PersonDataContext dc = new PersonDataContext())
 {
  Console.WriteLine(dc.Persons.First().Name);
 }
 Console.ReadKey();
}


-- Puedes bajar este ejemplo aquí --


Suerte !

Federico.

1 comentario:

Unknown dijo...

Gracias man me re sirvio el codigo para un proyecto

Datos personales