Antes de nada, vamos a definir qué entendemos por arquitecturas
N-Tier y N-Layer.
Una aplicación N-Tier es una aplicación basada en tres o más
niveles físicos, como nivel de cliente/presentación, nivel de
servidor de componentes de negocio y nivel de datos (servidores de
bases de datos, etc.). Actualmente también hacemos uso de servicios
Web (o, mejor incluso, específicamente de servicios WCF) para
comunicar el nivel cliente/presentación con el nivel de servidor de
componentes.
La figura 1 muestra un diagrama típico de arquitectura N-Tier. Es
un diagrama que me costó bastante encontrar, porque es de una
presentación de Microsoft de aproximadamente 1998, en la época de
Windows NT 4.0 y MTS (Microsoft Transaction Server), y poco después
Windows DNA y COM+. Sin embargo, aunque las tecnologías internas
han cambiado mucho en estos años, la arquitectura básica de estos
niveles físicos (tiers) es la misma J.
Como se puede observar, en una arquitectura N-Tier las
aplicaciones cliente (bien basadas en Windows Forms, WPF o incluso
Silverlight) necesitan acceder remotamente al nivel de servidor de
aplicaciones haciendo uso, por ejemplo, de servicios WCF. Relativo
a esto, en mi opinión, las aplicaciones RIA (Rich Internet
Applications) son un tipo especial de aplicaciones N-Tier.
Otro tema diferente es que probablemente habremos diseñado
nuestras aplicaciones como aplicaciones N-Layer (N-Capas), es
decir, con varias capas lógicas en las que implementamos diferentes
tipos de tareas. Por ejemplo, podemos tener capas como DAL (Data
Access Layer), la capa BLL (Business Logic Layer), la capa BFLL
(Business Façade Logic Layer), la capa de servicios WCF y una o
varias capas de presentación (interfaz de usuario), dependiendo del
patrón que usemos, como MVC (Modelo-Vista-Controlador), MVP
(Modelo-Vista-Presentador), MV-VM (Modelo-Vista-Vista Modelo),
etc.
Por supuesto, dentro de dicha arquitectura N-Layer, usted podrá
suponer que Entity Framework (en adelante EF) encaja inicialmente
en la capa DAL (Data Access Layer), donde se accede a los orígenes
de datos, así como también haciendo uso de entidades desconectadas
de EF, las cuales se pasarán/comunicarán de unas capas de la
arquitectura a otras. La figura 2 muestra un diagrama típico de
arquitectura N-Layer. Y en la figura 3 vemos situadas en las capas
las diferentes tecnologías de Microsoft, donde resalto la
utilización de EF en la capa de acceso a datos en la parte inferior
de la arquitectura.
Por cierto, quiero resaltar que no todas las aplicaciones N- Layer
tienen que ser aplicaciones N-Tier (niveles físicos, con nivel de
presentación/cliente remoto), aunque todas las aplicaciones N-Tier
sí deberían haber sido diseñadas en modo N-Layer (capas lógicas
diferenciadas). Quiero decir con esto que, en algunos casos, en
cuantos menos niveles físicos tengamos dividido nuestro modelo,
mejor rendimiento tendremos; por ejemplo, en una aplicación ASP.NET
tendremos la mayoría de las veces la capa de presentación y la de
componentes de negocio en el mismo servidor, como parte de un único
nivel físico. Muchos niveles físicos pueden ser buenos para
conseguir una óptima escalabilidad, sin embargo no favorecen el
rendimiento puro debido a razones de latencia en las llamadas
remotas. En cualquier caso, recuerde que N-Layer tiene que ver con
capas lógicas.
Si volvemos a una arquitectura N-Tier (niveles físicos), como dije
antes, necesitamos de mecanismos de acceso remoto (como WCF) para
poder comunicar el nivel de cliente (por ejemplo una aplicación WPF
o Silverlight) con el nivel de servidor de aplicación.
Posteriormente, cuando se realizan consultas desde el servidor de
componentes de negocio a la base de datos para obtener datos, como
por ejemplo un pedido, lo obtendremos como una entidad de EF.
Entonces lo desconectamos (detach) de su contexto de EF, se
serializa automáticamente por WCF y se manda como entidad
desconectada por la red (gracias a los servicios WCF) hasta llegar
al nivel de presentación (aplicación y máquina cliente).
Así pues, la mayoría de las aplicaciones empresariales usan
patrones de arquitectura que necesita de una fachada sin estados
para su lógica de negocio (como aplicaciones N-Tier y N-Layer).
Tendremos pues escenarios con orientación a servicios, con
servicios Web básicos o servicios WCF, aplicaciones ASP.NET,
aplicaciones tradicionales de escritorio Windows (cliente rico
basado en Windows Forms o WPF) o incluso aplicaciones RIA con
Silverlight que consumen servicios WCF, etc. En la mayoría de estos
casos se trabaja con una filosofía de acceso a datos basada en
"entidades de datos desconectadas". Esto es bueno porque se
consiguen aplicaciones altamente escalables, pues los datos de la
base de datos (en las tablas reales) no sufren bloqueos mientras
los usuarios están trabajando en el nivel cliente (por ejemplo, un
formulario WPF o Silverlight). Por ejemplo, en dicha aplicación
cliente el usuario podría estar trabajando, cambiando datos de
forma "desconectada", y podría incluso irse diez minutos a tomarse
un café mientras el formulario está abierto con los datos, volver,
y finalmente actualizar los datos en el sistema central (servidor
de aplicaciones). En ese momento, lo que pasará es que al final de
una cadena de llamadas (pasando por servicios Web, componentes de
negocio, etc.), una clase de la capa DAL, haciendo uso de "LINQ to
Entity Framework", actualizará dichos datos en la base de datos.
¿Correcto? Bueno, si hacemos simplemente esto, sería una
actualización de tipo "El último gana", o lo que yo llamo
"Actualización muy optimista" J.
Me explico. En muchas aplicaciones en las que no tengamos
requisitos muy exigentes/complejos a nivel de actualizaciones, el
sistema que hemos comentado puede ser suficiente. Pero en cambio,
podemos tener otras aplicaciones donde el usuario necesita saber si
los datos que está usando en la aplicación cliente han cambiado en
la base de datos mientras él estaba trabajando (o tomando un café)
con el formulario abierto y antes de actualizar los datos. En ese
caso, deberemos hacer uso de "actualizaciones con concurrencia
optimista", manejando las posibles "excepciones de concurrencia
optimista".
En este artículo nos referiremos al primer caso; vamos a ver cómo
realizar actualizaciones de datos del tipo "El último gana" en
aplicaciones N-Tier (con aplicación cliente remota y datos
desconectados). Para cubrir el otro escenario, más avanzado, es
posible que escriba un próximo artículo; en cualquier caso, al
final de éste tenéis una referencia a mi blog [1], donde ese
escenario lo tengo también contemplado.
Bien, pues a nivel de tecnología, estamos hablando de:
• Uso de Entity Framework y LINQ to Entities en nuestra capa
DAL.
• WCF como tecnología de comunicaciones remotas y
orientación a servicios.
• Cualquier tecnología .NET remota (WPF, Windows Forms, OBA
o incluso Silverlight) para el nivel de presentación (interfaz de
usuario con ejecución en máquina cliente).
Opción base: actualización de datos en aplicaciones N-Tier
haciendo uso de EF y entidades desconectadas
Para entender completamente lo que quiero decir, inicialmente
vamos a explicar la forma más sencilla, es decir, cómo implementar
actualizaciones simples de datos en aplicaciones N-Tier haciendo
uso de Entity Framework. Así pues, éste es el caso llamado "El
último gana" o "Actualización muy optimista". En este caso, no es
necesario gestionar las posibles situaciones de concurrencia
optimista.
Primeramente, tendremos una clase de acceso a datos, CustomerDal,
donde haremos uso de EF y LINQ to Entities para realizar la
consulta inicial a la base de datos; por ejemplo, digamos que vamos
a obtener los datos de un cliente y devolver dichos datos al nivel
cliente/presentación mediante un servicio WCF. Generalmente, se
tiene una o más clases de negocio (BLL) entre el servicio WCF y la
clase DAL (invocaciones intermedias); en aras de simplificar el
artículo, aquí la obviamos.
El código de dicha clase DAL sería similar al que muestra el
listado 1.
Probablemente, una muy buena opción sería usar tipos genéricos en
las clases de acceso a datos, para así no tener que repetir código
parecido para distintas entidades; eso también lo obvio, por
simplificar y hacer hincapié solo en la parte relativa a la
actualización de datos desconectados en aplicaciones N-Tier.
Si conoce algo de LINQ to Entities, estará de acuerdo conmigo en
que el código de arriba es muy sencillo. Lo importante a destacar
de este código es que estamos devolviendo un objeto de entidad
desconectada, pasándolo a las capas lógicas superiores (DAB
->BLL -> Capa servicios WCF -> Presentación). Eso
significa que de cara a EF estaremos tratando los objetos
ObjectContext como "contextos de vida corta". Básicamente, cuando
se devuelva dicha entidad al servidor con los datos modificados,
tendremos que hacer una re-conexión (re-attach) de dicha entidad
mediante un nuevo objeto de contexto, puesto que la entidad fue
modificada en un estado desconectado.
Este escenario es muy típico siempre que usemos fachadas sin
estado (como hace la mayoría de las aplicaciones N-Tier y
N-Layer).
Volviendo a nuestro código, después podríamos tener un servicio
WCF encargado de serializar y devolver dicha entidad
desconectada.
El código del contrato de la operación de servicio WCF para
recuperar un cliente se muestra en el listado 2. Hay que destacar
que los métodos de servicios Web deben ser lo más ligeros posibles
(simplemente son un interfaz de entrada externa); por eso, nuestro
método del servicio Web está simplemente invocando a una clase de
negocio (CustomerBll). La clase BLL es donde situaríamos código de
lógica de negocio, seguridad, transacciones, etc.; pero en este
caso es tan simple que consiste en un único método que llama a la
clase DAL y devuelve lo mismo (lo que normalmente se llama en
inglés Man in the middle). Por esta razón obvio su
presentación.
Después de eso, la aplicación cliente consumirá dicho servicio WCF
y mostrará los datos de la entidad (los datos de un cliente, en
este caso), por ejemplo en un formulario WPF. Recordad ahora que la
aplicación cliente está trabajando con un objeto de entidad que
está desconectado, por lo que no tenemos ningún ObjectStateManager
de EF registrando los cambios. Veremos después cómo afecta
esto.
Así pues, una vez que los datos están visibles en el formulario,
el usuario puede modificar los datos hasta que decida
actualizarlos, pulsando, por ejemplo un botón "Grabar". En este
momento, la aplicación cliente llamará a otro método de WCF con un
código similar al del listado 3. El método WCF llama a la clase de
negocio y ésta a su vez a la clase de acceso a datos, que es donde
tendremos el código interesante de LINQ to Entities y donde vamos a
ver cómo utilizar EF para actualizar la entidad modificada que nos
viene "desconectada" desde la aplicación cliente.
El listado 4 muestra el método de actualización de la capa DAL
(recuerde que en este caso no tenemos que tratar las situaciones de
concurrencia optimista).
Para poder aplicar cualquier actualización en la base de datos
usando context.SaveChanges(), primero necesitamos tener la entidad
ligada a ese contexto. Si simplemente utilizáramos una llamada al
método predefinido context.Attach(customer), la cosa no
funcionaría, porque la entidad customer es desconocida en el nuevo
contexto que tenemos: este nuevo objeto de contexto no sabe nada de
esta entidad, es decir, no tiene registrado ningún cambio que se
haya producido sobre ella.
Por esta razón es por la que he creado un método extensor propio
para ObjectContext llamado AttachUpdated. Este método permite
enlazar/conectar objetos de entidad que vienen, desconectados, de
la aplicación cliente remota. Su código se presenta en el listado
5.
Bueno, con este código ya hacemos cosas más especiales. En primer
lugar, en nuestro código tenemos que determinar qué datos nuevos
hay en nuestra entidad, es decir, cuáles serían los cambios si lo
comparamos con los datos actuales en la base de datos. Para saber
eso, necesitamos hacer una consulta a la base de datos y obtener
los datos actuales. Lo hacemos mediante el método
context.TryGetObjectByKey, y eso es lo que establecemos como
valores originales de esa entidad para el contexto actual.
Entonces, llamamos al método context.ApplyPropertyChanges,
proporcionando nuestra nueva entidad (con datos cambiados). Lo que
hace EF es comparar los "datos originales" (datos actuales en la
base de datos) con nuestros datos en la entidad que viene del
cliente, y entonces actualiza en el contexto todas los cambios de
propiedades/campos de nuestra entidad. Después de eso, ya podremos
llamar a context.SaveChanges en el método principal. Ahí, EF sí
detectará los cambios con respecto a la base de datos y será
posible actualizar la base de datos real (SQL Server, por ejemplo)
con los nuevos datos.
Quiero destacar que después de llamar a ApplyPropertyChanges
también estoy llamando a ApplyReferencePropertyChanges. Para este
caso sencillo, no es realmente necesario, pero explico qué es. Este
método es también otro método extensor que aplica los cambios de
propiedades de mi entidad, pero en todas las entidades
referenciadas dentro de nuestro modelo EF (Customer podría estar
relacionado con Order, etc.), siempre y cuando estas otras
entidades estén presentes en el grafo de datos del contexto actual.
Este método es sumamente importante; en caso contrario, tendríamos
algo que solo valdría para entidades completamente aisladas. El
código de ApplyReferencePropertyChanges se presenta en el listado
6.
Con esto, ya habríamos acabado. Pero aún mostraremos otra
alternativa para implementar la estrategia "El último en actualizar
gana". El resultado final es similar, pero sin embargo nos
ahorraríamos hacer una consulta a la base de datos para conocer los
datos actuales (no ejecutaríamos el método
context.TryGetObjectByKey). En cambio, lo que hacemos es "decirle"
al contexto que "todas las propiedades de mi entidad han cambiado,
simplemente porque yo lo digo". Después de eso, simplemente
llamaremos de igual forma a context.SaveChanges, y en este caso se
actualizarán todas las propiedades/campos de la entidad.
Simplemente "porque yo lo he dicho" J.
He implementado esta técnica por medio de otro método extensor,
SetAllModified, en este caso asociado a la entidad en lugar del
contexto (listado 7). Usando este método extensor, el código para
la actualización de un cliente quedaría tan simple como se ve en el
listado 8.
Esta sería la otra opción que tenemos para actualizaciones simples
de tipo "El último gana". La ventaja, como ya hemos mencionado,
estriba en que no necesita consultar a la base de datos para
conocer los datos originales. La desventaja: que si tenemos muchas
propiedades/campos en la entidad, actualizaremos siempre todas las
propiedades del registro, aun cuando el usuario hubiera cambiado
solo un campo o unos pocos. Dependiendo de cada caso concreto,
podemos utilizar un camino u otro.
¡Fenómeno! Pues eso parece sencillo, "y vivimos en un mundo
siempre feliz". ¿Seguro? Bueno, pues depende, porque como dije al
principio del artículo, esta forma de actualizar los datos no cubre
escenarios en los que queramos gestionar actualizaciones con
concurrencia optimista. Retomando nuestro caso de uso, supongamos
que mientras el usuario original estaba tomándose su café, otro
usuario utiliza la aplicación y actualiza datos del mismo cliente
en la base de datos. ¿Qué pasará cuando el primer usuario vuelva y
pulse el botón "Grabar"? Pues lo dicho, el usuario original ni se
enterará de va a sobrescribir modificaciones que ha hecho un
segundo usuario, y de hecho, machacará dichos datos ("El último
gana"). En muchos casos, esto sería incluso lo deseable; pero en
otros puede ser inaceptable. Todo depende del caso concreto.
Como ya hemos mencionado, para un análisis de este segundo tipo de
situación, en la que es necesario gestionar las actualizaciones con
concurrencia optimista y sus excepciones, podéis consultar mi blog
[1], donde explico en detalle una implementación de dicho
escenario.