Mostrando entradas con la etiqueta entity framework. Mostrar todas las entradas
Mostrando entradas con la etiqueta entity framework. Mostrar todas las entradas

miércoles, 4 de julio de 2012

Entity Framework y el borrado de entidades (II) - usando lambda y árboles de expresión

Como ya explique en el artículo anterior la forma más natural de borrar un registro es a través de su clave primaria, aunque eso no siempre tiene porque ser así. Un par de ejemplos sobre esto puede ser
  • Borrar aquellos registros que tengan más de un año de antigüedad. Práctico en tablas de logs.
  • Borrar aquellos registros que tengan un estado determinado.
  • O simplemente querer borrar registros en base a una clave ajena, como por ejemplo, borrar todas los lineas de una factura.
En todos estos casos la solución planteada en el artículo anterior no nos valdría ya que asumimos que tan sólo vamos a borrar un registro.

Borrado de varias entidades
Basándonos en este ejemplo
public class Bar
{
  public int IdBar { get; set; }
  public string Description { get; set; }
  public int IdState { get; set; }
}
y queriendo borrar aquellos registros que estén en el estado 1 podríamos hacer lo siguiente
using (DataContext context = new DataContext())
{
  var list = context.Bar.Where(m => m.IdState == 1);
  foreach (Bar bar in list)
    context.Bar.Remove(bar);
  context.SaveChanges();
}
Si analizamos el código anterior veremos que para borrar n elementos, debemos hacer n + 1 accesos a la base de datos. Lanzaremos n consultas de borrado, y necesitaremos una más para recuperar los elementos a borrar.

Como últimamente me he vuelto un inconformista esta solución no me gusta porque para borrar 1000 elementos necesito hacer 1001 acceso a base de datos y eso me parece demasiada carga para un servidor, que en mi caso, ya anda demasiado justo de recursos.

La primera solución que se me ocurrió fue usar un método de este estilo
using (DataContext context = new DataContext())
{
  context.Database.ExecuteSqlCommand("DELETE FROM Bar WHERE IdState = @IdState", new 
object[] { new System.Data.SqlClient.SqlParameter("IdState", 1) });
}
Esta solución es bastante buena aunque me deja atado a una tabla y a un filtro en concreto, lo cual me obliga a estar estableciendo métodos por cada tabla donde desee borrar un conjunto de datos y si uso varios filtros a estar usando parámetros opcionales (que suelen afear el código bastante).

Para mi lo ideal sería en un sólo método poder resolver esta situación, y tras investigar un poco se puede hacer con algo de código extra. Dado que estoy usando Code First he creado un método Delete en el contexto con el siguiente aspecto
public void Delete<T>(Expression<Func<T, bool>> condition)
{
  string where = generateWhere(condition.Body);
  string query = String.Format("DELETE FROM {0} WHERE {1}", typeof(T).Name, where);
  ((IObjectContextAdapter)this).ObjectContext.ExecuteStoreCommand(query);
}
De esta manera puedo pasar que entidad estoy intentado borrar y los filtros que voy a usar en el borrado. Antes de continuar me gustaría aclarar que transformar un árbol de expresion a una condición SQL no es una tarea fácil y esta llena de pequeño matices que puede que hagan que no sea rentable realizar el esfuerzo. En mi caso las condiciones de borrado son muy simples y la mejora de rendimiento (que veremos al final) justificaban el pequeño esfuerzo que ha llevado el desarrollo de este método. Pero ojo, hacer un LINQ Provider no es una tarea que se pueda considerar trivial.

Bien, con esto podríamos llamar a nuestro método de borrado de la siguiente manera
using (DataContext context = new DataContext())
{
  context.Delete<Bar>(m => m.IdState == 1);
}
Si analizamos nuestra Expression de entrada vemos como tiene una propiedad Body donde está la/s condiciones que hemos pasado y que tenemos que transformar a una condición Where de SQL. En esta propiedad tenemos tanto la parte izquierda como la parte derecha de la condición, así como el operador que se está usando. En el método generateWhere analizaremos todas estas partes para devolver un Where válido.
private string generateWhere(dynamic operation)
{
  string left = generateToken(ExpressionTypeEnum.Left, operation.Left);
  string right = generateToken(ExpressionTypeEnum.Right, operation.Right);

  // TODO: Completar operaciones (por ahora con estas es suficiente)
  var ops = new Dictionary();
  ops.Add(ExpressionType.Equal, "=");
  ops.Add(ExpressionType.GreaterThan, ">");
  ops.Add(ExpressionType.GreaterThanOrEqual, ">=");
  ops.Add(ExpressionType.LessThan, "<");
  ops.Add(ExpressionType.LessThanOrEqual, "<=");

  ops.Add(ExpressionType.And, "AND");
  ops.Add(ExpressionType.AndAlso, "AND");
  ops.Add(ExpressionType.Or, "OR");
  ops.Add(ExpressionType.OrElse, "OR");

  return string.Format("{0} {1} {2}", left, ops[operation.NodeType], right);
}
En mi caso estoy tomando como base que la Expression es de tipo BinaryExpression aunque más exactamente es de tipo LogicalBinaryExpression (esto lo podemos comprobar en tiempo de ejecución). En otras expresiones este análisis de la expresión fallaría por no tener alomejor una propiedad Right.

Vemos que tanto para la parte izquierda como para la parte derecha llamamos al método generateToken que se encarga de generarnos una cadena con el valor contenido en esa parte, e incluso, de llamar recursivamente al método generateWhere en caso que estemos combinado filtros de esta manera
using (DataContext context = new DataContext())
{
  context.Delete<Bar>(m => m.IdState == 1 || m.IdState == 2);
}
El método generateToken tendría el siguiente aspecto
private string generateToken(ExpressionTypeEnum type, dynamic operation)
{
  string token = string.Empty;
  if (operation is BinaryExpression)
    token = generateWhere(operation);
  else
    token = (type == ExpressionTypeEnum.Left ? operation.Member.Name : operation.Value.ToString());
   
  return token;
}
Como único detalle hay que tener en cuenta que dependiendo de que parte de la expresión estemos analizando extraeremos el token de una u otra propiedad.

Con esto ya tenemos nuestro pequeño "parser" listo para ser usado en un entorno controlado y con condiciones simples. He detectados otras expresiones que poco a poco iré añadiendo. Estas expresiones son
  • UnaryExpression. Para filtros de tipo m => !m.IsActive. Donde IsActive es de tipo boolean.
  • PropertyExpression: Para filtros de tipo m=> m.IsActive. Donde IsActive es de tipo boolean.
  • MethodBinaryExpression: Para filtros de tipo m=>m.Date < DateTime.Now. Donde como es lógico Date es de tipo DateTime.
Para ver la mejora de rendimiento he realizado un borrado iterativo y un borrado con esté método usando el siguiente código
Iterativo
using (DataContext context = new DataContext())
{
  var list = context.Bar.Where(m => m.IdState == 1);
  foreach (Bar bar in list)
    context.Bar.Remove(bar);
  context.SaveChanges(); 
}
Directo
using (DataContext context = new DataContext())
{
  context.Delete<Bar>(m => m.IdState == 1);
}

He lanzado estos métodos borrado 10, 100, 1000 y 10000 elementos. Cada bloque lo he lanzado tres veces y he calculado la media para dar la duración de la duración. Con estas condiciones estos son los datos que he obtenido

Registros a borrar - Tiempo de ejecución en ms 

Conclusiones
Como vemos en la tabla para borrados de más de 1000 registros el borrado iterativo se puede vuelve un poco pesado, ya que nos lleva casi 4 segundos ejecutarlo (producto de tener que lanzar 1001 consultas a la base de datos), mientra que nuestro borrado directo no llega a los 12 ms. En condiciones más extremas (10.000 registros) el borrado iterativo llega a casi 2 minutos de ejecución mientras que el directo no nos pasa de un cuarto de segundo. Interesante, ¿verdad?

En la mayoría de los escenarios no merece la pena complicarnos la vida tanto ya que tan solo borraremos un par de registros y podemos optar por el método iterativo, ya que como vemos entre ambos métodos para 10 registros casi no tenemos diferencias de tiempos. Para borrados de más registros siempre podemos optar procedimientos almacenados o lanzar directamente nosotros la consultas desde nuestro programa, pero si queremos tener a nuestra disposición mucho filtros para borrar está opción nos puede valer.

Eso si, como ejercicio de programación ha estado realmente bien y me he permitido ver por encima como funcionan los árboles de expresión.
Happy coding!

martes, 3 de julio de 2012

Entity Framework y el borrado de entidades (I)

A la hora de borrar entidades en Entity Framework nos damos cuenta que podemos actuar de dos maneras diferentes sobre todo si tenemos en cuenta factores como el número de consultas que tenemos que lanzar contra nuestra base de datos.

Borrado de entidades por la clave primaria
Quizás es la forma más natural de realizar un borrado aunque no por ello es la única. Supongamos que tenemos la siguiente entidad POCO con la cual hemos montado nuestra base de datos a través de Code First.
public class Bar
{
  public int IdBar { get; set; }
  public string Description { get; set; }
  public int IdState { get; set; }
}
El código para borrar una entidad por su clave primera podría ser algo así
private void DeleteByID(int idBar)
{
  using (DataContext context = new DataContext()) {
    Bar bar = context.Bar.Where(m => m.IdBar == idBar).SingleOrDefault();
    context.Bar.Remove(bar);
    context.SaveChanges();
  }
}
¿Sencillo verdad? La verdad es que si, aunque si somos un poco curiosos o simplemente sabemos lo que estamos haciendo y usamos el SQL Server Profiler veremos que la ejecución de ese código tan simple genera estas dos consultas
exec sp_executesql N'SELECT TOP (2) 
[Extent1].[IdBar] AS [IdBar], 
[Extent1].[Description] AS [Description], 
[Extent1].[IdState] AS [IdState]
FROM [dbo].[Bar] AS [Extent1]
WHERE [Extent1].[IdBar] = @p__linq__0',N'@p__linq__0 int',@p__linq__0=1
exec sp_executesql N'delete [dbo].[Bar]
where ([IdBar] = @0)',N'@0 int',@0=1
O sea, que estamos lanzando dos consultas contra nuestra base de datos para simplemente hacer un borrado. Esto desde mi punto de vista es un poco absurdo, aunque menos mal que con un pequeño truco podemos resolver este pequeño inconveniente. El truco consiste en adjuntar a nuestro contexto una entidad con la clave que queramos borrar, luego la borramos y grabamos los cambios. Con esto se lanzará la consulta de borrado con la información de la clave que deseamos borrar. El código para esto es el siguiente
private void DeleteByID(int idBar)
{
  using (DataContext context = new DataContext())
  {
    Bar bar = new Bar() { IdBar = idBar };
    context.Bar.Attach(bar);
    context.Bar.Remove(bar);
    context.SaveChanges();
  }
}
Si usamos el SQL Profiler con este método veremos que tan solo se ejecuta esta consulta
exec sp_executesql N'delete [dbo].[Bar]
where ([IdBar] = @0)',N'@0 int',@0=1
Como vemos, con un pequeño cambio hemos podido resolver de una manera fácil el problema de rendimiento que podríamos tener si hacemos muchos borrados en nuestra aplicación.

En el próximo artículo trataré el borrado de registros cuando no estamos borrando por la clave primaria y donde una vez más el número de accesos a la base de datos puede ser un factor muy importante a tener en cuenta.

Happy codding!

miércoles, 25 de abril de 2012

Entity Framework Code First DbMigration (II)

En el anterior artículo sobre las migraciones automáticas en Entity Framework vimos como lanzar las migraciones desde la Package Manager Console de Visual Studio. La pregunta ahora sería, ¿podemos lanzar las migraciones desde el código de nuestra aplicación?, y la respuesta como no cabía esperar de otra manera es que si.

Para crear los ficheros de migración seguiremos usando la consola siguiendo los pasos descritos en el artículo anterior, pero para actualizar la base de datos haremos lo siguiente:
var config = new Migrations.Configuration();
var dbMigrator = new DbMigrator(config);
dbMigrator.Update();
Esto lanzará las migraciones pendientes. En caso que queramos lanzar una migración en concreto tendremos que indicárselo al método Update de la siguiente manera
var config = new Migrations.Configuration();
var dbMigrator = new DbMigrator(config);
dbMigrator.Update("201204191333459_InitialCreate");
Como vemos es bastante fácil lanzar nuestras actualizaciones desde código, pero cuidado con lo que hacemos sobre todo si la base de datos está en producción no sea que nos llevemos sorpresas desagradables.

Happy coding!

Mas info | Entity Framework Code First DbMigration (I)

lunes, 23 de abril de 2012

Entity Framework Code First DbMigration (I)

Siguiendo con mi línea de aprendizaje de Entity Framework esta vez le ha tocado el turno a una de las novedades introducidas en la versión 4.3, las migraciones automáticas o DbMigration. Creo que su nombre deja claro para que sirve, pero básicamente nos da soporte para realizar actualizaciones de nuestra base de datos. ¿Y qué podemos actualizar?, pues básicamente podremos crear, renombrar y borrar tanto tablas como columnas, así como crear índices, claves ajenas o incluso ejecutar sentencias SQL para actualizar registros. ¿Impresionado? ¿No? Pues deberías, y sino sigue leyendo para que veas lo que podrás hacer.

Bien, para poder hacer uso de las migraciones deberás tener instalado la versión 4.3 de Entity Framework como mínimo, en caso que no tengas estas versión o no estés seguro de que versión estás usando te recomiendo una lectura de mi artículo sobre Entity Framework Fluent Api, donde en la primera parte cuento como actualizar el Entity Framework a través del Package Manager Console de Visual Studio.

Lo primero que haremos será crear nuestras clases POCO, y nuestro DataContext (esto también está explicado en el artículo anteriormente mencionado).
Primero las clases POCO
public class Blog
{
 public int IdBlog { get; set; }
 public string Title { get; set; }
 public string Url { get; set; }

 // Navigation properties
 public virtual ICollection<Post> Posts { get; set; }
}

public class Post
{
 public int IdPost { get; set; }
 public string Title { get; set; }
 public string Body { get; set; }  

 // Foreign key
 public int IdBlog { get; set; }

 // Navigation properties
 public virtual Blog Blog { get; set; }
 public virtual Category Category { get; set; }
 public virtual ICollection<Tag> Tags { get; set; }
}

public class Category
{
 public int IdCategory { get; set; }
 public string Name { get; set; }

 // Navigation properties
 public virtual ICollection<Post> Posts { get; set; }
}

public class Tag
{
 public int IdTag { get; set; }
 public string Name { get; set; }

 // Navigation properties
 public virtual ICollection<Post> Posts { get; set; }
}

Y ahora nuestro DbContext
public class DataContext : DbContext
{
 public DataContext() : base("DataContext") {
            
 }

 public DataContext(string nameOrConnectionString) : base(nameOrConnectionString)
 {

 }

 public DbSet<Blog> Blog { get; set; }
 public DbSet<Post> Post { get; set; }
 public DbSet<Category> Category { get; set; }
 public DbSet<Tag> Tag { get; set; }

 protected override void OnModelCreating(DbModelBuilder modelBuilder)
 {
  modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
  
  // Primary Keys
  modelBuilder.Entity<Blog>().HasKey(c => c.IdBlog);
  modelBuilder.Entity<Post>().HasKey(c => c.IdPost);
  modelBuilder.Entity<Category>().HasKey(c => c.IdCategory);
  modelBuilder.Entity<Tag>().HasKey(c => c.IdTag);

  // Properties
  modelBuilder.Entity<Blog>().Property(p => p.Title).IsRequired();
  modelBuilder.Entity<Blog>().Property(p => p.Title).HasMaxLength(50);

  modelBuilder.Entity<Post>().Property(p => p.Title).IsRequired();
  modelBuilder.Entity<Post>().Property(p => p.Title).HasMaxLength(50);
 }
}
Llegados a este punto ya podemos empezar a usar las migraciones propiamente dichas. Lo primero que haremos será habilitar las migraciones ejecutando lo siguiente en la consola
PM> Enable-Migrations
Si todo va bien deberemos ver algo parecido a esto
Detected database created with a database initializer. Scaffolded migration '201204191333459_InitialCreate' corresponding to current database schema. To use an automatic migration instead, delete the Migrations folder and re-run Enable-Migrations specifying the -EnableAutomaticMigrations parameter.
Code First Migrations enabled for project EntityFramework.CodeFirst.DbMigration.
¿Qué mas ha pasado? Pues se habrá creado una carpeta en nuestro proyecto con dos ficheros
  • *_InitialCreate.cs: Donde tendremos el script de creación de nuestra base de datos, en base a nuestro modelo de clase POCO.
  • Configuration.cs: Donde podremos configurar la migración y sobreescribiendo el método Seed llenar de datos nuestra base de datos.
¿Y ahora? Pues simple, actualizaremos nuestra base de datos con el script que hemos creado. Para esto usaremos el siguiente comando
PM> Update-Database
Y si todo ha ido bien veremos algo así
Specify the '-Verbose' flag to view the SQL statements being applied to the target database.
Applying explicit migrations: [201204191333459_InitialCreate].
Applying explicit migration: 201204191333459_InitialCreate.
Si vamos a la base de datos veremos que se ha creado la base de datos y dentro de la tablas del sistema se ha creado __MigrationHistory. Esta tabla es usada por el Entity Framework para llevar un control de las versiones.

Ahora supongamos que queremos añadir un campo a nuestra tabla blog, por ejemplo el campo Author, que deberá ser de tipo varchar, obligatorio y tener una longitud máxima de 250 caracteres. Lo primero será añadir este campo al objeto Blog como propiedad
public string Author { get; set; }
Lo siguiente será crear un nuevo script de migración de la siguiente forma
PM> Add-Migration BlogAddAuthor
Y si todo va bien veremos lo siguiente
Scaffolding migration 'BlogAddAuthor'.
The Designer Code for this migration file includes a snapshot of your current Code First model. This snapshot is used to calculate the changes to your model when you scaffold the next migration. If you make additional changes to your model that you want to include in this migration, then you can re-scaffold it by running 'Add-Migration 201204191351431_BlogAddAuthor' again.
También veremos que se ha creado un fichero *_BlogAddAuthor.cs, con esta estructura
public partial class BlogAddAuthor : DbMigration
{
    public override void Up()
    {
        AddColumn("Blog", "Author", c => c.String());
    }
        
    public override void Down()
    {
        DropColumn("Blog", "Author");
    }
}
Primero decir, que el método Up se llama cuando actualizamos (update) la base de datos y el método Down se llama cuando retrocedemos a una versión (downgrade). Aunque podemos modificar esto para ajustarnos a nuestras necesidades
public override void Up()
    {
        AddColumn("Blog", "Author", c => c.String(nullable: false, maxLength: 250));
    }
¿Qué cosas podemos hacer en estos métodos? Pues tenemos a nuestro alcance los siguiente métodos a través de la clase DbMigration
  • AddColumn
  • AddForeignKey
  • AlterColumn
  • CreateIndex
  • CreateTable
  • DropColumn
  • DropForeignKey
  • DropIndex
  • DropPrimaryKey
  • DropTable
  • MoveTable
  • RenameColumn
  • RenameTable Sql
Si no hubiéramos añadido la propiedad a la clase, obtendríamos este fichero vacio y tendríamos que añadir las sentencias AddColumn y DropColumn nosotros mismos (no siempre todos lo campos de la base de datos estarán en nuestras clases). Para actualizar nuestro modelo nuevamente usaremos Update-Database aunque esta vez usaremos el modificador -Verbose para ver un log de lo se está haciendo
PM> Update-Database -Verbose
Using NuGet project 'EntityFramework.CodeFirst.DbMigration'.
Using StartUp project 'EntityFramework.CodeFirst.DbMigration'.
Target database is: 'DbMigration' (DataSource: .\sql2k8r2, Provider: System.Data.SqlClient, Origin: Configuration).
Applying explicit migrations: [201204191358443_BlogAddAuthor].
Applying explicit migration: 201204191358443_BlogAddAuthor.
ALTER TABLE [Blog] ADD [Author] [nvarchar](250) NOT NULL DEFAULT ''
[Inserting migration history record]


¿Podemos hacer más? Pues si, por ejemplo volver a un versión. En este caso volveremos a la versión inicial ejecutando esto
PM> Update-Database –TargetMigration:$InitialDatabase –Force
Specify the '-Verbose' flag to view the SQL statements being applied to the target database.
Reverting migrations: [201204191358443_BlogAddAuthor, 201204191333459_InitialCreate].
Reverting explicit migration: 201204191358443_BlogAddAuthor.
Reverting explicit migration: 201204191333459_InitialCreate.
Si queremos ir a nuestra versión en concreto ejecutaremos
‘Update-Database –TargetMigration:"NombreDeLaVersion" –Force’
Para obtener más ayuda sobre los distintos comandos que tenemos (Enable-Migrations: Enables Code First Migrations in a project. Add-Migration: Scaffolds a migration script for any pending model changes. Update-Database: Applies any pending migrations to the database. Get-Migrations) tan solo debemos escribir lo siguiente en la Package Manager Console
PM> get-help Update-Database
Y obtendremos una ayuda detallada del comando.

Como vemos tenemos en nuestras manos una excelente herramienta para llevar una sincronización perfecta de nuestras base de datos

Happy coding!

martes, 17 de abril de 2012

Entity Framework Code First Fluent API (I)

Code First es un nuevo enfoque de trabajo que aparece en el Entity Framework a partir de las versión 4.1, que nos permite crear nuestro modelo mediante clases POCO (Plain Old CLR Object), a partir de las cuales se generará nuestro modelo de base de datos. Otra característica es que en este modelo no existe el fichero edmx de definición del modelo (conceptual y de datos).

La principal ventaja de usar clases POCO es que el modelo de clases es más limpio ya que las clases no tiene ninguna dependencia con el Entity Framework (clase EntityObject). Por defecto se utiliza el nombre de nuestras clases y correspondientes propiedades para generar los nombre de tablas y campos. Esto puede ser más que suficiente para muchos proyectos, sobre todo si están empezando de cero, pero, ¿que ocurre si   queremos hacer modificaciones en nuestro modelo de datos? Para eso tenemos dos alternativas
  • Data Annotations
  • Fluent Api
Como ya he comentado al principio, para poder trabajar con CodeFirst es necesario usar la versión 4.1 (o superior) del Entity Framework. Para descargar e instalar esta versión la mejor alternativa es utilizar NuGet Gallery. Para eso en nuestro Visual Studio vamos a Herramientas > Library Package Manager > Package Manager Console. Con esto se nos abrirá una consola


donde escribiremos
Install-Package EntityFramework -Version 4.3.1
Si todo ha ido bien veremos algo parecido a esto


Ya tenemos la última versión del Entity Framework instalada en nuestro proyecto, así que, ¿ahora qué? Para demostrar el uso de Fluent Api para definir nuestro modelo de datos, voy a usar la estructura que podría tener un blog (muy simplificado). Para estos tendremos
  • Un blog con su identificador y título.
  • Una lista de post asociada a nuestro blog. Cada post tendrá su identificador, titulo, un título en html y un texto.
  • Cada post pertenecerá a una categoría y podrá tener una lista de etiquetas (tags). Tanto las categorías como las etiquetas, tendrán tanto un identificador como un nombre.
Con este esquema tan simple crearemos nuestras clases de la siguiente manera.
public class Blog
{
    public int IdBlog { get; set; }
    public string Title { get; set; }

    // Navigation properties
    public virtual ICollection<Post> Posts { get; set; }
}

public class Post
{
    public int IdPost { get; set; }
    public string Title { get; set; }
    public string HtmlTitle { get; set; }
    public string Body { get; set; }

    // Foreign key
    public int IdBlog { get; set; }

    // Navigation properties
    public virtual Blog Blog { get; set; }
    public virtual Category Category { get; set; }
    public virtual ICollection<Tag> Tags { get; set; }
}

public class Category
{
    public int IdCategory { get; set; }
    public string Name { get; set; }

    // Navigation properties
    public virtual ICollection<Post< Posts { get; set; }
}

public class Tag
{
    public int IdTag { get; set; }
    public string Name { get; set; }

    // Navigation properties
    public virtual ICollection<Post> Posts { get; set; }
}


Una vez hecho esto empezaremos a definir nuestro contexto de la siguiente manera
public class DataContext : DbContext { }
Ahora definiremos un constructor donde especificaremos el nombre de la cadena de conexión y el conjunto de entidades que serán mapeadas (en nuestro caso todas)
public class DataContext : DbContext
{
    public DataContext() : base("DataContext") { }

    public DbSet<Blog> Blog { get; set; }
    public DbSet<Post> Post { get; set; }
    public DbSet<Category> Category { get; set; }
    public DbSet<Tag> Tag { get; set; }
}
Lo siguiente que debemos hacer es definir nuestro modelo de datos, por ejemplo, definiendo cuales son las claves de nuestras tablas. Para estas definiciones usaremos la sintaxis que nos brinda Fluent Api, así que, sobreescribiremos el método OnModelCreating de nuestro DataContext y escribiremos lo siguiente para definir nuestras claves primarias
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    // Primary Keys
    modelBuilder.Entity<Blog>().HasKey(c => c.IdBlog);
    modelBuilder.Entity<Post>().HasKey(c => c.IdPost);
    modelBuilder.Entity<Category>().HasKey(c => c.IdCategory);
    modelBuilder.Entity<Tag>().HasKey(c => c.IdTag);           
}
Con este código "casi" ya podríamos crear nuestro primer modelo de datos, y ver como funciona Code First con Fluent Api. Tan solo deberíamos añadir este código a nuestro programa (para este ejemplo yo lo estoy poniendo en el constructor de un formulario)
public Main()
{
    InitializeComponent();

    Database.SetInitializer<DataContext>(new DropCreateDatabaseIfModelChanges<DataContext>());

    using (DataContext context = new DataContext()) {
        Blog b = context.Blog.FirstOrDefault();
    }
}
Bien este código tiene dos partes importantes
  • En la primera parte indicaremos cual es la estrategia de incialización de nuestro modelo. Entity Framework Code First incluye 3 inicializadores (ojo con estos inicializadores cuando pasemos nuestra aplicación a producción porque podemos perder todos nuestros datos): 
    1. CreateDatabaseIfNotExist
    2. DropCreateDatabaseAlways
    3. DropCreateDatabaseIfModelChanges
  • En la segunda parte hacemos una simple consulta (obtenemos el primer blog) a nuestro contexto porque si no realizamso una consulta no se ejecutaría nuestro inicializador (si nunca accedemos a nuestros datos, para que se va a perder el tiempo en crear el modelo de datos).
Si todo va bien veremos que el siguiente modelo de datos se ha creado

Vemos que con el modelo conceptual (clases POCO) y la definición de las claves primarias Entity Framework ha creado un esquena de base de datos que se ajusta perfectamente a nuestra definición. Inlcluso a creado una tabla intermedia (TagPost) para poder gestionar la relación muchos a muchos que hay entre los Posts y los Tags.

A partir de aquí podemos modificar nuestro modelo de datos a través de Fluent Api añadiendo estas lineas al método OnModeloCreating. Por ejemplo:

Evitar que la clave primaria sea un identity en la tabla Categoria
modelBuilder.Entity<Category>()
  .Property(p => p.IdCategory) 
  .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
Forzar a que un campo sea obligatorio (NOT NULL) y tenga una longitud máxima de 50 caracteres
modelBuilder.Entity<Blog>().Property(p => p.Title).IsRequired();
modelBuilder.Entity<Blog>().Property(p => p.Title).HasMaxLength(50);
Evitar que una propiedad de nuestro modelo conceptual esté en en la base de datos
modelBuilder.Entity<Post>().Ignore(p => p.HtmlTitle);
Si volvemos a ejecutar nuestra aplicación y dado que nuestra estrategia de inicialización era borrar la base de datos si el modelo ha cambiado, y éste ha cambiado, veremos que que nuestros cambios se ha realizado de la forma esperada.
  • El identificador de la tabla Category no es un identity
  • Se han modificado la características del campo Title de la tabla Blog

  • El campo HtmlTitle no se mapea en la tabla Post

Como vemos, tenemos total libertad para ir refinando nuestro modelo de datos a partir de nuestro modelo conceptual.

Happy coding !

lunes, 16 de abril de 2012

Registrando los cambios de nuestros objectos en Entity Framework (II)

En mi anterior artículo sobre como registrar cambios de nuestros objetos en el Entity Framework dejé sin terminar una parte. En concreto, la parte en la que obtendríamos el IdResource en nuestro TrackFactory.

La primera alternativa que utilice fue la siguiente
private Track TrackFactory(ObjectStateEntry entry)
{
 Track track = new Track();
 track.IdTrack = Guid.NewGuid();
 track.TableName = entry.EntitySet.Name;
 track.FC = DateTime.Now;
 track.UC = string.Empty;

 if (entry.State == EntityState.Added)
 {
  // Insert (I)
  track.IdResource = entry.EntityKey.EntityKeyValues[0].Value.ToString();  
  track.NewData = this.GetXml(entry, false);
  track.Action = TrackActionsEnum.I.ToString();
 }
 else if (entry.State == EntityState.Deleted)
 {
  // Update (U)
  track.IdResource = entry.EntityKey.EntityKeyValues[0].Value.ToString();
  track.OldData = this.GetXml(entry, true);
  track.NewData = this.GetXml(entry, false);
  track.Action = TrackActionsEnum.U.ToString();
 }
 else
 {
  // Delete (D)
  track.IdResource = entry.EntityKey.EntityKeyValues[0].Value.ToString();
  track.OldData = this.GetXml(entry, true);
  track.Action = TrackActionsEnum.D.ToString();
 }

 return track;
}
Esta solución hubiera sido fantástica salvo porque no funciona a la hora de obtener el identificador en la inserción de un registro, ya que EntityKey es nulo. Tras darle varias vueltas al asunto la única posibilidad que me quedó fue recorrer por reflexión todas las propiedades del objeto y detectar aquella que tuviera el atributo EdmScalarPropertyAttribute definido y la propiedad EntityKeyProperty a true. Al final el código no queda muy largo y es el siguiente
private string GetIdResource(ObjectStateEntry entry) {
 object entity = entry.Entity;

 PropertyInfo[] properties = entity.GetType().GetProperties();
  foreach (PropertyInfo pi in properties)
  {
  System.Object[] attributes = pi.GetCustomAttributes(true);
  foreach (object attribute in attributes)
    {
   if (attribute is EdmScalarPropertyAttribute)
    if (((EdmScalarPropertyAttribute)attribute).EntityKeyProperty == true)
     return pi.GetValue(entity, null).ToString();
  }
 }

 return null;
}
Por lo que el método TrackFactory quedo así al final
private Track TrackFactory(ObjectStateEntry entry)
{
        // ...
 if (entry.State == EntityState.Added)
 {
  // Insert (I)
  track.IdResource = this.GetIdResource(entry); 
  track.NewData = this.GetXml(entry, false);
  track.Action = TrackActionsEnum.I.ToString();
 }
        // ...
}
Con esta mejora es fácil hacer el seguimiento a nuestros cambios. Por ejemplo con este código
using (DataContext context = new DataContext())
{
 // Insertamos una películas
 context.AddToPelicula(new Pelicula() { IdPelicula = 1, Titulo = "El padrino", Sinopsis = "...", Calificacion = 10 });
 context.SaveChanges();
    
 // Modificamos la película
 Pelicula p1 = context.Pelicula.Where(m => m.IdPelicula == 1).Single();
 p1.Calificacion = 9;
 context.SaveChanges();

 p1 = context.Pelicula.Where(m => m.IdPelicula == 1).Single();
 p1.Calificacion = 10;
 context.SaveChanges();

 // Borramos la película
 p1 = context.Pelicula.Where(m => m.IdPelicula == 1).Single();
 context.Pelicula.DeleteObject(p1);
 context.SaveChanges();
}
Sería fácil (sabiendo el Id que tenía la película en la base de datos, ver la secuenda de la vida del registro en la base de datos como vemos en nuestra tabla.


En la próxima entrega intentaré resolver de una forma "no muy elegante" (pero tampoco he encontrado otra) el caso de la inserciones de registros donde el Id es un identity, por lo que no conocemos el valor del Id antes de grabar. Lo que nos llevará a tener que grabar todos nuestros objectos en la base de datos para posteriormente grabar el registro de cambios.

Happy coding!

 Mas Info | Registrar cambios en nuestros objecto en el Entity Framework (I)

miércoles, 11 de abril de 2012

Registrando los cambios de nuestros objetos en Entity Framework (I)

En estos días de descanso me he propuesto meterme más en las tripas del Entity Framework. Para ello me he planteado hacer un pequeño sistema de control de cambios en el que se controle cuando se inserta o elimina un registro, o si se modifica, los valores que tenía antes y después de la modificación.

La primero entonces sería interceptar, o bien las inserciones, borrado o modificaciones de los objetos o detectar cuando se van a grabar los cambios. Tras leer la documentación que hay parece ser que la única valida es engancharnos a el evento SavingChanges que dispara la clase ObjectContext antes de iniciar el grabado de nuestros datos.

Con esto claro, me creo el edmx de una base de datos muy simple donde tan solo tengo una tabla que almacena películas, con su titulo, sinopsis y calificación. Despliego el Model.edmx y luego abro el Model.Designer.cs para ver el nombre exacto de la clase y así poder crear una clase parcial donde pondré toda la lógica de mi sistema de control de cambios.



Con esto me creo mi clase parcial DataContext y ahí me engancharé al evento SavingChanges. Ahora bien, ¿donde me engancho a este evento? Revisando el objeto DataContext veo que hay un método parcial que se llama OnContextCreated que es llamado cuando el contexto se crea y que parece un buen sitio donde engancharnos al evento, ya que sino tendríamos que sobreescribir los 3 constructores de la clase ObjectContext. El código de nuestra clase quedaría así

public partial class DataContext
{
  partial void OnContextCreated()
  {
    this.SavingChanges += new EventHandler(DataContext_SavingChanges);
  }

  private void DataContext_SavingChanges(object sender, EventArgs e)
  {
    // Gestión del evento SavingChanges
  }
}

Con esto ya tenemos la primera parte del trabajo hecha, que es interceptar las llamadas al método SaveChanges de nuestro contexto. Ahora queda la segunda parte del trabajo, detectar los cambios en nuestros objectos para registrar la información donde estimemos oportuno, en mi caso, en la base de datos.


Para esto lo primero que haremos será definir una tabla donde almacenaremos los cambios. La tabla tendría una pinta como esta


Donde:
  • IdTrack: Identificador de cada registro en la tabla.
  • TableName: Nombre de la tabla donde estamos insertando, modificando o borrando los registros.
  • Action: Acción a realizar, inserción (I), modificación (U) o borrado (D).
  • IdResource: Identificador del registro insertado, modificado o borrado en su tabla.
  • OldData y NewData: Objeto antes y después de ser insertado, modificado o borrado.
  • UC y FC: Usuario y fecha de creación del registro en la tabla de Tracks.
Una vez hecho tendremos que recorrer la lista de objectos que mantiene en nuestro DataContext y obtener aquellos que han cambiado. Para esto usaremos el ObjetctStateManager. El ObjectStateManager mantiene el estado de los objetos y la administración de identidades en las instancias de tipo de entidad y en las instancias de relación.
IEnumerable<ObjectStateEntry> pendingChanges = 
    this.ObjectStateManager.GetObjectStateEntries(
        EntityState.Added | 
        EntityState.Deleted | 
        EntityState.Modified);

En pendingChanges tenemos aquellos elementos que han cambiado y que deberemos procesar para registrar los cambios. Tan solo debemos recorrer la lista descartando aquellos elemento que sean una relación o que sean del tipo Track (no vamos a registrar los cambios de los objetos que registran los cambios).
foreach (ObjectStateEntry obj in pendingChanges)
{
    // Comprobamos si el objeto no es una relación y no es de tipo Track
    if (!(obj.IsRelationship) && (obj.Entity != null) && !(obj.Entity is Track))
    {
        Track audit = this.TrackFactory(obj);
        tracks.Add(audit);
    }
}

if (tracks.Count > 0)
{
    foreach (var audit in tracks)
        this.AddToTrack(audit); // Añadimos los Tracks
}
Para crear el objeto Track, usaremos el método TrackFactory que tendría esta pinta
private Track TrackFactory(ObjectStateEntry entry)
{
    Track track = new Track();
    track.IdTrack = Guid.NewGuid();
    track.TableName = entry.EntitySet.Name;
    track.FC = DateTime.Now;        
    track.UC = string.Empty;

    if (entry.State == EntityState.Added)
    {
        // Insert (I)
        track.NewData = this.GetXml(entry, false);
        track.Action = TrackActionsEnum.I.ToString();
    }
    else if (entry.State == EntityState.Modified)
    {
        // Update (U)
        track.OldData = this.GetXml(entry, true);
        track.NewData = this.GetXml(entry, false);
        track.Action = TrackActionsEnum.U.ToString();
    }
    else
    {
        // Delete (D)
        track.OldData = this.GetXml(entry, true);
        track.Action = TrackActionsEnum.D.ToString();
    }

    return track;
}
Como vemos en el método tan solo miramos el estado en el que se encuentra la entidad (Added, Modified o Deleted) para consecuentemente montar el objeto Track. En caso de ser un objeto nuevo, dicho objeto sólo tendrá valores nuevos, y en caso de ser un borrado, sólo tendrá valores viejos. En el caso de las modificaciones, guardamos los valores antes y después de la modificación. El método GetXml tan solo genera un xml con el objeto que le pasemos haciendo uso del XmlSerializer de .NET.

Con esto ya tenemos montado la estructura básica de nuestro sistema, así que va siendo hora de probarlo. Para probarlo primero insertamos dos registros en nuestra base de datos de la siguiente manera
using (DataContext context = new DataContext())
{
    // Insertamos dos películas
    context.AddToPelicula(new Pelicula() { IdPelicula = 1, Titulo = "El padrino", Sinopsis = "...", Calificacion = 10 });
    context.AddToPelicula(new Pelicula() { IdPelicula = 2, Titulo = "La guerra de las galaxias", Sinopsis = "...", Calificacion = 10 });
    context.SaveChanges();
}
Y si consultamos la base de datos vemos lo siguiente
Luego probaremos a modificar y a borrar para ver el resultado
// Modificamos una película
Pelicula p1 = context.Pelicula.Where(m => m.IdPelicula == 1).Single();
p1.Calificacion = 9;
context.SaveChanges();

// Borramos una película
Pelicula p2 = context.Pelicula.Where(m => m.IdPelicula == 2).Single();
context.Pelicula.DeleteObject(p2);
context.SaveChanges();
Si consultamos la base de datos veremos que también se han registrado las operaciones
Si analizamos los campos OldData y NewData en el caso de la modificación vemos que efectivamente se guarda el estado del objeto antes de modificar y el estado del mismo después de la modificación
Como vemos el sistema funciona correctamente aunque hay algunos detalles que pulir como asignar el identificador del objecto que se está registrando al objeto Track. Esto es relativamente simplemente si lo identificadores son asignados por nosotros, pero se complica es caso que sean valores asignados en la base de datos, como puede ser valores por defecto (newid() en caso de los uniqueidentifier) o identity.

Otro punto a tener en cuenta es buscar la forma de desacoplar este código del ObjectContext y que pudiera ser un módulo independiente y que funcionará con cualquier .dbml que queramos. Tengo algunas al respecto pero lo dejaré para otros artículos.

Cualquier duda, comentario o sugerencia será bien recibida.

Happy coding!

Mas info | Entity Framework

jueves, 22 de marzo de 2012

Depurando las consultas SQL generadas por Entity Framework

Muchas veces, sobre todo en consultas complejas Entity Framework puede no devolvernos los datos que esperábamos. Normalmente cuando esto ocurre suelo recurrir al SQL Server Profiler, pero no siempre lo tengo disponible ya que es una funcionalidad que no está presente en la versión Express del SQL Server. Pero como siempre, los chicos de Microsoft han pensado en todo y hay una forma bastante simple de obtener la consulta que ha generado el Entity Framework para poder jugar con ella y ver donde está el fallo.
using (FooEntities entities = new FooEntities())
{
 var result = (from p in entities.Bar
   where p.IdBar.Equals(1001)
   select p);

 System.Diagnostics.Trace.Write(string.Format("SQL: {0}", ((System.Data.Objects.ObjectQuery)result).ToTraceString()));    
}
Esto nos devolverá en nuestra ventana de Resultados lo siguiente
SQL: SELECT 
[Extent1].[IdBar] AS [IdBar], 
[Extent1].[BarDesc] AS [BarDesc], 
[Extent1].[Valor] AS [Valor], 
[Extent1].[FC] AS [FC], 
[Extent1].[UC] AS [UC]
FROM [dbo].[Bar] AS [Extent1]
WHERE 1001 = [Extent1].[IdBar]
Algo bastante simple que nos puede ayudar bastante en algún momento.

Happy coding!