Eliminando acoplamiento en un controlador ASP.net MVC

Cuando desarrollamos aplicaciones web podemos caer en el error de dar demasiada responsabilidad a nuestros controladores lo que nos puede traer problemas en el futuro al intentar refactorizar ese código.

En este artículo veremos cómo partiendo de una acción donde la carga de datos se realiza desde el propio controlador podemos reducir el acoplamiento a la manera de guardar los datos. Con ello conseguiremos mejorar la reusabilidad y legibilidad del mismo, así como permitir realizar pruebas unitarias o cambiar componentes de manera sencilla.

Código inicial

Nuestro primer código es un simple controlador con un método que lista un catálogo de libros. Este controlador contiene un método que lista un catálogo de libros. Usaremos directamente sentencias SQL contra una base de datos LocalDB.

public class HomeController : Controller
{
    public ActionResult Books()
    {
        List<Book> books = new List<Book>();
        SqlConnection connection = new SqlConnection(@"Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\Books.mdf;Integrated Security=True");
        SqlCommand cmd = new SqlCommand();
        SqlDataReader reader;

        cmd.CommandText = "SELECT Title,Author FROM [Table]";
        cmd.CommandType = CommandType.Text;
        cmd.Connection = connection;

        connection.Open();

        reader = cmd.ExecuteReader();
        if (reader.HasRows)
        {
            while (reader.Read())
            {
                books.Add(new Book
                {
                    Title = reader.GetString(0),
                    Author = reader.GetString(1)
                });
            }
        }

        connection.Close();
        return View(books);
    }
}

El resultado es una lista de objetos de tipo Book, que es un POCO (Plain Old C# Object) que contiene dos campos, Title y Author. La principal desventaja, y es la que intentaremos resolver a lo largo de este artículo, es que presenta un fuerte acoplamiento con el gestor de base de datos, y si cambiamos a MySQL o MongoDB, por poner un ejemplo, nos veríamos obligados a reescribir la esta implementación.

Primera aproximación, patrón Repository

El objetivo de un patrón Repository es crear una capa de abstracción entre la lógica de negocio (representada en este caso por el controlador) y la lógica de base de datos (representada por el acceso a SQL). Dicha capa de abstracción nos permite probar nuestro controlador de manera independiente a los datos y a la manera de almacenar los mismos.

La implementación de un patrón repository la podemos realizar usando una interfaz y su correspondiente implementación:

public interface IBookRepository
{
    List<Book> GetAllBooks();
}

public class BookRepository : IBookRepository
{
    private SqlConnection _connection;
    private string _connectionString = @"Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\Books.mdf;Integrated Security=True";
    public BookRepository()
    {
        _connection = new SqlConnection(_connectionString);
    }

    public List<Book> GetAllBooks()
    {
        List<Book> books = new List<Book>();
        SqlCommand cmd = new SqlCommand();
        SqlDataReader reader;

        cmd.CommandText = "SELECT Title,Author FROM [Table]";
        cmd.CommandType = CommandType.Text;
        cmd.Connection = _connection;

        _connection.Open();

        reader = cmd.ExecuteReader();
        if (reader.HasRows)
        {
            while (reader.Read())
            {
                books.Add(new Book
                {
                    Title = reader.GetString(0),
                    Author = reader.GetString(1)
                });
            }
        }

        _connection.Close();
        return books;
    }
}

El código de la implementación es el mismo, ya que seguimos usando SQL para el acceso a datos, sin embargo hemos creado una abstracción que nos permite realizar pruebas unitarias y dejar los detalles de la implementación del acceso a datos al Repositorio.

El código del controlador se reduce al código siguiente:

public class HomeController : Controller
{
    private IBookRepository _bookRepository;

    public HomeController()
    {
        _bookRepository = new BookRepository();
    }

    public ActionResult Books()
    {
        return View(_bookRepository.GetAllBooks());
    }
}

Pese a que el repositorio se inicia desde el controlador, podríamos usar una solución de inyección de dependencias como Ninject o crear nuestra propia solución para que la inicialización de las dependencias se realice fuera del mismo.

Reduciendo acoplamiento en nuestro Repositorio

Una vez tenemos separados el controlador y el repositorio, tenemos varias alternativas para mejorar la legibilidad y la facilidad de uso del código

LINQ to SQL

Usar LINQ to SQL nos permite realizar un mapeado directo entre nuestra tabla y una clase, así como una capa de abstracción adicional sobre nuestro repositorio.

public class LinqToSqlBookRepository : IBookRepository
{
    private DataContext _db;
    private string _connectionString = @"Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\Books.mdf;Integrated Security=True";
    public LinqToSqlBookRepository()
    {
        _db = new DataContext(_connectionString);
    }

    public List<Book> GetAllBooks()
    {
        Table<Book> book = _db.GetTable<Book>();
        return book.ToList<Book>();
    }
}

Para que el mapping funcione correctamente, hemos de decorar nuestro modelo con los nombres de las columnas equivalentes:

[Table(Name = "Table")]
public class Book
{
    [Column(Name = "Title")]
    public string Title { get; set; }
        
    [Column(Name = "Author")]
    public string Author { get; set; }
}

Esta aproximación nos da una capa de abstracción mayor, comprobación de tipos en tiempo de compilación, capacidad de debug, y facilidad de acceso, a costa de perder algo de flexibilidad y rendimiento.

Conclusiones y pasos adicionales

LINQ to SQL no es la única manera de agregar abstracciones a nuestros datos, también podemos usar sistemas más complejos como Entity Framework, NHibernate, o Massive, siendo este el último que he descubierto y al que espero dedicarle pronto un artículo. Cada capa de abstracción, como decíamos anteriormente, nos permite desacoplar el código que estamos usando, aumentar su facilidad para ser probado, y su legibilidad, a la vez que cumplimos con principios como el de responsabilidad simple.

El proyecto termina con 3 capas diferenciadas:

  • Capa de presentación (Controlador)
  • Capa de datos (Repositorio)
  • Capa de persistencia (SQL)

Podemos agregar capas de lógica de negocio (si queremos realizar validaciones sobre los datos antes de que lleguen a la base de datos) o de servicio (si tenemos varias interfaces para nuestra aplicación). Cada capa aumenta la complejidad de nuestra aplicación pero también aumenta nuestra capacidad para limitar las responsabilidades de cada clase.

Puedes ver el código completo en github

Lecturas adicionales

Extra: Código de creación de la base de datos y datos de ejemplo

El código de creación de la base de datos y de los datos de ejemplo también es muy simple (SQL)

CREATE TABLE [dbo].[Table]
(
    [Id] INT NOT NULL PRIMARY KEY IDENTITY, 
    [Title] NVARCHAR(50) NULL, 
    [Author] NVARCHAR(50) NULL
)
INSERT INTO [dbo].[Table] (Title,Author) VALUES ('Hamlet', 'William Shakespeare')
INSERT INTO [dbo].[Table] (Title,Author) VALUES ('El Ingenioso Hidalgo Don Quijote de la Mancha', 'Miguel de Cervantes y Saavedra')
INSERT INTO [dbo].[Table] (Title,Author) VALUES ('La divina comedia', 'Dante Alighieri')

Cómo configurar una máquina virtual de SQL Server 2012 en Azure

Con la última actualización de Azure, a la manera clásica de trabajar con SQL en la nube se le agrega una segunda opción, más potente, ya que nos permite contar con una máquina virtual completa con Windows Server a la cual podemos acceder remotamente, ajustar los servicios a nuestras necesidades, o instalar el software necesario para nuestra infraestructura.

Esta opción requiere algo de configuración, y este artículo pretende ser un pequeño resumen de temas a tener en cuenta a la hora de realizar la puesta a punto de este servicio:

  • Se puede crear la máquina virtual o bien desde línea de comandos o desde el portal de Azure. Para ello es necesario habilitar la característica, que en estos momentos se encuentra en fase Preview. Se ha de especificar que se trata de SQL Server 2012 Evaluation Edition, aunque se cuenta también con la edición 2008 R2.
  • El proceso de aprovisionamiento (copiar, instalar e iniciar la máquina por primera vez) tarda un rato, ya que tiene que crear los discos, copiar la información, y realizar una primera configuración.
  • Al realizar el aprovisionamiento, se configura automáticamente un endpoint para la conexión por RDP, pero es necesario habilitar otro para el puerto 1433 si queremos tener acceso remoto a la base de datos, esto se hace desde la vista de endpoints dentro del panel de la máquina virtual.
  • La máquina tiene el firewall activado por defecto, así que es necesario crear una regla para que permita el paso al puerto 1433 definido antes, la regla debe estar bajo Inbound Rules ya que se desea permitir conexiones entrantes.
  • Desde SQL Server Configuration Manager, habilitar las opciones Named PipesTCP/IP para poder acceder remotamente.
  • A no ser que se configure la máquina virtual para que forme parte de un dominio, será necesario activar la autenticación mixta (Windows y SQL Server) y crear un nuevo usuario para acceder remotamente, estableciendo los permisos adecuados. Ambas operaciones se realizan desde SQL Server Management Studio.
  • Si todo ha ido bien, se podrá realizar la conexión a la recién creada instancia desde un Management Studio local (disponible en el Centro de descargas de Microsoft)

La explicación completa, más ampliada, se puede encontrar en el Blog del equipo de Windows Azure

¿Qué es un ORM y por qué nos interesa?

En el desarrollo de una aplicación suelen estar involucradas dos entidades diferentes, por una parte  el código que mueve la aplicación y por otra los datos que se manejan. Con el tiempo estas dos entidades han evolucionado de manera diferente, y el acceso a los datos desde los programas se ha vuelto una tarea en ocasiones, complicada. Los sistemas de Mapeo Objeto-Relacional u ORM ayudan a combatir esta complicación.

En este artículo se verá una definición de estos sistemas, así como algunos ejemplos de motores ORM empleados en la actualidad de manera comercial.

Origen de los conflictos: Las bases de datos relacionales

En un principio los programas accedían físicamente al disco para escribir los datos, algo que implicaba que el programa tuviese que implementar toda la lógica de una base de datos para permitir agregar, modificar o eliminar datos.

Con el tiempo se han desarrollado lo que se conoce como sistemas de bases de datos relacionales, que permiten mantener entidades (Artículo, Ficha médica o Factura), sus respectivas relaciones (Una ficha médica corresponde a un solo paciente) y sus atributos (Una ficha médica contiene fecha de entrada, fecha de salida, síntomas… etc).

Estos sistemas poseen además una interfaz para acceder a ellos llamada SQL o Structured Queried Language (lenguaje estructurado de consultas) que permiten hacer peticiones de una base de datos usando una notación muy similar a lenguaje natural, como muestra el siguiente ejemplo:

    select nombre,apellidos from pacientes where id_paciente = 1;

Este código tendría como objetivo hacer una petición a la base de datos pacientes, buscar el identificador 1 y devolver la fila (o filas) resultante de la petición.

Estos sistemas permiten mantener cierta integridad de los datos, y usando ciertas reglas se pueden definir tablas y restricciones como muestra el siguiente ejemplo:

    create database pacientes(
        id_paciente int,
        nombre varchar(80),
        apellidos varchar(120)
    );

Entre los sistemas gestores de bases de datos más utilizados se encuentran:

  • SQL Server de Microsoft: Es la solución por excelencia del stack Microsoft. Existen al menos una docena de ediciones diferentes dirigidas a un público específico, desde la versión Express, gratuita y con limitaciones (1 GB de consumo de memoria y 4 GB de capacidad) hasta la Datacenter, que permite el uso de 256 núcleos (poca cosa…)  y dirigida a un mercado empresarial.
  • mySQL: Anteriormente propiedad de Sun Microsystems y con doble licencia (libre para proyectos compatibles con la licencia GNU GPL y privativa para el resto de los proyectos, que implica la compra de una licencia), actualmente propiedad de Oracle, es la base de datos más empleada por la comunidad OpenSource (+6.000.000 de instalaciones).
  • PosgreSQL: Empleada en ámbito docente aunque no por ello menos potente que la anterior, libre (licencia BSD) y mantenida por la comunidad de desarrolladores.
  • Oracle: Considerado por muchos uno de los sistemas de bases de datos más complejos, usado fundamentalmente en el mercado de servidores empresariales donde su hegemonía ha sido casi total hasta hace poco, por la competencia del resto de sistemas antes mencionados. Posee también una versión Express (gratuita)

Antes de los ORM

Antes de la aparición de estos sistemas las consultas se tenían que realizar a mano dentro de las propias aplicaciones, con lo cual la ventaja de los lenguajes orientados a objetos se perdía, ya que había que crear una petición a la base de datos de manera manual (y específica para cada sistema, ya que no todos los gestores de bases de datos tienen la misma implementación del lenguaje SQL).

Para muestra, un ejemplo que se puede encontrar en la documentación del lenguaje PHP:

// El siguiente código puede ser proporcionado por el usuario, a modo de ejemplo
$firstname = 'fred';
$lastname  = 'fox';

// Formular Consulta
// Este es el mejor método para formular una consulta SQL
// Para más ejemplos, consulte mysql_real_escape_string()
$query = sprintf(&quot;SELECT firstname, lastname, address, age FROM friends
    WHERE firstname='%s' AND lastname='%s'&quot;,
    mysql_real_escape_string($firstname),
    mysql_real_escape_string($lastname));

// Ejecutar Consulta
$result = mysql_query($query);

// Comprobar resultado
// El siguiente código muestra la consulta enviada a MySQL y el error ocurrido. Útil para debugging.
if (!$result) {
    $message  = 'Invalid query: ' . mysql_error() . &quot;\n&quot;;
    $message .= 'Whole query: ' . $query;
    die($message);
}

// Usar resultado
// Si intenta imprimir $result no será posible acceder a la información del recurso
// Una de las funciones mysql result debe ser usada
// Consulte también mysql_result(), mysql_fetch_array(), mysql_fetch_row(), etc.
while ($row = mysql_fetch_assoc($result)) {
    echo $row['firstname'];
    echo $row['lastname'];
    echo $row['address'];
    echo $row['age'];
}

// Libere los recursos asociados con el resultset
// Esto es ejecutado automáticamente al finalizar el script.
mysql_free_result($result);

Como se aprecia, es necesaria una adaptación de los datos a la aplicación además del aprendizaje del lenguaje de gestión de las bases de datos.

El mapeo relacional

La ventaja principal de estos sistemas es que reducen la cantidad de código necesario para lograr lo que se conoce como una persistencia de objetos

Esto permite además, lograr una integración con otros patrones como el Modelo-Vista-Controlador, donde el modelo puede ser este objeto. En el ejemplo siguiente se muestra cómo se define un modelo usando Doctrine, uno de los sistemas ORM más usados para PHP, donde se muestra el tipo usuario y los campos username y password. Será el propio sistema el que se encargue de convertir esta información a tablas SQL y a realizar el procesamiento, mientras que nosotros trabajaremos fundamentalmente con objetos

class User extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this-&gt;hasColumn('username', 'string', 255);
        $this-&gt;hasColumn('password', 'string', 255);
    }
}

Este otro ejemplo, que usa ADO.net Entity Framework, el ORM de .NET, se pueden apreciar también la facilidad para establecer propiedades a campos, como en este caso establecer obligatoriedad.

namespace MyApp.Models
{
    public class User
    {
        public int Id { get; set; }
        [Required] public string UserName { get; set; }
        [Required] public string Password { get; set; }
    }
}

Motores de persistencia

Estos son algunos motores de persistencia a los que he podido echar un vistazo, algunos forman parte de frameworks más potentes (Como Core Data en el caso de iOS) y otros son independientes (como el anteriormente mencionado Doctrine), aunque todos comparten la misma base, dar un modelo de persistencia de objetos.

  • C#: Entity Framework es un conjunto de APIs que proporcionan acceso a datos en .NET. Se distribuye junto con el .NET framework y tiene 3 posibles modos de trabajo, Database First, Model First y Code First, más información en el sitio oficial
  • Java: Hibernate es una herramienta de mapeo relacional para la plataforma Java, que emplea atributos declarativos mediante XML o anotaciones, se distribuye con licencia GNU/LGPL y posee una versión para .NET llamada nHibernate, más información en el sitio oficial
  • Objective-C: Core Data (Parte de la API de Cocoa), proporciona la capacidad de persistencia mediante serialización para dispositivos con Mac OSX o iOS, más información en la guía de programación de Core Data
  • PHP: Doctrine, mencionado anteriormente, un proyecto independiente, con la especialidad de que posee su propio lenguaje para el acceso a datos, llamado Doctrine Query Language. Más información en el sitio oficial
  • Ruby: ActiveRecord (Parte de Ruby On Rails), genera un modelo de persistencia basado en las clases, proporciona acceso a datos en el framework y es la clase base para los modelos del mismo. En la API de Rails se puede ver el sistema en detalle.

Conclusiones

Los modelos ORM proporcionan grandes ventajas a proyectos que empiezan, ya que permiten tener acceso a los datos de una manera sencilla y rápida, además de proporcionar cierta abstracción sobre la base de datos que se encuentra por debajo.

Es cierto que no es la única manera de acceder a los datos, ya que soluciones específicas habitualmente tendrán mejor rendimiento, ya que no se hace una transformación de las ordenes y de los datos.

Finalmente creo que se deben conocer, y tenerlos como alternativa o como sistema principal. Como siempre, todo depende del uso.

Errores en Reporting Services 2008 R2, Internet Explorer 9 y Windows 7.

Si desarrollas usando Reporting Services en SQL Server 2008 R2, es posible que hayas tenido problemas a la hora de ejecutar la administración de informes en Internet Explorer 9 y Windows 7.

Error de autenticación rsAccessDenied

El primer error que puede ocurrir es de autenticación, es decir, que no deje entrar en el sistema (Error rsAccessDenied). Para ello la solución a la que pude llegar, tras bastante investigación, ejecutar IE9 en modo Administrador, fue agregar en el registro un valor DWORD para permitir la autenticación a usuarios locales:

Ruta: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System
Nombre de clave: LocalAccountTokenFilterPolicy
Tipo de clave: DWORD 32 bits
Valor: 00000001

Error al ejecutar Report Builder

Una vez dentro del sistema, nos puede sorprender el hecho de que si intentamos cargar el visor de informes nos saldrá una molesta advertencia para instalar .Net Framework 3.5, lo que nos provoca una contradicción:

  • Si intentamos instalarlo, nos dice que usemos la herramienta de agregar características de Windows
  • Si entramos a las características de Windows, veremos que no solo lo tenemos instalado, sino que podemos tener una versión superior.
La culpa? La administración de Reporting Services no reconoce Internet Explorer 9, con lo cual tendremos que trucarlo:
Para ello activamos las herramientas de desarrollo F12, y seleccionaremos, como modo de explorador IE8, de esta manera se recargará la página y podremos ejecutar Report Builder sin problemas.

Error al instalar el Report Viewer

Otro error que puede traernos de cabeza es que al tratar de ejecutar el Report Builder es que intente instalarse, una vez instalado, y nos devolverá un error.

La solución a esto pasa por acudir a C:\Users\NombreDeUsuario\AppData\Local\Application Data y borrar la carpeta 2.0. Eso restaurará la configuración por defecto, y podremos volver a instalar la aplicación sin tener conflictos.