En esta quinta entrega de la serie, puede ver las anteriores entradas aquÃ, I,-II–III y IV, vamos a hablar sobre actualizaciones y los mecanismos de gestión de la concurrencia que tenemos, por supuesto intentando bajar lo más posible, para mi estas entradas son también una fuente de aprendizaje. Empezaremos la casa por dónde debemos, o sea que lo primero que haremos será hacer una pequeña inserción de un documento más o menos complejo, ya que esto nos dará juego para alguna cosa interesante, basado en las siguientes clases que podemos ver a continuación:
Actualizaciones
1 |
<span style="color: #0000ff">public</span> <span style="color: #0000ff">class</span> Post |
1 |
{ |
1 |
<span style="color: #0000ff">public</span> <span style="color: #0000ff">string</span> Id { get; set; } |
1 |
<span style="color: #0000ff">public</span> <span style="color: #0000ff">string</span> Title { get; set; } |
1 |
<span style="color: #0000ff">public</span> <span style="color: #0000ff">string</span> Body { get; set; } |
1 |
  |
1 |
<span style="color: #0000ff">public</span> List<Comment> Comments { get; set; } |
1 |
} |
1 |
  |
1 |
<span style="color: #0000ff">public</span> <span style="color: #0000ff">class</span> Comment |
1 |
{ |
1 |
<span style="color: #0000ff">public</span> <span style="color: #0000ff">string</span> UserName { get; set; } |
1 |
<span style="color: #0000ff">public</span> <span style="color: #0000ff">string</span> Text { get; set; } |
1 |
} |
En concreto, la inserción la haremos con los siguientes datos:
1 |
<span style="color: #0000ff">using</span> (IDocumentStore store = <span style="color: #0000ff">new</span> DocumentStore() { Url = <span style="color: #006080">"http://portblackcode"</span> }.Initialize()) |
1 |
<span style="color: #0000ff">using</span>(IDocumentSession session = store.OpenSession()) |
1 |
{ |
1 |
session.Store(<span style="color: #0000ff">new</span> Post() |
1 |
{ |
1 |
Title = <span style="color: #006080">"RavenDB(V): actualizaciones, concurrencia y otras hierbas!"</span>, |
1 |
Body = <span style="color: #006080">"En esta quinta entrega de la serie, puede ver las ...."</span>, |
1 |
Comments = <span style="color: #0000ff">new</span> List<Comment>() |
1 |
{ |
1 |
<span style="color: #0000ff">new</span> Comment() |
1 |
{ |
1 |
UserName = <span style="color: #006080">"preguntoncojonero"</span>, |
1 |
Text = <span style="color: #006080">"cualquier pregunta...."</span> |
1 |
} |
1 |
} |
1 |
}); |
1 |
  |
1 |
session.SaveChanges(); |
1 |
} |
Bien, llega la hora de empezar a hablar de actualizaciones. Como ya dijimos en la primera introducción a RavenDB, este ha sido creado apoyándose en patrones y experiencias provenientes de ORMs, muy especialmente de NHibernate, aunque algo también hay de otros….Uno de estos patrones recogidos de frameworks como NH o EF es UnitOfWork, el cual a estas alturas seguro que todos conocemos… Por lo tanto si un IDocumentSession es un UnitOfWork entonces la actualización de un documento deberÃa de ser tan facil como lo siguiente:
1 |
var post = session.Load<Post>(<span style="color: #006080">"posts/1"</span>); |
1 |
post.Comments.Add(<span style="color: #0000ff">new</span> Comment() |
1 |
{ |
1 |
UserName =<span style="color: #006080">"Unai"</span>, |
1 |
Text = <span style="color: #006080">"Pues efectivamente preguntoncojonero..."</span> |
1 |
}); |
1 |
1 |
session.SaveChanges(); |
Aunque por ahora seguimos sin hablar de la parte de consulta, fÃjese en le método Load, el cual, como seguramente habrá deducido, nos permite obtener un documento en función de su clave, en este caso, estamos utilizando la convención por defecto ( HiLo ). Si miráramos con Fiddler este proceso la primera entrada serÃa precisamente esta carga:
GET http://ravendbserver/docs/posts/1 HTTP/1.1
Accept-Encoding: deflate,gzip
Content-Type: application/json; charset=utf-8
Host: portblackcode
Connection: Keep-Alive
Ahora, puesto que cualquier IDocumentSession es un UnitOfWork, cualquier cambio en cualquier objeto consultado, y por lo tanto trackeado por la infraestructura de RavenDB, es llevado al servidor una vez realizada la llamada a SaveChanges, todos los cambios,inserciones,etc de una sesión se hacen en batch para reducir roundtrips.
POST http://ravendbserver/bulk_docs HTTP/1.1
Accept-Encoding: deflate,gzip
Content-Type: application/json; charset=utf-8
Host: portblackcode
Content-Length: 513
Expect: 100-continue
[{"Key":"posts/1","Method":"PUT","Document":{"Title":"RavenDB(V): actualizaciones, concurrencia y otras hierbas!","Body":"En esta quinta entrega de la serie, puede ver las ….","Comments":[{"UserName":"preguntoncojonero","Text":"cualquier pregunta…."},{"UserName":"Unai","Text":"Pues efectivamente preguntoncojonero…"}]},"Metadata":{"Raven-Entity-Name":"Posts","Raven-Clr-Type":"sampleupdate.Post, sampleupdate","@etag":"00000000-0000-0200-0000-000000000001","Last-Modified":"/Date(1323811962000+0100)/"}}]
Fijese en la traza anterior como el cuerpo de nuestro POST contiene la información de la operación a realizar y el nuevo documento a establecer en RavenDB. Esto es una nota muy importante, es decir, es todo el documento que vamos a modificar no es la modificación en si. Esto, por ejemplo, tiene mucho impacto cuando estamos hablando de concurrencia, en concreto de gestión de concurrencia optimista, porque si dos personas incluyen un comentario en un post de forma concurrente podrÃamos tener problemas. Esto es una de las cosas que quizás mas impacta con respecto a lo que sabemos de modelos relaciones, ¿un problema de concurrencia en una inserción, porque en realidad es lo que estamos haciendo??… Pues si, ahora no hay tablas y filas, hay un documento, una estructura compleja, que cambia, y cambia, en este caso, completamente, por lo tanto, si tenemos dos personas insertando un comentario sino manejamos el problema de concurrencia podrÃamos perder comentarios….
Concurrencia y ETag
Como podemos solventar el caso anterior, o más bien, como podemos enterarnos de un posible problema de concurrencia??? Bueno, a estas alturas ya todos sabemos que RavenDB es Restfull ¿verdad? y los que sabéis de http sabéis que existe el concepto de ETag para identificar un estado de un recurso en un momento dado. Pues bién, RavenDB utiliza este mismo concepto para gestionar la concurrencia de los documentos. Si volvemos a fijarnos en la trama de actulización veremos como junto con la metadata del documento tenemos una propiedad llamada etag, que en nuestro caso es la siguiente:
"@etag":"00000000-0000-0200-0000-000000000001"
Este valor es automaticamente modificado en el servidor cuando un documento se actualiza, por lo tanto, si tenemos un problema de concurrencia, lo que pasarÃa es que el etag del documento en el servidor no se corresponderÃa con el documento que nosotros enviamos, seria anterior, y por lo tanto podrÃamos suponer que estamos intentando actualizar algo que ya ha sido cambiado por otro proceso. Ok, ¿como se activa en RavenDB la comparación de los etag en los procesos de modificación?, pues muy sencillo, cualquier sesión, IDocumentSession, tiene un mecanismo para activar el chequeo de concurrencia de la siguiente forma:
1 |
session.Advanced.UseOptimisticConcurrency = <span style="color: #0000ff">true</span>; |
Con este simple flag, podemos hacer la indicación de comprobación, en realidad lo que hace es que el documento que se envia incluye otra propiedad ETAG, a mayores de la que hay en metadata, para que el servidor sepa que tiene que hacer esta comprobación. Ahora, si ocurriera un problema de concurrencia, recibirÃamos la siguiente respuesta del servidor:
HTTP/1.1 409 Conflict
Cache-Control: private
Content-Type: text/html
Server: Microsoft-IIS/7.5
Raven-Server-Build: 531
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Tue, 13 Dec 2011 22:49:33 GMT
Content-Length: 219
{
"Url": "/bulk_docs",
"ActualETag": "00000000-0000-0300-0000-00000000000b",
"ExpectedETag": "00000000-0000-0300-0000-00000000000a",
"Error": "PUT attempted on document ‘posts/1’ using a non current etag"
}
Esta respuesta, provoca que se lance una excepción dentro del API de RavenDB, llamada ConcurrencyException que nos dará la información del problema que tenemos, y por lo tanto, refrescar, si asà lo queremos nuestra operación.
Patching….
Bien, seguro que muchos estais pensando que, efectivamente, aunque la gestión de la concurrencia nos permite resolver nuestro problema, lo habitual ante la entrada de un post, es la coincidencia de muchos comentarios y por lo tanto tendremos muchos conflictos y demasiado trabajo para que estamos buscando hacer. Una alternativa para hacer esto es utilizar el concepto del verbo PATH en http. Hacer esto en RavenDB es realmente sencillo, como veremos a continuación, y nos permitirá, en el caso que estamos, crear nuevos comentarios a un post evitando problemas de concurrencia y mejorando el rendimiento ya que nos evitaremos tener que hacer un load de un documento.
Un patch request es, desde un punto de vista de código, tan sencillo como lo siguiente:
1 |
var patchRequest = <span style="color: #0000ff">new</span> PatchRequest() |
1 |
{ |
1 |
Type = PatchCommandType.Add, |
1 |
Value = RavenJToken.FromObject(<span style="color: #0000ff">new</span> Comment(){User =<span style="color: #006080">"Unai"</span>,Text = <span style="color: #006080">"mira ±or pregunton..."</span>}), |
1 |
Name = <span style="color: #006080">"Comments"</span> |
1 |
}; |
Si nos fijamos en el código anterior, un patch está representado por la clase PatchRequest, en la cual, podemos indicar el tipo de cambio a realizar, PatchCommandType nos ofrece comandos como ( Set, UnSet, Add, Modify,Rename …) la propiedad que modificaremos, en nuestro caso Comments y el cambio. En nuestro caso, el cambio es agregar un nuevo comentario, por lo tanto hacemos una transformación del objeto new Comment a Json por medio de la clase RavenJToken.
Una vez creado el request es suficiente con llamar a Patch definido en DatabaseCommands como podemos er a continuación:
1 |
<span style="color: #0000ff">using</span>(IDocumentStore store = <span style="color: #0000ff">new</span> DocumentStore(){ConnectionStringName =<span style="color: #006080">"Raven"</span>}.Initialize()) |
1 |
<span style="color: #0000ff">using</span>(IDocumentSession session = store.OpenSession()) |
1 |
{ |
1 |
var patchRequest = <span style="color: #0000ff">new</span> PatchRequest() |
1 |
{ |
1 |
Type = PatchCommandType.Add, |
1 |
Value = RavenJToken.FromObject(<span style="color: #0000ff">new</span> Comment(){User =<span style="color: #006080">"Unai"</span>,Text = <span style="color: #006080">"mira ±or pregunton..."</span>}), |
1 |
Name = <span style="color: #006080">"Comments"</span> |
1 |
}; |
1 |
  |
1 |
session.Advanced.DatabaseCommands.Patch(<span style="color: #006080">"posts/1"</span>, <span style="color: #0000ff">new</span> PatchRequest[] { patchRequest }); |
1 |
} |
Si prestamos un poco de atención vemos como el método Patch nos permite especificar la clave del documento sobre la que vamos a aplicar el cambio, en este caso posts/1. Si ejecutamos esto, y observamos fiddler verÃamos algo similar a lo siguiente:
POST http://ravendbserver/bulk_docs HTTP/1.1
Accept-Encoding: deflate,gzip
Content-Type: application/json; charset=utf-8
Host: portblackcode
Content-Length: 139
Expect: 100-continue
[{"Key":"posts/1","Method":"PATCH","Patches":[{"Type":"Add","Value":{"User":"Unai","Text":"mira señor pregunton…"},"Name":"Comments"}]}]
y una respuesta de confirmación de cambio:
HTTP/1.1 200 OK
Cache-Control: private
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/7.5
Raven-Server-Build: 573
X-AspNet-Version: 4.0.30319
Persistent-Auth: true
X-Powered-By: ASP.NET
Date: Thu, 15 Dec 2011 19:40:37 GMT
Content-Length: 271
[{"Etag":"00000000-0000-0b00-0000-000000000001","Method":"PATCH","Key":"posts/1","Metadata":{"Raven-Entity-Name":"Posts","Raven-Clr-Type":"RavenUpdate.Post, RavenUpdate","Last-Modified":"2011-12-15T18:52:37.8760000Z","Non-Authoritive-Information":false,"@id":"posts/1"}}]
Otras hierbas
Este mismo mecanismo nos puede servir también para realización de un cambio en multiples documentos, para ello tenemos el método Batch en vez de Path, desde aquà os dejo a vosotros la tarea de investigación…
Saludos
Unai