Haciendo un MongoDB con transacciones

Llevo unos cuantos meses trabajando con MongoDB, una base de datos NoSQL basada en documentos estilo JSON. Es muy potente y muy sencilla de usar.

Lo que más me gusto desde el primer momento fue la no necesidad de definir el esquema de las tablas de antemano. Al estar basada en documentos, es posible ir creando la estructura de la base de datos a medida que crece la aplicación, lo cuál te da mucha agilidad y libertad en el proceso de desarrollo de un proyecto. También implica un riesgo, uno ha de tener las ideas muy claras y no ir implementando a lo loco, pero vaya, que ya somos mayorcitos y se supone que sabemos lo que hacemos. :-)

transaction

Pero para una aplicación con cierto nivel de complejidad hay una cosa que echo en falta: las transacciones. MongoDB está pensada principalmente para aplicaciones que necesitan lecturas y escrituras rápidas y proporciona sistemas sencillos para montar replicas y sharding que facilitan la escalabilidad de los sistemas. Vamos, que es ideal para la web. Pero si queremos aplicaciones que ejecuten procesos largos con mucha actividad en la base de datos, tener transacciones te garantiza mantener la integridad.

Existen algunos ejemplos de "transacciones" a pequeño nivel, pero son muy artesanales y no ayudan demasiado si los errores que se producen son a nivel de hardware (cortes de conexión, caída de servidores, etc). Así que se me ocurrió que quizá lo más fácil era diseñar un sistema similar a MongoDB pero que funcionara sobre una base de datos relacional con transacciones como MariaDB o PostgreSQL.

Este nuevo sistema se llama Mojo.

Tipo de datos

Las bases de datos relacionales usan unos tipos de datos que podríamos definir como estándar (char, integer, float, datetime, blob, etc). Sin embargo, MongoDB permite guardar y realizar búsquedas sobre tipos más complejos, como listas y diccionarios, por lo que para poder dar soporte a estos tipos es necesario usar un serializador. Al principio usé Pickle, pero como solamente sirve para Python pasé a usar msgpack. Ahora mismo es el serializador por defecto, pero es posible especificar el que uno quiera en el momento de realizar la conexión.

Estructura de las tablas

Para guardar documentos en una base de datos relacional necesitaremos, como mínimo, tablas con la siguiente estructura: id(varchar), name(varchar) y value(text). De esta forma, si queremos guardar el documento {"a": 1, "b": [1, 2]} en la colección pruebas, en la base de datos quedaría:

Table "pruebas"
IdNameValue
'abc''a''1'
'abc''b''[1, 2]'

El primer defecto que me encontré con este esquema es que no es posible hacer búsquedas numéricas, ya que los valores se guardan serializados, por lo que es necesario guardar el valor numérico en una nueva columna.

Table "pruebas"
IdNameValueNumber
'abc''a''1'1
'abc''b''[1, 2]'NULL

El siguiente problema que nos encontramos es la imposibilidad de indexar por campo. Al estar todos los campos del documento en el mismo campo de la base de datos, sean del tipo que sean, se hace imposible crear índices que sean eficaces. Creo que existen algunos motores que permiten índices condicionales pero no es lo normal, por lo que la única solución que se me ocurre es separar los campos del documento entre varias tablas, una por campo. Por lo que el documento de prueba ahora se guardaría así:

Table "pruebas$_id"
IdNameValueNumber
'abc''_id''abc'NULL
Table "pruebas$a"
IdNameValueNumber
'abc''a''1'1
Table "pruebas$b"
IdNameValueNumber
'abc''b''[1, 2]'NULL

Con este esquema queda todo bastante atado, pero algunas funcionalidades extra de MongoDB no quedan resueltas, y es que MongoDB permite buscar dentro de listas y diccionarios de forma totalmente transparente. Por ejemplo, si buscamos {"b": 1} la base de datos debería devolvernos el documento, ya que dentro de la lista [1, 2] se encuentra el número 1.

Para poder realizar estas búsquedas será necesario replicar algunos de los datos, de forma que el campo "b" del documento quedaría guardado de la forma:

Table "pruebas$b"
IdNameValueNumber
'abc''b''[1, 2]'NULL
'abc''b..0''1'1
'abc''b..1''2'2

Con este último esquema, si guardamos un nuevo documento {"b": {"x": 100, "y": 200}}, la base de datos quedaría de la forma:

Table "pruebas$_id"
IdNameValueNumber
'abc''_id''abc'NULL
'def''_id''def'NULL
Table "pruebas$a"
IdNameValueNumber
'abc''a''1'1
Table "pruebas$b"
IdNameValueNumber
'abc''b''[1, 2]'NULL
'abc''b..0''1'1
'abc''b..1''2'2
'def''b''{"x": 100, "y": 200"}'NULL
'def''b.x''100'100
'def''b.y''200'200

Final

De momento estoy usando este esquema para la implementación de Mojo. El objetivo es replicar lo máximo posible la funcionalidad de MongoDB pero usando motores de bases de datos "tradicionales".

El código se encuentra disponible en el repositorio de Mojo y todas las ideas y aportaciones son bienvenidas.