Junio de 2016

Volumen 31, número 6

Tecnología de vanguardia: Creación de un CRUD histórico, parte 2

Por Dino Esposito | Junio de 2016

Dino EspositoEn términos conceptuales, un CRUD histórico (crear, leer, actualizar, eliminar) es el CRUD clásico extendido con un parámetro adicional: una fecha. Un CRUD histórico permite agregar, actualizar y eliminar registros de una base de datos, y también consultar el estado de la base de datos en un momento determinado. Un CRUD histórico ofrece a las aplicaciones una infraestructura integrada para las características de creación de informes avanzados y análisis de inteligencia empresarial.

En mi columna del mes pasado (msdn.com/magazine/mt703431), presenté la base teórica de los sistemas CRUD históricos. En este artículo, ofreceré una demostración práctica.

Presentación del escenario de ejemplo

Para los objetivos de este artículo, consideraré un sistema de reservas simple. Por ejemplo, considere el sistema que utiliza una compañía de forma interna para permitir que los empleados reserven salas de reuniones. Al fin y al cabo, dicho software es puramente CRUD, donde se crea un registro para reservar un horario. El mismo registro se actualiza si la reunión cambia a un horario diferente, o se elimina si se cancela la reunión.

Si codifica este sistema de reservas como un CRUD normal, conocerá el último estado del sistema, pero perderá toda la información sobre reuniones actualizadas y eliminadas. ¿Esto es realmente un problema? depende. Probablemente no sea un problema si considera que las reuniones son simplemente una parte más de la empresa. Sin embargo, si busca formas de mejorar el rendimiento general de los empleados, un CRUD histórico que realice un seguimiento de las actualizaciones y eliminaciones de registros podría revelar que las reuniones se cancelan o cambian de horario demasiadas veces, lo que podría indicar que los procesos internos no son óptimos o que hay una actitud inadecuada.

En la Figura 1 se presenta una interfaz de usuario realista para un sistema de reserva de salas. La base de datos subyacente es una base de datos SQL Server con un par de tablas vinculadas: Rooms y Bookings.

Interfaz de usuario del front-end de un sistema de reservas
Figura 1. Interfaz de usuario del front-end de un sistema de reservas

La aplicación de ejemplo está diseñada como una aplicación de ASP.NET MVC. Cuando el usuario hace clic para registrar la solicitud, un método de controlador interviene y procesa la información enviada. El fragmento de código siguiente da una idea clara del código que controla la solicitud en el lado servidor:

[HttpPost]
public ActionResult Add(RoomRequest room)
{
  service.AddBooking(room); 
  return RedirectToAction("index", "home");
}

El método pertenece a una clase BookingController y delega en una clase de servicio de trabajo insertado para las labores de organización del trabajo real. Un aspecto interesante sobre la implementación del método es que redirige a la página inicial de la Figura 1 después de crear la reserva. No se crea ninguna vista explícita como resultado de la operación de adición de reserva. Esto es un efecto secundario de la elección de una arquitectura de segregación de responsabilidad de consultas y comandos (Command Query Responsibility Segregation, CQRS). El comando de adición de reserva se envía al back-end, modifica el estado del sistema y eso es todo. Si la aplicación de ejemplo hubiese utilizado AJAX para el envío, no habría sido necesario actualizar nada y el comando habría sido una operación independiente sin ningún vínculo visible en la UI.

La diferencia principal entre un CRUD clásico y un CRUD histórico es que el histórico realiza un seguimiento de todas las operaciones que modifican el estado del sistema desde el principio. Para planificar un CRUD histórico, debería considerar que las operaciones de negocio son como comandos que se proporcionan al sistema y pensar en un mecanismo para localizar esos comandos. Cada uno de los comandos modifica el estado del sistema y un CRUD histórico conserva un seguimiento de cada estado en el que se ha encontrado el sistema. Cualquier estado que se haya producido se registra como un evento. Un evento es la descripción simple e inmutable de algo que ha sucedido. Cuando tenga la lista de eventos, puede crear varias proyecciones de datos encima de ella; la más popular de ellas es simplemente el estado actual de las entidades de negocio implicadas.

En una aplicación, los eventos se originan directamente a partir de la ejecución de comandos de usuario o indirectamente a partir de otros comandos o una entrada externa. En este escenario de ejemplo, se espera que el usuario haga clic en un botón para enviar una solicitud de reserva.

Procesamiento del comando

Esta es otra implementación posible del método AddBooking del controlador de la aplicación:

public void AddBooking(RoomRequest request)
{
  var command = new RequestBookingCommand(request);
  var saga = new BookingSaga();
  var response = saga.AddBooking(command);
  // Do something based on the outcome of the command
}

La clase RoomRequest es un objeto de transferencia de datos sin formato que ha rellenado la capa de enlace de ASP.NET MVC con datos enviados. En su lugar, la clase RequestBookingCommand almacena los parámetros de entrada necesarios para ejecutar el comando. En un escenario tan simple, las dos clases prácticamente coinciden. ¿Cómo procesaría el comando? En la Figura 2 se muestran los tres pasos principales del procesamiento de un comando.

La cadena de pasos principales para procesar un comando
Figura 2. La cadena de pasos principales para procesar un comando

El controlador es un componente que recibe el comando y lo procesa. Un controlador se puede invocar mediante una llamada en memoria directa desde dentro del código de servicio del trabajo o puede tener un bus en medio, como se muestra aquí:

public void AddBooking(RoomRequest request)
{
  var command = new RequestBookingCommand(request);
  // Place the command on the bus for
  // registered components to pick it up
  BookingApplication.Bus.Send(command);
}

Un bus podría aportar algunas ventajas. Una de ellas es que puede controlar con facilidad escenarios en los que varios controladores puedan estar interesados en el mismo comando. Otra ventaja es que sería posible configurar un bus para que sea una herramienta de mensajería confiable que garantizase la entrega del mensaje en tiempo y evitase posibles problemas de conectividad. Además, un bus puede ser simplemente el componente que ofrezca la capacidad para registrar el comando.

El controlador podría ser un componente simple de uso único que comience y termine en la misma solicitud, o puede ser un flujo de trabajo de larga ejecución que tarde horas o días en completarse y se pueda suspender a la espera de una aprobación manual en algún momento. Los controladores que no son simples ejecutores de tareas de uso único a menudo de denominan sagas.

En general, utilizará un bus o una cola si tiene requisitos concretos relacionados con la escalabilidad y la confiabilidad. Si solo busca crear un CRUD histórico en lugar de un CRUD clásico, probablemente no necesite usar un bus. Tanto si usa un bus como si no, en algún momento el comando podría alcanzar controlador de uso único o de larga ejecución. El controlador está pensado para llevar a cabo las tareas que se esperen. La mayoría de tareas están formadas por operaciones principales en una base de datos.

Registro del comando

En un CRUD clásico, la escritura de información en una base de datos implicaría la adición de un registro que dispusiese los valores que se han pasado como entrada. Sin embargo, en una perspectiva de CRUD histórico, el registro recién agregado representa el evento creado de una reserva. El evento de una reserva creado es información independiente e inmutable que incluye un identificador único del evento, una marca de tiempo, un nombre y una lista de argumentos específicos para el evento. Los argumentos de un evento creado normalmente incluyen todas las columnas que rellenaría en un registro Booking recién agregado a una tabla Bookings clásica. Los argumentos de un evento actualizado, por contra, están limitados a los campos que se actualicen. Por lo tanto, todos los eventos actualizados podrían no tener el mismo contenido. Por último, los argumentos de un evento eliminado están limitados a los valores que identifican de forma única la reserva.

Una operación de un CRUD histórico está compuesta por dos pasos:

  1. Registrar el evento y sus datos relacionados.
  2. Asegurarse de que el estado actual del sistema se pueda consultar de inmediato y de forma rápida.

De esta forma, el estado actual del sistema siempre está disponible y actualizado y todas las operaciones que llevaron a ese estado también están disponible para su análisis. Tenga en cuenta que el "estado actual del sistema" es simplemente el único estado que se ve en un sistema CRUD clásico. En pro de la eficacia en el contexto de un sistema CRUD simple, el paso en que se registra el evento y se actualiza el estado del sistema debería producirse de forma sincrónica dentro de la misma transacción, como se muestra en la Figura 3.

Figura 3. Registro de un evento y actualización del sistema

using (var tx = new TransactionScope())
{
  // Create the "regular" booking in the Bookings table   
  var booking = _bookingRepository.AddBooking(
    command.RoomId, ...);
  if (booking == null)
  {
    tx.Dispose();   
    return CommandResponse.Fail;
  }
  // Track that a booking was created
  var eventToLog = command.ToEvent(booking.Id);
    eventRepository.Store(eventToLog);
  tx.Complete();
  return CommandResponse.Ok;
}

En el estado actual, cada vez que se agregue, modifique o elimine un registro de una reserva, se mantiene la lista general de reservas actualizada a la vez que se conoce la secuencia de eventos exacta que llevó al estado actual. En la Figura 4 se muestran las dos tablas de SQL Server que forman parte de este escenario de ejemplo y sus contenidos después de una inserción y actualización.

Las tablas Bookings y LoggedEvents en paralelo
Figura 4. Las tablas Bookings y LoggedEvents en paralelo

En la tabla Bookings aparecen todas las reservas distintas que se encuentran en el sistema y se devuelve el estado actual de cada una de ellas. En la tabla LoggedEvents figuran todos los eventos de las distintas reservas en el orden en que se registraron. Por ejemplo, la reserva 54 se creó en una fecha determinada y se modificó unos días más tarde. En el ejemplo, la columna Cargo de la imagen almacena la secuencia JSON serializada del comando que se está ejecutando.

Uso de eventos registrados en la UI

Supongamos que un usuario autorizado quiere ver los detalles de una reserva pendiente. Probablemente el usuario llegará a la reserva mediante una lista de calendario o a través de una consulta basada en tiempo. En ambos casos, los aspectos fundamentales de la reserva (cuándo, cuánto tiempo y quién) ya se conocen y la vista de detalles podría no ser demasiado útil. Sin embargo, podría ser de utilidad si se pudiera mostrar el historial entero de la reserva, como se muestra en la Figura 5.

Consumo de eventos registrados en la UI
Figura 5. Consumo de eventos registrados en la UI

Mediante la lectura de los eventos registrados, es posible crear un modelo de vista que incluya una lista de estados de la misma entidad agregada: la reserva número 54. En la misma aplicación de ejemplo, cuando un usuario haga clic para ver los detalles de la reserva, aparece un elemento emergente modal y se descarga JSON en segundo plano. El punto de conexión que devuelve JSON se muestra aquí:

public JsonResult BookingHistory(int id)
{
  var history = _service.History(id);
  var dto = history.ToJavaScriptSlotHistory();
  return Json(dto, JsonRequestBehavior.AllowGet);
}

El método History del servicio del trabajo se ocupa de la mayor parte de cosas que es necesario hacer aquí. La parte principal de esas cosas es consultar todos los eventos que están relacionados con el identificador de reserva especificado:

var events = new EventRepository().All(aggregateId);
foreach (var e in events)
{
  var slot = new SlotInfo();
  switch (e.Action)
  {
    :
  }
  history.Changelist.Add(slot);
}

A medida que recorre los eventos registrados, se anexa un objeto adecuado al objeto de transferencia de datos que se devolverá. Las transformaciones que se realizan en ToJavaScriptSlotHistory permiten que sea fácil y rápido mostrar la delta entre dos estados consecutivos de la forma que puede observar en la Figura 5.

No obstante, es destacable que aunque los eventos de registro, incluso los que están dentro de un CRUD, permiten esas mejoras en la UI, su aspecto más importante es el hecho de que usted no conoce todo lo que ha sucedido dentro del sistema, y puede procesar esos datos para extraer cualquier proyección de datos que pueda necesitar en un momento determinado. Por ejemplo, podría crear una estadística de la actualización y permitir que los analistas llegaran a la conclusión de que el proceso entero de solicitar salas de reuniones no funciona en la empresa, debido a que con demasiada frecuencia la gente hace una reserva y luego la actualiza o elimina. También puede observar cuál era la situación de las reservas en una fecha concreta, simplemente mediante una consulta de los eventos registrados hasta entonces y un cálculo del estado posterior general. En resumidas cuentas, un CRUD histórico abre un mundo de posibilidades completamente nuevo para las aplicaciones.

Resumen

Sencillamente, mediante CRUD es posible evolucionar las aplicaciones puramente CRUD de una forma elegante. No obstante, en este artículo se han mencionado conceptos en boga y patrones que tienen mucho más potencial, como CQRS, abastecimiento de eventos, bus y colas, y lógica de negocios basada en mensajes. Si el artículo le ha resultado útil, le recomiendo que lea mis columnas de julio de 2015 (msdn.com/magazine/mt238399) y agosto de 2015 (msdn.com/magazine/mt185569). Tras ver este ejemplo, puede que esos artículos le parezcan aún más inspiradores.


Dino Espositoes el autor de "Microsoft .NET: Architecting Applications for the Enterprise" (Microsoft Press, 2014) y "Modern Web Applications with ASP.NET" (Microsoft Press, 2016). Como experto técnico para las plataformas .NET y Android en JetBrains y orador frecuente en eventos mundiales de la industria, Esposito comparte su visión sobre el software en software2cents@wordpress.com y en su Twitter @despos.

Gracias al siguiente experto técnico de Microsoft por revisar este artículo: Jon Arne Saeteras