Ir al contenido principal

Entity Framework Code First

Seguro que todos han oído hablar de los conceptos Database First y Code First. Así como de los ORM's tales como Entity Framework.

Imaginarán que no les voy a proponer aquí una conferencia acerca de estos asuntos ya que existe abundante documentación en la web.

Los menciono porque, para que puedan Uds. disfrutar de esta plataforma de pruebas, he implementado un sistema mediante el cual, en la primera ejecución se detecta si existe ya una base de datos, y si no existe la crea en tiempo de ejecución, y la alimenta con los datos de los Test, sus TestCases y sus categorías.

La cosa es que lo he implementado de un modo artesanal no demasiado óptimo que procederé a mostrarles, ya que no carece de ciertos aspectos técnicos interesantes. No obstante Pepito -el arquitecto de software que vive en mi cabeza- me ha prohibido terminantemente seguir adelante con esta estrategia, obligándome a redefinir el proyecto con Entity Framework CodeFirst, implementación que será como no un gusto compartir con Uds lectores.

Prepárense porque este va a ser un post denso y repleto de curiosidades técnicas. Al lío:

PASO 1: Usando el registro de Windows

Este es un asunto algo farragoso y delicado. Les recomiendo hacer poco uso de esta opción que puede darles no pocos quebraderos de cabeza. Yo aun así me enfrentaré a ello con la finalidad de detectar la primera ejecución de la plataforma en un cliente. Veámoslo.

Lo primero es que en el evento Load del formulario principal verificaré si la base de datos existe. No comprobaré realmente la existencia de la base de datos, tan solo leeré una clave en el registro de Windows. En caso de que exista con valor positivo, daré por sentado que la base de datos ya se creó con anterioridad, y en caso contrario abriré un formulario modal donde procederemos a la creación de la misma en tiempo de ejecución.

        private void frmMonitor_Load(object sender, EventArgs e)
        {
            registry_functions _reg = new registry_functions();

            bool existeBBDD = _reg.ExisteBBDD();

            //primero comprobamos si existe la BBDD
            if (!existeBBDD)
            {
                frmStart _frm = new frmStart(this);
                _frm.ShowDialog();
            }
            else
            {
                GetCategories();
            }
        }

En este punto, entremos de lleno en la clase que implementé para manipular el registro de Windows, y comentemos sus método:

    public class registry_functions
    {
        public bool ExisteBBDD()
        {
            RegistryKey rk1 = Registry.LocalMachine;

            RegistryKey rkSoftware = rk1.OpenSubKey("SOFTWARE", true);

            RegistryKey rk_enigma = rkSoftware.OpenSubKey("EnigmaSoft", true);

            if (rk_enigma == null)
            {
                RegistryKey rk2 = rkSoftware.CreateSubKey("EnigmaSoft");

                rk2.SetValue("BBDDCreated", "N", RegistryValueKind.String);

                return false;
            }
            else
            {
                string valor = rk_enigma.GetValue("BBDDCreated").ToString();

                if (valor != "Y")
                {
                    return false;
                }
                else
                {
                    return true;
                }
            }
        }

        public bool SetBBDDCreada(string Server)
        {
            try
            {
                RegistryKey rk1 = Registry.LocalMachine;

                RegistryKey rkSoftware = rk1.OpenSubKey("SOFTWARE", true);

                RegistryKey rk_enigma = rkSoftware.OpenSubKey("EnigmaSoft", true);

                rk_enigma.SetValue("BBDDCreated", "Y", RegistryValueKind.String);
                rk_enigma.SetValue("Server", Server, RegistryValueKind.String);

            }
            catch (Exception ex)
            {
                return false;
            }

            return true;
        }

        public string GetRegisteredServer()
        {
            string res = "";

            try
            {
                RegistryKey rk1 = Registry.LocalMachine;

                RegistryKey rkSoftware = rk1.OpenSubKey("SOFTWARE", true);

                RegistryKey rk_enigma = rkSoftware.OpenSubKey("EnigmaSoft", true);

                if (rk_enigma == null)
                {
                    res = "";
                }
                else
                {
                    res = rk_enigma.GetValue("Server").ToString();
                }
            }
            catch (Exception ex)
            {

            }

            return res;
        }
    }

Bien, el primer método llamado ExisteBBDD navega a través de la estructura de registro que he definido para almacenar las claves BBDDCreated y Server. Si no existe esta estructura devuelve false, y además la crea (la estructura), con un valor "N" en la clave BBDDCreated.

El método SetBBDDCreada navega en esta misma estructura (que ya debe estar creada por el llamado al método anterior), y establece el valor de la clave BBDDCreated a "Y", de modo que en la segunda y posteriores ejecuciones de la plataforma no repetirá el proceso de creación. Además guardaremos el valor del servidor SQL Server que previamente le solicitamos al usuario. Este valor lo usaremos para montar la cadena de conexión.

A este método llamaremos cuando tengamos confirmación de que la base de datos se creó efectivamente.

Y por último, el método getRegisteredServer nos servirá para recuperar el valor almacenado en la clave Server del registro.

En la siguiente captura ven como quedaría nuestra estructura mediante el programa regedit:



Un dato importante para manipular el registro de Windows: para acceder al registro es preciso que la aplicación disponga de privilegios de administrador. Para este fin debemos, en primer lugar, acceder a Windows con un usuario que sea administrador de la máquina, obvio. Pero además, la aplicación en si debe correr con dichos permisos de administración.

Para que la aplicación le solicite automáticamente al usuario estos privilegios al arrancar debemos hacer lo siguiente: primero, añadir un manifiesto a la aplicación. Esto se hace añadiendo un nuevo elemento al proyecto de tipo "archivo de manifiesto de aplicación" tal como muestro en la siguiente captura:


Y a continuación buscamos el tag "RequestedExecutionLevel" y modificamos su valor por "requireAdministrator":

<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />

PASO 2: Creando la base de datos

Veamos ahora el procedimiento que usé para crear la base de datos en tiempo de ejecución. Quiero insistir en que no es la forma idónea de hacerlo, y que será en breve sustituida.

Primeramente, tras la llamada al método ExisteBBDD, si obtengamos un valor negativo abriremos el formulario frmStart, lo cual debería producirse exclusivamente en la primera ejecución de la plataforma.


Aquí solicitamos al usuario el nombre de su propio servidor SQL Server. Y al pulsar el botón Crear BBDD, se hace la magia. veamos el código:

        private void cmdEmpezar_Click(object sender, EventArgs e)
        {
            cmdCancelar.Enabled = false;
            cmdEmpezar.Enabled = false;

            this.Cursor = Cursors.WaitCursor;

            //data_functions _df = new data_functions();
            //_df.CreateDatabaseEF();

            if (txtServer.Text.Trim().Length > 2)
            {
                data_functions _df = new data_functions();

                if (_df.TestMasterDB(txtServer.Text.Trim()))
                {
                    List<data_object> _files = _df.GetFilesPath(txtServer.Text.Trim());

                    if (_files.Count >= 2)
                    {
                        if (_df.CreateDatabase(txtServer.Text.Trim(), _files))
                        {
                            registry_functions _reg = new registry_functions();

                            if (_reg.SetBBDDCreada(txtServer.Text.Trim()))
                            {
                                _container.GetCategories();
                                this.Close();
                            }
                            else
                            {
                                MessageBox.Show("Error al registrar");
                            }
                        }
                        else
                        {
                            MessageBox.Show("No se ha podido crear la base de datos");
                        }
                    }
                    else
                    {
                        MessageBox.Show("conexión OK. No se encuentran los ficheros de datos");
                    }
                }
                else
                {
                    MessageBox.Show("Error");
                }
            }
            else
            {
                MessageBox.Show("Introduzca el nombre del servidor SQL Server");
            }

            cmdCancelar.Enabled = true;
            cmdEmpezar.Enabled = true;

            this.Cursor = Cursors.Default;
        }

Bueno, sencillo hasta aquí. Verifico que el usuario introdujo un nombre de servidor, y hago tres llamadas a métodos contenidos en la clase data_functions, dentro del proyecto de capa de datos.

La primera llamada es a una función llamada TestMasterDB, que verifica que la conexión al servidor especificado es válida, realizando una conexión a la base de datos master.

Si todo va bien sabemos ya que el servidor es válido y el usuario tiene privilegios para acceder, de modo que llamamos al método GetFilesPath. Este método me devolverá dos rutas, las correspondientes a los ficheros de DATOS (.mdf) y de LOG (.ldf) usados por el servidor SQL Server. Enseguida veremos el código de estos métodos.

La tercera llamada es al método CreateDatabase, que montará definitivamente la base de datos. Si todo esto funciona bien, volvemos al registro para establecer la clave BBDDCreated a "Y" y guardar en la clave Server el nombre del servidor para usos posteriores.

Veamos ahora la clase data_functions:

    public class data_functions
    {
        const string DBMaster = "master";
        private string DBLabs;

        public data_functions()
        {
            DBLabs = ConfigurationManager.AppSettings["DBLABS"].ToString();
        }

        private List<string> lstFicheros = new List<string>() { "getCategories",
                                                                "getExecutions",
                                                                "getTestCases",
                                                                "getTests",
                                                                "insertExecution",
                                                                "insertTest",
                                                                "insertTestCase" };

        public string GetLabsCnx()
        {
            registry.registry_functions _regfunc = new registry.registry_functions();

            string server = _regfunc.GetRegisteredServer();
            string res = GetCnx(server, DBLabs);

            return res;
        }

        public bool TestMasterDB(string Server)
        {
            string cnxstr = GetCnx(Server, DBMaster);

            bool res = data_labs.Test(cnxstr);

            return res;
        }

        public List<data_object> GetFilesPath(string Server)
        {
            string cnx_str = GetCnx(Server, "master");

            List<data_object> _files = data_labs.GetFilesPath(cnx_str);

            foreach (data_object _do in _files)
            {
                char chr = '\\';
                int index = _do.Path.LastIndexOf(chr);

                string ruta = _do.Path.Substring(0, index+1);

                _do.Path = ruta;
            }

            return _files;
        }

        public bool CreateDatabase(string Server, List<data_object> Files)
        {
            bool res = true;

            try
            {
                string cnx_str_master = GetCnx(Server, DBMaster);
                string cnx_str_labs = GetCnx(Server, DBLabs);

                TextReader tr = new StreamReader(@"sqlfiles\createdatabase.txt");
                string script = tr.ReadToEnd();

                string rutaDatos = Files.Where(tp => tp.FileType == data_object.enumFileType.data).First().Path;
                string rutaLog = Files.Where(tp => tp.FileType == data_object.enumFileType.log).First().Path;

                script = script.Replace("##RUTADATOS##", rutaDatos).Replace("##RUTALOG##", rutaLog);

                res = data_labs.ExecScript(script, cnx_str_master);

                if (res)
                {
                    res = InitializeTables(Server);

                    if (res)
                    {
                        bool resProcedimientos = true;

                        foreach (string _file in lstFicheros)
                        {
                            TextReader txtProcedure = new StreamReader(@"sqlfiles\" + _file + ".txt");
                            string scriptProcedure = txtProcedure.ReadToEnd();

                            resProcedimientos = data_labs.ExecScript(scriptProcedure, cnx_str_labs);

                            if (!resProcedimientos)
                            {
                                return false;
                            }
                        }
                    }
                }
            }

            catch (Exception ex)
            {
                res = false;
            }

            return res;
        }

        public bool CreateDatabaseEF()
        {
            bool res = true;

            try
            {
                ZMLabsData.Migrations.Configuration _confDB = new ZMLabsData.Migrations.Configuration();
                _confDB.CreateDataBase(false);
            }

            catch (Exception ex)
            {
                res = false;
            }

            return res;
        }

        //-->> Privados

        private bool InitializeTables(string Server)
        {
            bool res = true;

            try
            {
                string cnx_str = GetCnx(Server, DBLabs);

                TextReader tr = new StreamReader(@"sqlfiles\InitializeTables.txt");
                string script = tr.ReadToEnd();

                res = data_labs.ExecScript(script, cnx_str);

                if (res)
                {
                    res = data_labs.InitializeTables(cnx_str);
                }
            }

            catch (Exception ex)
            {
                res = false;
            }

            return res;
        }

        private string GetCnx(string Server, string DB)
        {
            string cnx = ConfigurationManager.ConnectionStrings["cnxLABS_DB_STR"].ConnectionString;

            cnx = cnx.Replace("##DB##", DB).Replace("##SERVER##", Server);

            return cnx;
        }

Como no quiero extenderme demasiado les diré que en esencia lo que hace esta clase es ejecutar unos scripts SQL que tengo previamente codificados y guardados en el proyecto en formato .txt.

El primero ataca a la base de datos master y ejecuta un Create Database sustituyendo los valores ##RUTADATOS## y ##RUTALOG## por los valores que obtuvimos previamente llamando al método GetFilesPath:



        public static List<data_object> GetFilesPath(string cnx_str)
        {
            List<data_object> res = new List<data_object>();

            SqlConnection cnx = new SqlConnection(cnx_str);
            SqlCommand cmd = new SqlCommand();

            cmd.Connection = cnx;
            cmd.CommandType = CommandType.StoredProcedure;
            cmd.CommandText = "sp_helpfile";

            cnx.Open();

            SqlDataReader reader = cmd.ExecuteReader();

            while (reader.Read())
            {
                data_object _do = new data_object();

                if (reader["usage"].ToString() == "data only")
                {
                    _do.FileType = data_object.enumFileType.data;
                }
                else if (reader["usage"].ToString() == "log only")
                {
                    _do.FileType = data_object.enumFileType.log;
                }

                _do.Path = reader["filename"].ToString();

                res.Add(_do);
            }

            cnx.Close();

            return res;
        }

Tengan en cuenta que para que en tiempo de ejecución estos ficheros de texto guardados en la solución se encuentren en la ruta relativa indicada, hay que ir a las propiedades de cada uno de los ficheros, y en la propiedad "copiar en el directorio de salida" cambiar su valor por defecto por "copiar siempre".


Ya hemos creado una base de datos vacía. Lo siguiente que hace el método CreatedataBase es llamar al método InitializeTables que no hace sino ejecutar otro script que en este caso ataca a la recién creada base de datos, para montar la estructura de tablas y llenarlas de datos:


Finalmente creamos los procedimientos almacenados, usando la misma técnica de ejecución de Scripts.

Y bueno, no voy a extenderme más porque, como ya les dije, todo esto quedará desde ya en desuso, aunque me pareció interesante mostrarles esta solución, digamos "imaginativa", que efectivamente funciona.

¿Qué inconvenientes presenta? Muchos. El más inmediato es que el mantenimiento de los cambios sería una cuestión harto engorrosa. Por esta razón vamos a ver cómo podríamos resolver este asunto de un modo más elegante, eficiente y rápido: utilizando  Entity Framework Code First. Igualmente creo interesante dejar todo este código en el proyecto (Enigma Software - ZM LABS) por si desean revisarlo.

Paso 2.1. La capa de acceso a datos

Tenemos una base de datos creada en tiempo de ejecución, con tablas, datos y procedimientos almacenados. Ésta ha sido creada en el servidor que nos indicó el usuario. Como es natural, para acceder a esta base de datos no podemos, en el Web.Config, crear una cadena de conexión estática como sería habitual. Necesitamos que al menos la propiedad Server de la cadena de conexión sea dinámica:

    <add name="cnxLABS_DB_STR_EF" 
         connectionString="Persist Security Info=False;Integrated Security=true;Initial Catalog=EnigmaLABS_EF;Server=##SERVER##"
         providerName="System.Data.SqlClient" />

Por consiguiente en nuestra capa de acceso a datos admitiremos, en su constructor, un parámetro que indique la cadena de conexión ya con el valor Server adecuado.


Y la sustitución del valor ##SERVER## se realizará en el constructor de las clases de negocio, accediendo al registro de Windows.



        public string GetLabsCnx()
        {
            registry.registry_functions _regfunc = new registry.registry_functions();

            string server = _regfunc.GetRegisteredServer();
            string res = GetCnx(server, DBLabs);

            return res;
        }

PASO 3: Recodificarlo todo usando Entity Framework Code First

No quisiera enredarme mucho en lo que es la instalación desde Nuget de Entity Framework, ya que podrán encontrar abundantes y detallados artículos sobre el asunto en la web. Me limitaré a recomendarles este videito explicativo: Ef Code First - First Steps

Si vieron el vídeo, comprobarán que un uso corriente de Code First pasa por crear la base de datos a partir de un modelo en tiempo de diseño, mediante la consola de comandos Nuget, ejecutando el comando enable-migration, update-database, etc.. Decirles que no es nuestro caso, ya que lo que queremos nosotros es crear la base de datos en tiempo de ejecución. Veamos cómo.

Bien pues, volvamos a los inicios. Voy a mantener la funcionalidad del registro de Windows para evaluar si la base de datos fue ya creada o por el contrario, se debe desencadenar el proceso de creación de la base de datos.

Ahora al pulsar en el botón Crear BBDD del formulario frmStart incorporo una cláusula SWITCH para redirigir al método de creación de base de datos con Entity Framework.

        private void cmdEmpezar_Click(object sender, EventArgs e)
        {
            cmdCancelar.Enabled = false;
            cmdEmpezar.Enabled = false;

            this.Cursor = Cursors.WaitCursor;

            switch (MetodoCreacionBBDD)
            {
                case enumMetodoCreacion.Scrpts:

                    CrearMedianteScripts();
                    break;

                case enumMetodoCreacion.EF:

                    CrearMedianteEF();
                    break;
            }

            cmdCancelar.Enabled = true;
            cmdEmpezar.Enabled = true;

            this.Cursor = Cursors.Default;
        }

En el método de CrearMedianteEF() mantendremos la funcionalidad que testea el servidor mediante una conexión a la base de datos master.

Aparte de eso, lo que viene a continuación se hará de modo bien distinto.

        private void CrearMedianteEF()
        {
            if (txtServer.Text.Trim().Length > 2)
            {
                data_functions _df = new data_functions();

                if (_df.TestMasterDB(txtServer.Text.Trim()))
                {
                    if (_df.CreateDatabaseEF(txtServer.Text.Trim()))
                    {
                        registry_functions _reg = new registry_functions();

                        if (_reg.SetBBDDCreada(txtServer.Text.Trim()))
                        {
                            _container.GetCategories();
                            this.Close();
                        }
                        else
                        {
                            MessageBox.Show("Error al registrar");
                        }
                    }
                    else
                    {
                        MessageBox.Show("Error al crear la base de datos");
                    }
                }
                else
                {
                    MessageBox.Show("El servidor no responde");
                }
            }
            else
            {
                MessageBox.Show("Introduzca el nombre del servidor SQL Server");
            }
        }

Una vez tenemos confirmada la conexión al servidor llamamos al método CreateDatabaseEF que les muestro a continuación:

        public bool CreateDatabaseEF(string Server)
        {
            bool res = true;

            try
            {
                ZMLabsData.Migrations.Configuration _confDB = new ZMLabsData.Migrations.Configuration();
                _confDB.CreateDataBase(false, GetCnxEF(Server));

                //Crea los procedimientos almacenados
                bool resProcedimientos;

                foreach (string _file in lstFicheros)
                {
                    TextReader txtProcedure = new StreamReader(@"sqlfiles\" + _file + ".txt");
                    string scriptProcedure = txtProcedure.ReadToEnd();

                    resProcedimientos = data_labs.ExecScript(scriptProcedure, GetCnxEF(Server));

                    if (!resProcedimientos)
                    {
                        return false;
                    }
                }
            }

            catch (Exception ex)
            {
                res = false;
            }

            return res;
        }

Como ven en primer término instanciamos la clase Migrations.Configuration. Esta clase la crea  Entity Framework al habilitar las migraciones automáticas,y sirve entre otras cosas para realizar la carga inicial de datos.

Yo además he incorporado en la misma el método CreateDatabase, al que le paso la cadena de conexión ya bien construida con el Server informado por el usuario.

Veamos la clase Migrations.Configuration:

    public sealed class Configuration : DbMigrationsConfiguration<ZMLabsData.context.LabsContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
        }

        public void CreateDataBase(bool Recreate, string cnx_str)
        {
            context.LabsContext _myContext = new context.LabsContext(cnx_str);

            _myContext.Database.Create();

            Seed(_myContext);   
        }

        protected override void Seed(context.LabsContext context)
        {
            //Categorías

            //Ramas de nivel 1
            EFModels.Categories _cat_sqlserver = new EFModels.Categories()
            {
                id = 1,
                Categorie = "SQL Server Tips"
            };

            EFModels.Categories _cat_csharpr = new EFModels.Categories()
            {
                id = 2,
                Categorie = "C# Tips"
            };

            context.Categories.AddOrUpdate(x => x.id, _cat_sqlserver);
            context.Categories.AddOrUpdate(x => x.id, _cat_csharpr);

            //Ramas de nivel 2
            EFModels.Categories _cat_Multithreading = new EFModels.Categories()
            {
                id = 3,
                Categorie = "Multithreading Tests",
                Categorie_dad = _cat_csharpr
            };

            EFModels.Categories _cat_Basics = new EFModels.Categories()
            {
                id = 4,
                Categorie = "Basics Tips",
                Categorie_dad = _cat_csharpr
            };

            context.Categories.AddOrUpdate(x => x.id, _cat_Multithreading);
            context.Categories.AddOrUpdate(x => x.id, _cat_Basics);

            context.SaveChanges();

            //Tests
            etc... etc ...
        }
    }

Lo más reseñable aquí es que el método CreateDatabase tiene acceso al contexto (context) de base de datos de Entity Framework de tal suerte que podemos realizar acceso a nuestras entidades, pre-definidas en los modelos. El parámetro Recreate, que como ven no estoy utilizando todavía, servirá para realizar actualizaciones de objetos. Como en este punto nos estamos encargando de la creación y carga inicial le pasaremos un valor false.

El método Seed lo crea igualmente de modo automático E.F. para realizar la carga inicial de datos, recomendando usar en su contenido el método AddOrUpdate, para el caso de que se ejecute no solo en la creación, sino también en las actualizaciones posteriores.

Vamos a ver nuestro context y nuestros modelos, herramientas que en combinación usará E.F. para definir y materializar un modelo entidad-relación:

namespace ZMLabsData.context
{
    public class LabsContext : DbContext
    {
        public LabsContext(string cnx_str) : base(cnx_str)  {  }

        public DbSet<EFModels.Categories> Categories { get; set; }
        public DbSet<EFModels.Tests> Test { get; set; }
        public DbSet<EFModels.TestCases> TestCases { get; set; }

    }
}

Aquí en el context definimos qué modelos van a participar en nuestros accesos a datos. Cada uno de estos modelos se convertirá en una tabla. Para que E.F. reconozca esta clase como contexto no tiene más que heredar la clase DbContext.

A continuación les mostraré los modelos que están participando del contexto. Conviene recordar cual es el modelo de datos que deseamos obtener como resultado:

En el context aun me falta incorporar la entidad Executions. De momento haremos la prueba con Categorías, Test y TestCases.

Aquí les muestro los modelos:

public class Categories
{
 public int id { get; set; }

 [Required]
 [StringLength(255)]
 public string Categorie { get; set; }

 public Categories Categorie_dad { get; set; }
}

public class Tests
{
 public Int64 id { get; set; }

 [Required]
 [StringLength(255)]
 public string Test { get; set; }

 [StringLength(555)]
 public string Description { get; set; }

 [StringLength(255)]
 public string Classname { get; set; }

 [StringLength(555)]
 public string Url_Blog { get; set; }

 [StringLength(555)]
 public string Url_Git { get; set; }

 [Required]
 public Categories Categorie { get; set; }
}

public class TestCases
{
 public Int64 id { get; set; }

 [Required]
 [StringLength(255)]
 public string Function { get; set; }

 [StringLength(555)]
 public string Description { get; set; }

 [Required]
 public EFModels.Tests Test { get; set; }
}

Bien, pues volviendo a la clase Migrations.Configuration, ven que el contexto tiene un método Database.Create(). Éste lo que hace es recorrer los modelos especificados en el context, y crea tanto la base de datos -atendiendo a la cadena de conexión del context- como el modelo de datos (las tablas) atendiendo a la definición de los modelos.

Acto seguido llamamos al método Seed() que realiza la carga de los datos, y ¡voilá! Base de datos creada y alimentada.

¿Y los procedimientos almacenados? Habrán observado, lectores avezados, que he mantenido el sistema de creación de procedimientos almacenados basado en scripts sql. Esto lo he hecho así para comprobar fácilmente que todo ha funcionado bien, ya que al ejecutar la aplicación me sigue mostrando el árbol de test y categorías, y al clicar en un test me sigue mostrando la información (descripción, testcases....) correctamente.

Pero este es un asunto que no termina aquí. A raíz de haber adoptado la estrategia Code First nos veremos en la obligación de replantear también nuestra capa de acceso a datos.

Paso 3.1. La capa de acceso a datos

Antes de que Pepito Saltamontes venga a echarme otra tremenda bronca, me planteo lo siguiente: si he empezado a utlizar E.F. Code First como estrategia de acceso a datos, deberé llevar esta decisión hasta sus últimas consecuencias. Esto es, no usar E.F. solo para crear la base de datos (que podría), sino también para realizar los accesos a la misma.

Para ello se me plantean dos cambios importantes. Uno tendrá lugar en la propia capa de datos, donde como es lógico sustituiré en acceso tradicional mediante ADO.NET por el acceso vía Entity Framework. Esto lo haremos creando repositorios para nuestros modelos.

Una segunda problemática que se nos plantea es la siguiente: en nuestra estrategia anterior la capa de datos estaba devolviendo entidades de dominio, que eran manualmente alimentadas contra la información devuelta por un DataReader. Al cambiar el acceso a los datos, la información devuelta por base de datos va a quedar almacenada en los modelos.

En este post, que ya ha sido suficientemente extenso, vamos a abordar tan solo la primera problemática, y les emplazo a seguir leyendo próximas publicaciones, en una de las cuales abordaremos esta interesante cuestión arquitectónica.

Vamos con los repositorios: este asunto es harto sencillo. Se trata de crear una alternativa para cada uno de los métodos de la clase data_test, que son getCategories, getTests, insertTest, insertTestCase, InsertExecution y getTestCases.

Ilustraremos como cambiar un par de método de tipo Get, por ejemplo getCategories y getTests, y otro de tipo Insert, pongamos por caso insertTest.

Veamos el método getCategories implementado con ADO.NET:

        public List<Categories> getCategories()
        {
            List<Categories> res = new List<Categories>();

            try
            {
                SqlConnection cnx = new SqlConnection(str_cnx);
                SqlCommand cmd = new SqlCommand();

                cmd.Connection = cnx;
                cmd.CommandType = CommandType.StoredProcedure;
                cmd.CommandText = "GetCategories";

                cnx.Open();

                SqlDataReader reader = cmd.ExecuteReader();

                while (reader.Read())
                {
                    Categories _cat = new Categories()
                    {
                        id = int.Parse(reader["idCategorie"].ToString()),
                        Categorie = reader["Categorie"].ToString()
                    };

                    if (reader["idCategorieNode"] != DBNull.Value)
                    {
                        _cat.Categorie_dad = new Categories()
                        {
                            id = int.Parse(reader["idCategorieNode"].ToString()) 
                        };
                    }

                    res.Add(_cat);
                }

                cnx.Close();
            }
            catch (Exception ex)
            {

            }

            return res;
        }

Se llama a un procedimiento almacenado, y el resultado devuelto por un dataReader se mapea contra la entidad de dominio Categories.

Y el procedimiento no hace sino devolverme los datos de la tabla tal cual:

ALTER procedure [dbo].[getCategories]
as
begin
 select id as idCategorie, Categorie, categorie_dad_id as idCategorieNode 
 from
 [dbo].[Categories]
end

Ahora vamos a ver cuál sería la alternativa con E.F. Como Uds. sabrán todo modelo definido en un context es inmediatamente accesible con E.F. a través de un repository. de lo que se desprende que acceder a los datos de la tabla Categories será algo tan sencillo como ésto:

        public List<EFModels.Categories> getCategories(string cnx_str)
        {
            using (var db = new context.LabsContext(cnx_str))
            {
                return db.Categories.ToList();
            }
        }

Sencillo, ¿verdad? Observen la problemática que antes les comentaba. Mientras la opción con ADO nos devuelve una lista de objetos de tipo Categories, la alternativa con E.F. nos está devolviendo un modelo de tipo EFModels.Categories. Como les dije, lidiaremos con esto en posteriores publicaciones.

Quiero ahora mostrarles la recodificación del método GeTests por sus perculiaridades con respecto al método anterior. Observemos primero el método implementado con ADO.NET:

        public List<test_object> getTests()
        {
            List<test_object> res = new List<test_object>();

            try
            {
                SqlConnection cnx = new SqlConnection(str_cnx);
                SqlCommand cmd = new SqlCommand();

                cmd.Connection = cnx;
                cmd.CommandType = CommandType.StoredProcedure;
                cmd.CommandText = "getTests";

                cnx.Open();

                SqlDataReader reader = cmd.ExecuteReader();

                while (reader.Read())
                {
                    test_object _test = new test_object()
                    {
                        id = int.Parse(reader["idTest"].ToString()),
                        Test = reader["Test"].ToString(),
                        Classname = reader["ClassName"].ToString(),
                        Description = reader["Description"].ToString(),
                        Url_blog = reader["Url_Blog"].ToString(),
                        Url_git = reader["Url_GIT"].ToString(),

                        Categorie = new Categories()
                        {
                            id = int.Parse(reader["idCategorie"].ToString()),
                            Categorie = reader["Categorie"].ToString()
                        }
                   };

                    res.Add(_test);
                }

                cnx.Close();

                foreach (test_object _test in res)
                {
                    _test.Execution.testcases = getTestCases(_test.id);
                }
            }
            catch (Exception ex)
            {

            }

            return res;
        }

Aquí vemos que el procedimiento no devuelve solo los datos propios del test como su descripción. También devuelve la propiedad Categorie, que es a su vez una clase. Por último, por cada test devuelto llama a un procedimiento almacenado más que obtiene los testcases asociados al test.

Veremos que con la instrucción Include de Entity Framework estas operaciones se realizan de modo mucho más sencillo. Pero antes vemos el procedimiento almacenado GetTests para observar cómo a través de un INNER JOIN obtenemos la información tanto de la entidad Test como de su categoría relacionada:

ALTER procedure [dbo].[getTests]
as
begin
 select t.id as idTest, t.Test, t.ClassName, t.[Description], t.Url_Blog, t.Url_GIT, t.Categorie_id as idCategorie, c.Categorie
 from 
 [dbo].[Tests] t
 inner join Categories c on t.Categorie_id = c.id
end

Veamos ya como haríamos un método equivalente con E.F. Tan sencillo como ésto:

        public List<test_object> getTests()
        {
            List<test_object> res = new List<test_object>();

            using (var db = new context.LabsContext(_str_cnx))
            {
                var res_model = db.Test.Include(tc => tc.TestCases).Include(c => c.Categorie).ToList();
            }

            return res;
        }

No tenemos más que indicar qué entidades relacionadas con la principal (Test) queremos incluir. E.F. hace el resto. Les recuerdo que para poder usar las expresiones lambda en la cláusula Include (absolutamente recomendable) es preciso incluir la librería System.Data.Entity

Por último veremos el ejemplo de creación de un nuevo registro en la tabla Test. Como imaginan, con ADO.NET lo hacíamos mediante procedimiento almacenado, al cual por parámetro le pasábamos los valores de la clase Test. Esto ya no será necesario. Vean el nuevo método insertTest:

        public bool insertTest(test_object Test)
        {
            try
            {
                EFModels.Tests _testmodel = mapper.Map<EFModels.Tests>(Test);

                using (var db = new context.LabsContext(_str_cnx))
                {
                    db.Test.Add(_testmodel);
                    db.SaveChanges();
                }
            }
            catch (Exception ex)
            {
                return false;
            }

            return true;
        }

Aquí ya se ven algunas pistas acerca de nuestra problemática con las entidades de dominio y los modelos, que abordaremos más adelante.

Si han llegado hasta aquí, ¡enhorabuena! Creo que ha quedado un post bien completo. Como siempre, les animo a seguir leyendo y aprendiendo. También a dejarme sus comentarios y propuestas para la plataforma.

Saben que disponen del proyecto íntegro en GitHub (Enigma Software - ZM LABS)

Suerte y gracias por estar ahí. Saludos.




Comentarios

Entradas populares de este blog

Test 3: ¿Son eficientes los ORMs?

Bienvenidos amigos. Me complace anunciar que por fin estrenamos la categoría " SQL Server Tips ", y lo hacemos por todo lo alto, entrando de lleno en un aspecto altamente polémico entre programadores. ¿Es eficiente un ORM en los accesos a datos? Ya conocen la filosofía de nuestros Tests, no vamos a teorizar demasiado, pero sí una pequeña base va a ser necesaria para conseguir una buena respuesta a nuestra pregunta. He leído un interesante  artículo de nuestros súper amigos de Deloitte ( cuando usar ORM ) argumentando que el uso o no de un ORM hay que decidirlo en relación a la complejidad de nuestro modelo de datos, y al rendimiento que requeriremos en nuestras soluciones, pero, ¿cuándo no deseamos el mejor rendimiento para nuestro software? Lo cierto es que, como ya hemos visto, el ORM facilita mucho las cosas, aporta claridad al código, de eso no cabe duda, pero, ¿es eficiente? He ahí la cuestión. Sobre este asunto vamos a poner a funcionar nuestros apreciados Test. C

Proyecto GEOW. Implementando el patrón CQRS. 2ª parte

En el anterior post ( Proyecto GEOW. Implementando el patrón CQRS ) nos adentramos en el funcionamiento del proyecto GEOW que nos va a servir de base para implementar un patrón arquitectónico, CQRS , pensado para dar respuesta a sistemas con alta exigencia de lecturas y escrituras simultáneas. Para ello hemos creado una interfaz gráfica con una serie de figuras geométricas en movimiento. Ahora vamos a ir a la parte de EL DATO. Cada vez que uno de estos cuadrados cambia de posición envía una trama con sus propias características, y sus nuevas coordenadas. Cada uno de los cuadrados realiza un movimiento cada 300 milisegundos, y he llegado a probar con hasta unas 700 figuras. En estos niveles el software empieza a sufrir, pero más la parte gráfica. Aparentemente el sistema de grabación de coordenadas se mantiene en buena forma. Vamos a ver, precisamente, este sistema de grabación: Grabando lotes de coordenadas en BBDD En el objeto PointObj que representa cada una de las figuras, en el

Primeros pasos en Enigma Software Labs

En pocas palabras: vamos a probar y estresar distintas soluciones de software para un mismo problema, en busca de realidades matemáticas. Para ello, lo primero que necesitamos en averiguar el modo de publicar código aquí en Blogger . Para ello voy a usar un sistema súper sencillo y eficiente. Ahí va un ejemplo de código C# private void frmMonitor_Load ( object sender, EventArgs e) { TreeNode _tr = new TreeNode( "SQL Server Tips" ); TreeNode _tr2 = new TreeNode( "C# Code" ); treeCatalogo.Nodes.Add(_tr); treeCatalogo.Nodes.Add(_tr2); //splitContainer.Panel1.Focus(); //lstMonitor.Focus(); } Hilarante ¿verdad?   hilite.me  lo hace posible. Bien, paso 2, fácil y sencillo: vamos a crear una plataforma de testeo para C# en una bonita solución que compartiré en el repositorio GIT, para disfrute de todos. Enigma Software - ZM LABS Como posteriormente me dive