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

No hay comentarios:

Publicar un comentario