czwartek, 1 lipca 2010

Rejestrowanie zmian w bazie danych w Entity Framework

Tworząc bazę danych należy rozważyć problem bezpieczeństwa przechowywanych tam danych. Stosuje się tu różne metodyki: tworzenie użytkowników, ról, autoryzację, uwierzytelnianie użytkowników itp.

Jedną z istotniejszych rzeczy jest śledzenie zmian jakie dokonuje użytkownik podczas pracy z bazą danych. Ma to na celu nie tylko identyfikację celowego działania (np. w przypadku usuwania z bazy kluczowych klientów), ale także pomaga w przypadku kiedy potrzebujemy cofnąć się w przeszłość i zobaczyć starą wartość danej kolumny.

Entity Framework to ORM stworzony przez Microsoft. Działa on bardzo podobnie jak popularne ORMy NHibernate, LINQu itp. Dzięki narzędziom ORM możemy zaoszczędzić bardzo dużo czasu przeznaczonego na budowanie od podstaw warstwy dostępu do danych.

Tak więc przejdźmy do przykładu. Na potrzeby tego wpisu mam stworzoną prostą bazę danych:


Dwie tabele: Person - jedna z wielu tabel w naszej aplikacji; ApplicationLog - tą tabelą będziemy się zajmować w tym artykule. Tabela ta zawiera pola umożliwiające identyfikację użytkownika modyfikującego dane, datę i czas modyfikacji, rodzaj modyfikacji (dodanie, usunięcie, zmiana), starą i nową wartość (pozwalamy na nulle ponieważ nie zawsze obie będą wykorzystane, np. w przypadku usunięcia nie mamy pola nowe dane). Dla prostoty pola OldValue i NewValue są typu varchar(1000) w realnej aplikacji warto zastosować xml. Typ ten jest bardziej wszechstronny i umożliwi łatwiejszą pracę ze zwracanymi danymi.

Po stworzeniu bazy danych przechodzimy do Visuala i tworzymy nowy projekt np. WindowsForms (w innych projektach cały przebieg wygląda podobnie). Do projektu dodajemy ADO.NET Entity Data Model. Za pomocą Server Explorer dodajemy tabelę do naszego projektu:


Teraz czas na dodanie zasadniczej części odpowiedzialnej za logowanie zmian dokonywanych przez użytkowników. Na początek potrzebujemy jeszcze zmodyfikować plik Program.cs w który dodajemy właściwość UserName. Będzie ona przechowywała nazwę aktualnie zalogowanego użytkownika (w przypadku aplikacji internetowej ASP.NET nazwę użytkownika można przechować w np. sesji czy też pliku cookie itp):


 Cała metoda opiera się o obiekt ObjectStateManage, który jak możemy przeczytać na MSDN: "ObjectStateManager tracks query results, and provides logic to merge multiple overlapping query results. It also performs in-memory change tracking when a user inserts, deletes, or modifies objects, and provides the change set for updates."Czyli w naszym przypadku jest idealnym punktem startu. Jak zwykle istnieje kilka podejść do tworzenia kodu. Jednym podejściem jest dodanie kodu do pliku wygenerowanego przez EF drugim stworzenie własnej hierarchii klas rozszerzających metody zawarte w oryginalnym pliku wygenerowanym przez EF. Drugie podejście sprawia, że kod jest bardziej czytelny oraz można go w łatwy sposób zaimplementować w przyszłych projektach.
Tak więc stworzyłem sobie dwa pliki:


 
 W pliku EFExtension.cs rozszerzymy klasę wygenerowaną przez narzędzie EF. Klasa zawarta w pliku EntityLogging będzie odpowiedzialna za logowanie zmian dokonywanych na tabeli Person (oraz innych jeżeli dołożymy je do schematu).

Zawartość pliku EFExtension.cs:

namespace EntityFrameworkAuditLogging
{
    public partial class EFLoggingEntities
    {
        partial void OnContextCreated()
        {
            EntityLogging el = new EntityLogging(this);
        }
    }
}

Zawartość pliku EntityLogging.cs:

using System;
using System.Text;

namespace EntityFrameworkAuditLogging
{
    public class EntityLogging : IDisposable
    {
        private EFLoggingEntities _objCont;
        public EntityLogging(EFLoggingEntities objCont)
        {
            _objCont = objCont;
            //we'll catch moment when user save changes to db
            _objCont.SavingChanges += new EventHandler(_objCont_SavingChanges);
        }

        private void _objCont_SavingChanges(object sender, EventArgs e)
        {
            //we get all entities which were modified, insert or deleted
            foreach (var item in _objCont.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Added | System.Data.EntityState.Deleted | System.Data.EntityState.Modified))
            {
                //check if entity isn't our table for logging (we don't want to log any operation on this table
                if (!(item.Entity is ApplicationLog))
                {
                    EFLoggingEntities db = new EFLoggingEntities();
                    ApplicationLog log = new ApplicationLog();
                    log.DateTimeChanges = DateTime.Now;
                    log.UserName = Program.UserName;
                    log.KindOfChanges = item.State.ToString();
                    log.TableName = item.EntitySet.Name;
                    StringBuilder sb = new StringBuilder();

                    //StringBuilder is faster than using simple concatenate 
                    sb.Clear();
                    //if we add new entity, we don't have old values
                    if (item.State == System.Data.EntityState.Added)
                    {
                        for (int i = 0; i < item.CurrentValues.FieldCount; i++)
                        {
                            sb.Append(item.CurrentValues[i].ToString() + " ");
                        }
                        log.NewValue = sb.ToString();
                    }
                    //if we delete entity, we don't have new value
                    if (item.State == System.Data.EntityState.Deleted)
                    {
                        for (int i = 0; i < item.OriginalValues.FieldCount; i++)
                        {
                            sb.Append(item.OriginalValues[i].ToString() + " ");
                        }
                        log.OldValue = sb.ToString();
                    }
                    //when we modyfing entity we have both old and new value
                    if (item.State == System.Data.EntityState.Modified)
                    {
                        for (int i = 0; i < item.OriginalValues.FieldCount; i++)
                        {
                            sb.Append(item.OriginalValues[i].ToString() + " ");
                        }
                        log.OldValue = sb.ToString();
                        sb.Clear();
                        for (int i = 0; i < item.CurrentValues.FieldCount; i++)
                        {
                            sb.Append(item.CurrentValues[i].ToString() + " ");
                        }
                        log.NewValue = sb.ToString();

                    }

                    db.AddToApplicationLog(log);
                    db.SaveChanges();
                }
            }
        }

        public void Dispose()
        {
            if (this._objCont != null)
            {
                _objCont.SavingChanges -= new EventHandler(_objCont_SavingChanges);
                _objCont = null;
            }
        }
    }
}


Następnie tworzymy przykładowe rekordy w bazie danych, oraz formatkę do obsługi danych oraz podglądu logu:




Do pliku formatki obsługującej wyświetlanie danych i ich modyfikację wprowadzamy kod:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace EntityFrameworkAuditLogging
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            Program.UserName = "Kowalski";
        }

        private void button1_Click(object sender, EventArgs e)
        {
            EFLoggingEntities db = new EFLoggingEntities();
            var q = from a in db.Person
                    select a;
            dataGridView1.DataSource = q;
        }

        private void dataGridView1_RowEnter(object sender, DataGridViewCellEventArgs e)
        {
            textBox1.Text = dataGridView1[1, e.RowIndex].Value.ToString();
            textBox2.Text = dataGridView1[2, e.RowIndex].Value.ToString();
            textBox3.Text = ((DateTime)dataGridView1[3, e.RowIndex].Value).ToShortDateString();
        }

        private void bInsert_Click(object sender, EventArgs e)
        {
            Person p = new Person
            {
                FirstName = textBox1.Text,
                LastName = textBox2.Text,
                BirthDate = DateTime.Parse(textBox3.Text)
            };

            EFLoggingEntities db = new EFLoggingEntities();
            db.AddToPerson(p);
            db.SaveChanges();
            button1_Click(this, e);
        }

        private void bDelete_Click(object sender, EventArgs e)
        {
            DataGridViewRow row = dataGridView1.SelectedRows[0];
            int idToRemove = (int)row.Cells[0].Value;
            EFLoggingEntities db = new EFLoggingEntities();
            Person q = (from a in db.Person
                    where a.IdPerson == idToRemove
                    select a).First();

            db.Person.DeleteObject(q);
            db.SaveChanges();
            button1_Click(this, e);
        }

        private void bUpdate_Click(object sender, EventArgs e)
        {
            DataGridViewRow row = dataGridView1.SelectedRows[0];
            int idToRemove = (int)row.Cells[0].Value;
            EFLoggingEntities db = new EFLoggingEntities();
            Person q = (from a in db.Person
                        where a.IdPerson == idToRemove
                        select a).First();

            q.FirstName = textBox1.Text;
            q.LastName = textBox2.Text;
            q.BirthDate = DateTime.Parse(textBox3.Text);
            db.SaveChanges();
            button1_Click(this, e);
        }

        private void bViewLog_Click(object sender, EventArgs e)
        {
            Log log = new Log();
            log.ShowDialog(this);
        }
    }
}

Dla formatki wyświetlającej log:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace EntityFrameworkAuditLogging
{
    public partial class Log : Form
    {
        public Log()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            this.Close();
        }

        private void Log_Load(object sender, EventArgs e)
        {
            EFLoggingEntities db = new EFLoggingEntities();
            var q = from a in db.ApplicationLog
                    select a;

            dataGridView1.DataSource = q;
        }
    }
}

Mając wprowadzony kod możemy testować stworzone przez nas rozwiązanie:





Jak widać zmiana Sebastian na Filip została automatycznie zapisana w naszym logu. 

Teraz zobaczymy jeszcze co się stanie jeżeli do schematu dorzucimy kolejną tabelę:





Jak więc widać po dodaniu nowej tabeli nic nie musimy robić aby nasz system nadal działał poprawnie i wykonywał powierzone mu zadanie w przypadku modyfikacji schematu bazy danych.

Projekt można pobrać stąd.

Źródło:
http://msdn.microsoft.com/en-us/library/system.data.objects.objectstatemanager.aspx
http://www.codeproject.com/KB/database/EF4_Adventures.aspx
http://www.codeproject.com/KB/database/ImplAudingTrailUsingEFP1.aspx

Brak komentarzy:

Prześlij komentarz