He estado pensando en crear una aplicación de referencia para usar .NET y Couchbase Server. Mientras viajaba a Londres para la conferencia Progressive NoSQL en Las competencias importanPasé las horas de espera a que mi habitación de hotel estuviera lista escribiendo algo de código en un Startbucks para una aplicación que llamo TapMap.
La aplicación es bastante sencilla: se trata de una imitación de Twitter centrada en las cervezas. La idea básica es que alguien en un bar informe de que ha encontrado (y probablemente bebido) una cerveza concreta en un lugar determinado. Ese informe se llama "grifo". La función "mapa" muestra una serie de grifos en un mapa de Nokia, utilizando GeoCouch. El valor es que los usuarios pueden aprender rápidamente dónde encontrar sus cervezas favoritas.
El código es todavía muy alfa y mis habilidades HTML necesitan ser mejoradas. Pero he implementado lo suficiente como para empezar una serie de posts sobre los patrones de código y las soluciones expuestas por TapMap. Este post será la primera parte y describirá como crear un simple Repositorio de T para Couchbase usando la librería .NET Client. Puedes obtener el código en http://github.com/couchbaselabs/TapMap.
Si no estás familiarizado con los repositorios, la idea básica es que tienes una clase que es responsable del acceso a los datos de un objeto de dominio. Así, para buscar un usuario por su dirección de correo electrónico, utilizarías el método GetByEmail(string email) del UserRepository. Los repositorios también suelen contener métodos CRUD estándar. Hay un poco más en el patrón, pero voy a demostrar su uso básico. Más información en http://msdn.microsoft.com/en-us/library/ff649690.aspx.
La herencia es una forma natural de conseguir la reutilización del código. TapMap utiliza una clase base de repositorio para proporcionar métodos CRUD a sus subclases. Además, la clase base hace uso de genéricos para que los métodos CRUD puedan devolver instancias de T en lugar de sólo objetos. Restringir T a un tipo particular también permite algunos beneficios adicionales que discutiré en breve.
{
//métodos basura aquí
}
Como ya sabrás, Couchbase Server 2.0 no impone un esquema a sus documentos. Almacena un documento JSON con las propiedades que desees incluir. Dicho esto, todos los documentos se almacenan con una propiedad reservada "_id" que deriva su valor de su clave.
Añadir un documento de la siguiente manera:
El resultado es un documento como el siguiente:
"_id" : "cerveza_tres_filósofos",
"nombre" : "Tres Filósofos"
}
Así que para asegurarnos de que todos nuestros objetos modelo tienen esta propiedad ID crearemos una clase base modelo que defina una.
público abstracto clase BaseModelo
{
[JsonProperty(NombrePropiedad = "_id")]
público cadena Id { consiga; configure; }
}
No voy a cavar en él JsonProperty piezas aquí. Estoy usando Json.NET para la serialización y escribí más sobre ello en http://www.couchbase.com/develop/net/next. Lo importante a tener en cuenta es que tenemos objetos modelo con una propiedad "Id" y al restringir nuestros repositorios para utilizar subclases de ModelBase, sabemos que siempre tendremos esta propiedad.
A la hora de crear vistas, es útil disponer de una taxonomía en los documentos para que sea fácil encontrar todos los documentos "cerveza" o todos los documentos "usuario" o incluso encontrar un documento "usuario" con una dirección de correo electrónico determinada. Para facilitar este requisito, impondremos una propiedad "tipo" a todos los documentos del modelo. A nuestra ModelBase, añadiremos una propiedad Type abstracta y de sólo lectura.
público abstracto cadena Tipo { consiga; }
Ahora que entendemos las suposiciones que podemos hacer de nuestro modelo, volvamos a nuestra clase RespositoryBase.
Lo primero que nuestra clase RepositoryBase necesita hacer es definir una instancia de CouchbaseCliet.
protegido estático sólo lectura CouchbaseClient _Cliente = null;
estático RepositoryBase()
{
Cliente = nuevo CouchbaseClient();
}
Dado que la configuración del cliente es costosa, vamos a asegurarnos de que sólo ocurra una vez por AppDomain creando una instancia estática de sólo lectura que se instancie en un constructor estático. El cliente es expuesto como un campo protegido a subclases que necesitan extender los métodos CRUD básicos en nuestra clase base. Nótese que este cliente depende de que el Web.config haya definido la sección "couchbase". En algún momento, añadiré una versión IoC amigable de nuestro RepositoryBase.
La base del repositorio define los métodos Crear, Actualizar y Guardar. Estos métodos a su vez llaman al método privado "store" con el valor apropiado de StoreMode. Crear y Actualizar son sencillos. Guardar simplemente crea un documento cuando la clave no existe y lo reemplaza cuando existe. Los valores de retorno son el valor CAS de la operación. En algún momento, añadiré CAS como parámetro opcional. El método store también llama a un método de extensión CasJson que serializa todos los objetos a cadenas JSON antes de guardarlos en Couchbase.
público virtual ulong Cree(Modelo T)
{
devolver tienda(StoreMode.Añadir, modelo);
}
público virtual ulong Actualización(Modelo T)
{
devolver tienda(StoreMode.Sustituir, modelo);
}
público virtual ulong Guardar(Modelo T)
{
devolver tienda(StoreMode.Establecer, modelo);
}
privado ulong tienda(Modo StoreMode, modelo T)
{
var resultado = Cliente.CasJson(modo, BuildKey(modelo), modelo);
devolver resultado.Resultado ? resultado.Cas : 0;
}
El método privado store también hace uso del método protegido BuildKey para crear claves mediante una convención. Específicamente, las claves se componen a partir de la propiedad Type del modelo y su propiedad Id, con espacios reemplazados por guiones bajos. TapMap en realidad no hace uso de esta versión de BuildKey ya que se sobrescribe en las subclases que utilizan Create, Update o Save. Estoy considerando añadir una propiedad "Name" al ModelBase para permitir componer una clave basada en un tipo y un nombre. Por ejemplo, una cerveza con nombre "Old Yankee Ale" obtiene una clave "beer_Old_Yankee_Ale".
{
devolver cadena.Concat(modelo.Tipo, “_”, modelo.Id.Sustituir(” “, “_”));
}
El método Get toma el _id del documento como argumento cuando es llamado. Ese valor se utiliza como clave para llamar al método Get genérico del cliente. Otro método de extensión - GetJson(string id) - también está siendo utilizado por el método Get para convertir el documento JSON en una instancia de T.
{
var doc = Cliente.GetJson<T>(id);
si (doc != null) doc.Id = id; //servidor no devuelve el _id en el JSON
devolver doc;
}
Remove es muy sencillo. Simplemente toma la clave del documento (de nuevo, también el _id) y elimina el elemento de Couchbase.
{
Cliente.Eliminar(id);
}
Proporcionar soporte base para el acceso a las vistas se realiza por convención en el método View. Este método asume que el nombre del documento de diseño es el nombre del tipo de modelo pluralizado. Así BeerRepository tendrá automáticamente sus peticiones de vista mapeadas a un documento de diseño llamado "cervezas". El método View devuelve una instancia IView. Dado que la llamada HTTP no se realiza hasta que un llamador itera sobre el IView, las subclases pueden llamar a la vista utilizando los métodos de consulta fluidos. Tendré un ejemplo de una llamada específica a la vista en breve.
{
devolver Cliente.GetView(InflectorNet.Pluralizar(tipode(T).Nombre.ToLower()), viewName);
}
También existe actualmente un método SpatialView, que permite realizar consultas geoespaciales utilizando GeoCouch. El método GetSpatialView del cliente es actualmente experimental y no ha sido añadido a la rama maestra 1.2 del cliente. Sin embargo, se añadirá próximamente. Si clona el repositorio TapMap, obtendrá un binario del cliente con soporte para este método.
{
devolver Cliente.GetSpatialView(InflectorNet.Pluralizar(tipode(T).Nombre.ToLower()), viewName);
}
Eso cubre RepositoryBase. Así que ahora que hemos visto sus métodos y su propósito, vamos a profundizar en una subclase. Primero, la clase modelo User define propiedades para Username, Email y Password. También proporciona mapeos de propiedades JSON a versiones en minúsculas de las propiedades de la clase. Observa también que User extiende ModelBase e implementa la propiedad Type devolviendo la cadena "user".
[Serializable]
público clase Usuario : BaseModelo
{
[Requerido]
[JsonProperty("nombre de usuario")]
público cadena Nombre de usuario { consiga; configure; }
[Requerido]
[JsonProperty("email")]
público cadena Correo electrónico { consiga; configure; }
[Requerido]
[JsonProperty("contraseña")]
público cadena Contraseña { consiga; configure; }
público anular cadena Tipo
{
consiga { devolver "usuario"; }
}
}
La clase UserRepository extiende RepositoryBase con T de tipo User.
{
//métodos
}
Con la simple definición anterior, el UserRepository tiene bastante poder. Puede realizar toda la funcionalidad CRUD básica definida en su clase base. Sin embargo, hay algunos escenarios especiales para trabajar con documentos de Usuario en la aplicación y el UserRepository añade métodos para soportar estos escenarios.
Cuando un usuario se registra con un nombre de usuario y una contraseña, una opción sería crear una vista que emita una clave compuesta de nombre de usuario y contraseña. Sin embargo, una alternativa mejor es utilizar el nombre de usuario y la contraseña para componer una clave para el documento. Dicha clave nos permitirá recuperar fácilmente un documento de usuario cuando éste se conecte. Este enfoque ahorra espacio al no tener que crear el índice secundario de la vista y permite una clave más rápida al utilizar la búsqueda hashtable.
En el método anulado Create, el nombre de usuario (Id) y la contraseña (hash) se utilizan para crear una clave de la forma user_Username_HashedPassword.
{
modelo.Contraseña = HashHelper.ToHashedString(modelo.Contraseña);
modelo.Id = BuildKey(modelo);
var resultado = Cliente.CasJson(StoreMode.AñadirBuildKey(modelo), modelo);
devolver resultado.Resultado ? resultado.Cas : 0;
}
BuildKey también está sobrecargado para permitir el nuevo formato de clave. El método privado buildKey es necesario para el método Get sobrecargado que veremos en breve.
protegido anular cadena BuildKey(Usuario)
{
devolver buildKey(usuario.Nombre de usuariousuario.Contraseña);
}
privado cadena buildKey(cadena nombre de usuario, cadena contraseña)
{
devolver cadena.Concat("usuario_"nombre de usuario, “_”Contraseña);
}
El método Get definido en RepositoryBase asume una búsqueda por clave. En lugar de exponer el formato especial de la clave de usuario a la clase UserController que llama, Get está sobrecargado para aceptar un nombre de usuario y una contraseña a partir de los cuales se compone la nueva clave.
{
devolver Visite(buildKey(nombre de usuario, HashHelper.ToHashedString(contraseña)));
}
La clase UserRepository también es responsable de ayudar a responder a las preguntas de si un nombre de usuario o un correo electrónico son únicos. Para ello, se utilizan dos vistas. La primera simplemente emite el email como clave para todos los documentos de usuario; la segunda emite el nombre de usuario como clave. Hay formas más ingeniosas de crear estos índices, pero para simplificar las cosas, hay un mapeo uno a uno entre la propiedad a comprobar y su vista.
función (doc) {
si (doc.tipo == "usuario") {
emite(doc.correo electrónico, null);
}
}
función (doc) {
si (doc.tipo == "usuario") {
emite(doc.usrename, null);
}
}
El UserRepository define los métodos GetByEmail y GetByUsername, que utilizan el método base View para consultar sus respectivas vistas. En ambos métodos, el límite se establece en 1 y hay un retorno duro en la primera iteración, ya que sólo nos importa que exista un único elemento con este valor de propiedad. No se permiten vistas obsoletas, ya que estamos tratando de hacer cumplir la unicidad de los datos y tenemos que asegurarnos de que tenemos el índice de vista más actualizado posible.
público Usuario GetByEmail(cadena correo electrónico)
{
foreach (var artículo en Ver("by_email").Límite(1).Clave(correo electrónico).Rancio(StaleMode.Falso))
{
devolver Visite(artículo.ItemId);
}
devolver null;
}
público Usuario GetByUsername(cadena nombre de usuario)
{
foreach (var artículo en Ver("por_nombre_de_usuario").Límite(1).Clave(nombre de usuario).Rancio(StaleMode.Falso))
{
devolver Visite(artículo.ItemId);
}
devolver null;
}
Exploraré el UserController en otro momento para mostrar cómo encaja todo. Mi objetivo con este post era introducir el RepositoryBase y cómo se extiende.