Adrianistán

El blog de Adrián Arroyo


Tutorial de CouchDB

- Adrián Arroyo Calle

Hoy vengo a hablar de CouchDB, una base de datos que no es precisamente nueva ni es precisamente famosa, pero que en mi opinión tiene unas características únicas.



En primer lugar vayamos con la teoría. Apache CouchDB es una base de datos NoSQL que almacena documentos JSON y tiene una potente API REST. Está programada en Erlang y es un proyecto Apache desde 2008. Usa JavaScript para la operativa interna. Visto este resumen uno podría pensar que estamos ante un clon de MongoDB (a pesar de que CouchDB es mucho más antigua) pero no. CouchDB parte de una filosofía distinta, ser la base de datos de la era de la web y se nota mucho como toma decisiones que la alejan de las bases de datos relacionales y de las bases de datos NoSQL más populares.

La API REST


Una parte fundamental de CouchDB es la API REST, que es la única API que dispone. CouchDB de este modo puede funcionar de dos formas:

  • En un modelo tradicional, con una servidor que haga las peticiones. Este servidor puede estar programado en cualquier cosa (Java, PHP, Node.js, C#, Python, Ruby, Elixir,...) y que no es muy distinto a como nos conectamos por ejemplo a MongoDB o a MySQL.

  • En un modelo innovador donde la base de datos está conectada directamente a Internet y no hace falta ningún servidor que gestione la comunicación. Este modelo da lugar a las CouchApps, aplicaciones CRUD pero sin servidor propiamente dicho, solo base de datos y un alojamiento estático de archivos. A través de JavaScript en el lado del cliente podemos obtener la información, conectándose directamente el navegador con la base de datos.


La posibilidad de hacer esto último es una de las mayores ventajas de CouchDB en mi opinión, ya que según el proyecto, esto puede ahorrar mucho tiempo y prevenir muchos posibles bugs en operaciones tontas. El desafío de este artículo será crear una aplicación en CouchDB que pudiera simular a una aplicación CRUD cualquiera. Esta aplicación reducirá costes respecto a una tradicional ya que eliminamos la parte del servidor, solo mantenemos la base de datos (y un proxy).

Instalando CouchDB


CouchDB está en casi todas las distros Linux. Es posible que no esté disponible la última versión, pero para lo que vamos a hacer no es necesario. Yo voy a usar CouchDB 1.4, una versión bastante antigua, pero es la que tiene Debian en sus repos. En Ubuntu tenéis CouchDB 1.6 y en la página oficial está la versión 2.0

Creando una base de datos


En CouchDB las bases de datos son cubos sin fondo. Podemos arrojar documentos JSON sin control.

Vamos a usar la API REST para crear la base de datos "supermercado".


curl -X PUT http://127.0.0.1:5984/supermercado


Podemos comprobar que la base de datos ha sido creada con GET


curl -X GET http://127.0.0.1:5984/supermercado


En CouchDB ciertas rutas que comienzan por barra baja son especiales. Por ejemplo si queremos pedir una lista de todas las bases de datos podemos hacer GET a _all_dbs.


curl -X GET http://127.0.0.1:5984/_all_dbs




Para borrar la base de datos usaríamos DELETE


curl -X DELETE http://127.0.0.1:5984/supermercado


Pero no lo hagas todavía. Si has pensado en lo que hemos hecho te estarás alarmando. Hemos creado una base de datos, por HTTP y no se nos ha pedido ninguna autorización ni nada. Por defecto CouchDB es inseguro ya que arranca en un modo llamado Admin Party, pero rápidamente veremos que si vamos a exponer CouchDB a Internet vamos a necesitar seguridad, y CouchDB la trae. Pero antes, vamos a trabajar con documentos.

Insertando un documento


Insertar un documento es tan fácil como tirar documentos a la papelera (bueno, quizá no tan fácil).


curl -X PUT http://127.0.0.1:5984/supermercado/UUID -d '{"nombre": "Manzana", "tipo" : "fruta", "precio" : 5}'


Donde UUID es un UUID. Si no tienes un UUID a mano, CouchDB te ofrece uno a través de _uuids.


curl -X GET http://127.0.0.1:5984/_uuids?count=10


En realidad no es estrictamente necesario poner un UUID, pero es la convención habitual. Una vez hagamos el PUT obtendremos de respuesta un JSON con tres campos muy importantes. Ok para decirnos si la operación fue bien o no, _id, con el ID del documento (es el UUID de antes) y el _rev. Este último campo, nos indica con que versión del documento hemos interactuado, en nuestro caso, era la primera así que hay un 1 delante. Esto es muy importante en CouchDB y tiene que ver con como gestiona la concurrencia. CouchDB es eventualmente consistente. Quiere decir que en una situación de escritura y lectura simultáneas, la base de datos no se bloquea sino que puede mandar distintas versione de los documentos. Cuando editemos un documentos tendremos que indicar  sobre que versión hacemos la modificación, y si algún otro cliente se nos adelantó y modificó el documento antes, tendremos que basarnos en su revisión para que sea aceptada por la base de datos. Hay que tener que CouchDB no es un sistema de control de versiones, las revisiones antiguas pueden desaparecer sin previo aviso. Esta característica, la de ser eventualmente consistente, también facilita mucho su desempeño en clústeres, pero no vamos a entrar en ello. De momento hay que saber que en CouchDB, a diferencia de otras bases de datos, para actualizar un documento necesitaremos su ID y su revisión.

Para ver el documento conociendo su ID hacemos un simple GET


curl -X GET http://127.0.0.1:5984/supermercado/UUID


Nos devuelve el documento entero junto con las propiedades _id y _rev.

Actualizando un documento


El precio de las manzanas ha cambiado, vamos a actualizarlo. El proceso es muy simple, es como insertar un documento pero añadimos la revisión sobre la que queremos hacerlo. Es importante mencionar que CouchDB no preserva nada en las actualizaciones. Si quieres dejar campos sin tocar, tendrás que volver a subirlos.


curl -X PUT http://127.0.0.1:5984/supermercado/98c003b03bc8aa87cb05983d1c000713 -d '{"_rev": "1-eba25568090eb2dfffad770b55147a67","nombre": "Manzana", "tipo" : "fruta", "precio" : 4}'


Para borrar documentos necesitas usar DELETE indicando el número de revisión.


curl -X DELETE http://127.0.0.1:5984/supermercado/98c003b03bc8aa87cb05983d1c000713?rev=2-298fdb46385be60609b242b3e5cc3566


(pero no lo hagas)

Vistas


Todo muy bonito, pero, ¿cómo accedo a la información? En SQL usaríamos SELECT, en bases como MongoDB lanzaríamos un find. En CouchDB hay que usar vistas, que se programan en JavaScript y siguen un esquema MapReduce (que recordemos, funciona muy bien en paralelo). Esto se puede hacer de varias formas. Se puede usar Fauxton, una interfaz web de CouchDB, se puede usar Erica, una herramienta para facilitar la creación y mantenimiento de CouchApps o se puede hacer con la API REST, ya que las vistas se definen en documentos JSON también.

Tenemos que crear un Design Doc. Los design doc son muy útiles y permiten por ejemplo validar documentos que vayan a entrar en la base de datos (podemos definir schemas así) o definir las vistas. Un design doc simple es así. Definimos una vista llamada all con una operación map.

 


{
"views" : {
"all" : {
"map" : "function(doc){ emit(null,doc); }"
}
}
}


Si lo tenemos guardado en un archivo JSON


curl -H "Content-Type: application/json" --data @super.json -X PUT http://127.0.0.1:5984/supermercado/_design/super


Ahora vamos a ejecutar la vista


curl -X GET http://127.0.0.1:5984/supermercado/_design/super/_view/all


Obtenemos un objeto JSON con todos los documentos que contiene la base de datos supermercado. Por supuesto es posible crear distintos documentos de diseño, con distintas vistas cada uno.

Las vistas solo las debería poder crear el administrador de la base de datos, por lo que al contrario que en otras bases de datos, el cliente no podrá realizar operaciones que no hayan sido definidas antes. En realidad, no es del todo cierto, ya que en CouchDB 2.0 se añadió Mango, un lenguaje de consulta declarativo que permite realizar cierta operativa sin usar las vistas, pero no es tan potente.

Otro ejemplo:


{
"views" : {
"by-price" : {
"map" : "function(doc){ emit(doc.precio,doc); }"
}
}
}


Esta vista puede ser llamada con parámetros


curl -X GET http://127.0.0.1:5984/supermercado/_design/super/_view/by-price?descending=true&limit=1


CouchDB realiza la operación de ordenado por su cuenta con el valor del key que devolvemos.

La base de datos en el salvaje oeste


Ahora vamos a ver como exponer CouchDB al salvaje Internet sin comprometer seguridad. En primer lugar toca hablar de los usuarios. Como hemos dicho, CouchDB arranca en el modo Admin Party. Este modo será desactivado en cuanto creemos un usuario como admin. CouchDB soporta además usuarios tradicionales, una característica muy útil que veremos después como usar.

Para crear un usuario como admin lanzamos esta petición:


curl -X PUT http://127.0.0.1:5984/_config/admins/aarroyoc -d '"MiPassword"'


A partir de ahora ya no estamos en una admin party y si intentamos crear una base de datos veremos que CouchDB nos deniega el acceso. Sin embargo, otras tareas como subir documentos a bases de datos ya existentes todavía funcionan.

Para protegernos mejor debemos saber los tipos de autorización que soporta CouchDB. En primer lugar soporta HTTP Basic Auth, un método en el que mandamos el usuario y la contraseña en texto plano en cada petición. Esto no es para nada seguro, pero combinado con SSL no es mala opción. Sin embargo sigue sin ser la mejor solución para los expertos en seguridad. CouchDB también acepta autenticación por cookies. Las cookies duran 10 minutos por defecto y se generan haciendo una petición a _session con formato de formulario HTTP.


curl -vX POST http://127.0.0.1:5984/_session -H "Content-Type:application/x-www-form-urlencoded" -d "name=aarroyoc&password=MiPassword"


Que nos devuelve una cookie válida para operar en la base de datos

Existen plugins que permiten gestionar desde CouchDB autorizaciones más diversas, como por ejemplo OpenID o OAuth2, pero pueden complicarnos mucho el desarrollo.

Los usuarios normales se guardan en la base de datos especial _users y se les puede asignar roles, con permisos para cada base de datos. Por defecto, se pueden crear cuentas de usuario nuevas de forma tan simple como así:


curl -X PUT http://127.0.0.1:5984/_users/org.couchdb.user:USUARIO -d '{"name" : "USUARIO", "password" : "MiPassword", "type" : "user", "roles" : []}'


Es importante seguir el esquema org.couchdb.user:USUARIO para los documentos. Los roles solo los puede ajustar una cuenta de admin, así que en las peticiones anónimas deberá ir vacío.

Si en algún momento quieres saber cuántos usuarios tiene la base de datos, puedes usar la vista predefinida _all_docs.


curl -X GET http://usuarioadmin:contraseñaadmin@127.0.0.1:5984/_users/_all_docs


Estos documentos de usuarios por defecto son privados pero podemos mostrar cierta información para el resto de usuarios, sobretodo si añadimos campos extra a los documentos


curl -X PUT http://usuarioadmin:contraseñaadmin@127.0.0.1:5984/_config/couch_httpd_auth/public_fields -d '"name"'


Hará visible el campo name a todas las peticiones GET anónimas a ese documento de usuario.

Accesos a la base de datos


Ahora vamos a ver como controlar las lecturas y escrituras de una base de datos en concreto. Aquí creo que CouchDB tiene limitaciones y que debe ser algo en lo que enfocarse en futuras versiones pues es un control muy limitado. Si fuese una base de datos interna no sería mucho problema, pero teniendo en cuenta que va a estar expuesta a Internet sería necesario algo más potente. Eso no quiere decir que no se puedan hacer cosas, pero vamos a tener que tirar de funciones JavaScript internas.

Por un lado, tenemos un documento especia en cada base de datos llamado _security. Este contiene listas de admins y miembros. Las listas pueden estar vacías, contener usuarios o contener roles. En caso de que la lista de miembros este vacía se considera que todos los usuarios son miembros (incluido los anónimos). Los miembros pueden leer y escribir todos los archivos. Esto es muy insuficiente. Por poner un caso completo, yo necesitaba:

  • Que todos los usuarios pudieran leer los documentos (incluido anónimos)

  • Que todos los usuarios registrados pudieran crear documentos

  • Que solo se pudiesen modificar los documentos creados por cada usuario


Esto se puede hacer si obligamos a los documentos a que especifiquen un dueño. Todo lo podemos hacer en esta función:


function (new_doc, old_doc, userCtx){
if(!userCtx.name)
throw({forbidden: "Not authorized"});

if(!new_doc.owner)
throw({forbidden: "Plase, set an owner"});

if(new_doc.owner != userCtx.name)
throw({forbidden: "Owner in document should be the same as user"})

if(old_doc!=null)
if(old_doc.owner != userCtx.name && userCtx.roles.indexOf("_admin") < 0)
throw({forbidden: "Not your document"});
return;
}


Esta función puede ser subida a CouchDB con la API HTTP dentro de un design doc. El design doc quedaría así:


{
"_rev" : "7-670f7428b5a5afb25ec61382024f0733",
"views" : {
"all" : {
"map" : "function(doc){ emit(doc.name,doc); }"
},
"by-price" : {
"map" : "function(doc){ emit(doc.precio,doc); }"
}
},
"validate_doc_update":"function (new_doc, old_doc, userCtx){\n if(!userCtx.name)\n throw({forbidden: \"Not authorized\"});\n\n if(!new_doc.owner)\n\tthrow({forbidden: \"Plase, set an owner\"});\n\n if(new_doc.owner != userCtx.name)\n throw({forbidden: \"Owner in document should be the same as user\"})\n\n if(old_doc!=null)\n if(old_doc.owner != userCtx.name && userCtx.roles.indexOf(\"_admin\") < 0)\n throw({forbidden: \"Not your document\"});\n return;\n} \n"
}



Por supuesto, en _rev irá la revisión que toque. Este sistema es más potente de lo que parece ya que podemos controlar todos los detalles de la operación. Es por ello que muchas veces la función validate_doc_update es la más compleja de los documentos de diseño. Para ayudarme un poco, estoy usando Node.js para leer archivos JavaScript y pasarlos a una cadena JSON válida.

CORS y SSL


Vamos a lanzar CouchDB a Internet. En primer lugar, un par de detalles. CouchDB idealmente vivirá en un dominio separado a la web estática. Para que las peticiones JavaScript funcionen hay que activar CORS. También vamos a proteger los datos así como los usuarios y contraseñas transmitidos. Todo esto podemos hacerlo del tirón con nginx:

 


server {
listen 443 ssl http2;
ssl_certificate /etc/letsencrypt/live/alejandria.adrianistan.eu/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/alejandria.adrianistan.eu/privkey.pem;
server_name alejandria.adrianistan.eu;
location / {
add_header Access-Control-Allow-Origin *;
proxy_pass http://localhost:5984;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Ssl on;
}

location /_utils/ {
return 404;
}
}


¡Listo! Ya tenemos una instancia de CouchDB directamente conectada a Internet. Como podéis comprobar es una alternativa muy interesante en muchos casos. Sin embargo, ciertas cosas son un poco diferentes y más complejas de realizar. Mencionar que CouchDB también es posible usarse con un servidor de por medio en un esquema tradicional. En este caso, podemos usar la cuenta de admin para crear bases de datos personales para cada usuario. Este enfoque es necesario si necesitamos que los datos sean privados o queremos afinar más que roles tienen acceso a una base datos. CouchDB anima a crear tantas bases de datos como necesitemos, varias por usuario si es necesario, para satisfacer nuestros requerimientos. Esto no tiene gran impacto en el rendimiento tal y como está diseñado.

Puede hacerse de forma ligera, con una pequeña app que gestione el alta de usuarios y una vez creada la base de datos y ajustados los permisos se puede usar la API REST de CouchDB desde el lado del cliente con total normalidad, con protecciones de lectura y escritura más precisas.

Este modelo que hemos presentado es ideal para aplicaciones cuya base de datos principal sea pública y accesible y con unos pequeños ajustes puede adaptarse a muchas situaciones. Espero que os haya gustado el artículo. Nos hemos dejado cosas como la replicación entre nodos, los ficheros binarios o attachments ydos complementos a las vistas llamados show y lists, que permiten renderizar HTML o XML entre otras cosas, con los datos de salida de una vista si es necesario.

 

Comentarios

jose de jesus ramon
por que couchdb no te permite crear otros usuarios con un usuario administrador que creaste con un usuario root?
aarroyoc
No estoy seguro de entender lo que preguntas, pero diría que CouchDB sí permite crear usuarios nuevos con diferentes cuentas de administrador.

Añadir comentario

Todos los comentarios están sujetos a moderación