homeASCIIcasts

194: MongoDB y MongoMapper 

(view original Railscast)

Other translations: En Pt Cn It

Written by Juan Lupión

MongoDB es una base de datos basada en documentos cuya diferencia con respecto a las bases de datos relacionales (como MySQL) es que no tiene esquema. En este episodio veremos los fundamentos del uso de MongoDB con la gema MongoMapper para crear una aplicación Rails sencilla. La primera vez que muchos desarrolladores Rails oyeron hablar de MongoDB fue con esta excelente anotación de John Nunemaker en el blog RailsTips que contrastaba siete funcionalidades de MongoMapper y MongoDB con los sistemas relacionales tradicionales. Merece la pena leer ese post si nos interesa utilizar MongoDB.

Una de las características mencionadas en esa anotación es que con MongoDB no tenemos que usar migraciones porque básicamente es un motor de base de datos al que se le ha eliminado el esquema. Cada fila es su propio documento, lo que significa que puede tener cualquier conjunto de atributos distinto o no de las otras filas de la base de datos. Dado que no hay un esquema fijo si queremos podemos definirlo sobre la marcha.

Instalación de MongoDB y MongoMapper

Para desarrollar nuestra aplicación en primer lugar tendremos que instalarnos MongoDB. Existen descargas para diferentes plataformas en la página de descargas del sitio oficial, pero para los que usamos OS X Chris Kampemier ha escrito un muy buen artículo en su blog. Este artículo incluye un práctico archivo plist que sirve para poder arrancar MongoDB automáticamente en el arranque de nuestra máquina. Tenemos que asegurarnos de instalar la versión correcta (1.2.0) porque este artículo hace referencia a una versión anterior. Una vez que tengamos instalado y configurado MongoDB ya podemos visitar http://localhost:28017/ para ver si está funcionando.

Comprobando que MongoDB está levantado.

Creación de una aplicación Rails con MongoDB

Ahora que tenemos MongoDB funcionando podemos empezar con la creación de nuestra aplicación. Crearemos una nueva aplicación desde cero llamada todo.

rails todo

Vamos a utilizar la gema MongoMapper para que nuestra aplicación hable con MongoDB. Por tanto añadiremos la siguiente línea en el bloque de configuración correspondiente en /config/environment.rb.

/config/environment.rb

config.gem "mongo_mapper"

También tenemos que dar cierta información adicional que configuraremos en su propio archivo de inicialización. En el directorio /config/initializers vamos a crear un nuevo archivo llamado mongo_config.rb. Sólo tenemos que añadir aquí una línea para decirle a MongoMapper a qué base de datos se tiene que conectar.

/config/initializers/mongo_config.rb

MongoMapper.database = "todo-#{Rails.env}"

Al pasar el directorio actual como parte del nombre de la base de datos iremos creando diferentes bases de datos para nuestros entornos de desarrollo, pruebas y producción. Por supuesto para hacer un despliegue serio en producción tendríamos que tener en cuenta aspectos como la autenticación pero esto nos bastará para nuestros propósitos de demostración.

El último paso de la configuración de nuestra aplicación es la ejecución del siguiente comando para comprobar que tenemos la gema MongoMapper instalada.

sudo rake gems:install

Construcción de la Aplicación

Ya podemos empezar a desarrollar nuestra aplicación. Se trata de una sencilla lista de tareas donde tendremos un modelo Project que puede tener muchas Tasks. Para facilitarnos la tarea vamos a utilizar los Nifty Generators de Ryan Bates, aunque debemos tener en cuenta que podríamos escribir la aplicación sin utilizarlos.

Lo primero que haremos será crear un layout para la aplicación:

script/generate nifty_layout

A continuación generaremos el modelo Project y un scaffold a juego. Project tendrá un único campo, name, y dado que no estamos generando un modelo normal de ActiveRecord con un esquema pasaremos la opci ón --skip-migration para que no se genere ninguna migración.

script/generate nifty_scaffold project name:string --skip-migration

Esto nos generará un modelo, controlador y sus vistas. El modelo Project así generado será un modelo ActiveRecord así que tendremos que cambiarlo para que trabaje con MongoMapper.

/app/models/project.rb

class Project < ActiveRecord::Base
  attr_accessible :name
end

El código generado para el modelo Project.

Todo lo que tenemos que hacer es eliminar la herencia de ActiveRecord::Base y en su lugar incluir MongoMapper::Document.

Para definir los atributos del modelo utilizamos el método key. Le pasaremos el nombre del atributo, en este caso :name, y también un tipo en forma de una calse Ruby. Para nuestro atributo :name será String. Nuestro modelo tendrá ahora el siguiente aspecto:

/app/models/project.rb

class Project
  include MongoMapper::Document

  key :name, String
end

Con estos cambios que le hemos hecho al modelo ya podemos ejecutar nuestra aplicación y crear, actualizar y listar proyectos tal y como lo haríamos con una aplicación basada en una base de datos relacional sólo que en lugar de MySQL estamos utilizando MongoMapper y MongoDB.

Creando un nuevo proyecto en nuestra aplicación.

En términos de interfaz MongoMapper funciona de manera parecida a ActiveRecord: podemos buscar, crear, actualizar y eliminar registros como siempre, e incluso soporta validaciones igual que ActiveRecord, así que podríamos añadir

/app/models/project.rb

a nuestro Proyecto y ya no seríamos capaces de crear un proyecto sin un nombre asignado, pero con MongoMapper hay una forma mejor de añadir validaciones in situ, así que para hacer que el atributo nombre sea obligatorio podemos añadir :required => true a los parámetros del método key.

/app/models/project.rb

class Project
  include MongoMapper::Document

  key :name, String, :required => true
end

Validación en el modelo Project.

Más atributos

Como MongoDB es una base de datos sin esquema podemos fácilmente añadir o alterar los atributos de un modelo sin tener que ejecutar migraciones. Si, por ejemplo, queremos añadir un atributo priority a nuestro Project tan sólo tenemos que añadírselo al modelo.

/app/models/project.rb

class Project
  include MongoMapper::Document

  key :name, String, :required => true
  key :priority, Integer
end

Podemos manipular este nuevo atributo igual que lo haríamos en ActiveRecord, por lo que en el parcial del formulario de Project podemos añadir un menú desplegable que nos permita escoger un valor al crear o modificar un proyecto.

/app/views/projects/_form.html.erb

<% form_for @project do |f| %>
  <%= f.error_messages %>
  <p>
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </p>
  <p>
    <%= f.label :priority %><br />
    <%= f.select :priority, [1,2,3,4,5] %>
  </p>
  <p><%= f.submit "Submit" %></p>
<% end %>

Podemos modificar la vista show para mostrar la prioridad de un proyecto:

/app/views/projects/show.html.erb

<% title "Project" %>
<p>
  <strong>Name:</strong>
  <%=h @project.name %>
</p>
<p>
  <strong>Priority:</strong>
  <%=h @project.priority %>
</p>
<p>
  <%= link_to "Edit", edit_project_path(@project) %> |
  <%= link_to "Destroy", @project, :confirm => 'Are you sure?', :method => :delete %> |
  <%= link_to "View All", projects_path %>
</p>

Cuando visitemos la página para crear un proyecto veremos el menú para asignar la prioridad, que será mostrada una vez creado.

Ahora tenemos prioridades en nuestros proyectos.

Dado que antes de añadir el atributo priority ya habíamos creado un proyecto, podríamos preguntarnos qué prioridad tendrá asignada. Si miramos en ese proyecto veremos que la prioridad está vacía. Dado que no existe un valor de prioridad para el documento correspondiente en MongoDB tendrá un valor nil.

El primer proyecto tiene un valor de prioridad vacío.

Asociaciones

En nuestra lista de tareas queremos tener un modelo Task; cada Project tendrá muchas Tasks. Vamos a generar el scaffold igual que hicimos con Project pero nótese que el project_id es una cadena en lugar de un número entero (que es lo que utilizaríamos normalmente).

script/generate nifty_scaffold task project_id:string name:string completed:boolean --skip-migration

Al igual que con el modelo Project tenemos que modificar el archivo del modelo para que funcione con MongoMapper, eliminando el código específico de ActiveRecord.

/app/models/Task.rb

Una vez más hemos incluido MongoMapper::Document y utilizado el método key para definir los atributos del modelo. Cabría esperar que el atributo project_id tuviese un tipo Integer, pero en MongoDB los identificadores son de tipo ObjectId.

Hemos definido la relación entre Task y Project igual que lo haríamos con ActiveRecord utilizando belongs_to. En Project en lugar de utilizar como sería de esperar has_many :tasks utilizaremos many.

/app/models/project.rb

Ahora podemos ejecutar la aplicación y utilizar el controlador y las vistas generadas por el scaffold para crear nuevas tareas. Será un poco difícil asignar el identificador de un proyecto a las tareas porque se habrá generado un campo de texto para el campo project_id (esto es así porque lo definimos como un campo de tipo cadena cuando generamos el scaffold). Vamos a modificar la vista de forma que utilice un menú de selección permitiéndonos escoger uno de los proyectos existentes. Para esto vamos a utilizar collection_select igual que haríamos con un formulario de ActiveRecord para crear un menú desplegable que muestre todos los proyectos.

/app/views/tasks/_form.html.erb

Ahora será más fácil escoger un proyecto cuando creemos una nueva tarea.

Con collection_select podemos crear un menú de selección igual que con ActiveRecord.

Después de haber creado la nueva tarea seremos llevados a la página de dicha tarea donde veremos el id del proyecto. Sería mucho mejor si pudiésemos mostrar el nombre del proyecto en su lugar, así que cambiaremos @task.project_id por @task.project.name en la vista show.

/app/views/tasks/show.html.erb

<% title "Task" %>
<p>
  <strong>Project:</strong>
  <%=h @task.project.name %>
</p>
<!-- Rest of form -->

Ahora el formulario mostrará el nombre del proyecto asociado, igual que lo haría con un formulario basado en ActiveRecord.

Podemos ver los atributos de los modelos relacionados igual que con ActiveRecord.

Búsquedas en MongoDB

Vamos a terminar este episodio mostrando algunas técnicas para realizar búsquedas de modelos de Mongo en la consola. Por ejemplo, podemos encontrar todos los proyectos con Project.all

>> Project.all
=> [#<Project name: "Yardwork", _id: 4b39d8c9a175750357000001, priority: nil>, #<Project name: "Housework", _id: 4b39fbd1a175750357000002, priority: 3>]

También podemos encontrar un proyecto por su id

>> Project.find('4b39d8c9a175750357000001')
=> #<Project name: "Yardwork", _id: 4b39d8c9a175750357000001, priority: nil>

…o pasar opciones para encontrar los registros ordenados de cierta manera.

>> Project.all(:order => "name DESC")
=> [#<Project name: "Yardwork", _id: 4b39d8c9a175750357000001, priority: nil>, #<Project name: "Housework", _id: 4b39fbd1a175750357000002, priority: 3>]

Al igual que con ActiveRecord podemos pasar condiciones a find pero la diferencia con ActiveRecord es que las condiciones se pasan inline, o sea que encontraríamos todos los proyectos con una prioridad de 3 con:

>> Project.all(:priority => 3)
=> [#<Project name: "Housework", _id: 4b39fbd1a175750357000002, priority: 3>]

¿Y con condiciones más complicadas? Dado que Mongo no está basado en SQL no podemos pasar una cadena SQL en las condiciones. En vez de eso Mongo tiene su propio lenguaje para crear consultas más complejas. MongoMapper nos permite hacerlo pasándo un método a un símbolo. Por ejemplo, para recuperar todos los proyectos que tienen una prioridad de dos o más podríamos usar:

>> Project.all(:priority.gte => 2)
=> [#<Project name: "Housework", _id: 4b39fbd1a175750357000002, priority: 3>]

También podemos usar in, pasándole un array de valores para encontrar, por ejemplo, los proyectos con una prioridad de 2 o 3.

>> Project.all(:priority.in => [2,3])
=> [#<Project name: "Housework", _id: 4b39fbd1a175750357000002, priority: 3>]

La documentación de las condiciones de búsqueda es todavía un poco escasa pero podemos encontrar más información leyendo el fichero de tests en Github.

Eso es todo hoy. Tan sólo hemos cubierto lo más básico de MongoDB y MongoMapper así que tendremos que investigar por nuestra cuenta si queremos ir un poco más lejos. Hay una lista de correo a la que suscribirnos y podemos seguir a MongoDB en Twitter.

La gran pregunta que tenemos que hacernos es si deberíamos usar MongoDB en lugar de una base de datos relacional: nos toca decidir si MongoMapper y MongoDB son lo más adecuado para nuestro proyecto concreto. En todo caso, parece ser que a largo plazo las bases de datos basadas en documentos jugarán un papel muy importante en el desarrollo de aplicaciones Rails.