homeASCIIcasts

238: Mongoid 

(view original Railscast)

Other translations: En Pt

Other formats:

Written by Juan Lupión

Hace unos meses, en el episodio 194 [verlo, leerlo], repasábamos MongoDB y MongoMapper. MongoMapper es una gema muy útil pero ahora existe una nueva alternativa llamada Mongoid que nos puede resultar interesante si estamos pensando en usar MongoDB con nuestra aplicación Rails. Mongoid destaca, entre otras cosas, por el cuidado aspecto de su sitio web y por lo detallado de su documentación. Muchos otros proyectos de código abierto podrían tomar nota en este aspecto.

Instalación de MongoDB

Si aún no hemos instalado MongoDB en nuestro sistema lo primero que tendremos que hacer es visitar la página de descargas de MongoDB para descargar los archivos apropiados. Si estamos usando Mac OS X podemos, en vez de esto, instalar MongoDB utilizando Homebrew. Una vez que esté instalado podemos comprobar que MongoDB está arrancado visitando http://localhost:28017. Si vemos una página como la siguiente podemos estar seguros de que todo está funcionando correctamente.

La página que se ve cuando MongoDB está en ejecución.

Una nueva aplicación Rails con Mongoid

Una vez que tenemos MongoDB instalado, vamos a crear una aplicación Rails 3 con Mongoid. Siguiendo la tradición de las aplicaciones Rails de ejemplo, haremos un blog.

$ rails new blog

Lo primero que tenemos que hacer es añadir la gema Mongoid al Gemfile. Como a día de hoy la versión de Mongoid que soporta Rails 3 es la versión 2 y está todavía en beta, tendremos que especificar el número de versión.

/Gemfile

source 'http://rubygems.org'

gem 'rails', '3.0.1'
gem 'sqlite3-ruby', :require => 'sqlite3'

gem 'mongoid', '2.0.0.beta.19'
gem 'bson_ext'

También tenemos que añadir la gema bson_ext. BSON es una versión binaria de JSON, y esta gema instala varias extensiones en C para acelerar la serialización de BSON en Ruby. Podemos luego instalar las gemas de la forma habitual.

$ bundle

Una vez instaladas las gemas tenemos que ejecutar el generador de configuración de Mongoid para que pueda crear el fichero YAML de configuración.

$ rails g mongoid:config

A continuación se muestra el fichero por defecto. Podemos dejarlo tal cual mientras desarrollamos la aplicación.

/config/mongoid.yml

defaults: &defaults
  host: localhost
  # slaves:
  #   - host: slave1.local
  #     port: 27018
  #   - host: slave2.local
  #     port: 27019

development:
  <<: *defaults
  database: blog_development

test:
  <<: *defaults
  database: blog_test

# set these environment variables on your prod server
production:
  host: <%= ENV['MONGOID_HOST'] %>
  port: <%= ENV['MONGOID_PORT'] %>
  username: <%= ENV['MONGOID_USERNAME'] %>
  password: <%= ENV['MONGOID_PASSWORD'] %>
  database: <%= ENV['MONGOID_DATABASE'] %>

Ya tenemos todo en su sitio para empezar a desarrollar nuestra aplicación. Empezaremos creando el modelo Article con los campos name y content y utilizaremos el andamiaje de Rails para crear el código de la vista y el controlador asociados.

$ rails g scaffold article name:string content:text
    invoke  mongoid
    create    app/models/article.rb

Cuando creamos un modelo se invoca el generador propio de Mongoid, no el de ActiveRecord. Si abrimos el fichero del modelo veremos que se trata de una simple clase que incluye Mongoid::Document.

/app/models/article.rb

class Article
  include Mongoid::Document
  field :name, :type => String
  field :content, :type => String
end

Una diferencia entre esta clase y un modelo de ActiveRecord normal es que cada campo tiene que ser explícitamente definido junto con su tipo. El tipo por defecto es String por lo que en este caso particular podemos omitir los tipos.

/app/models/article.rb

class Article
  include Mongoid::Document
  field :name
  field :content
end

Nuestra aplicación está lista para ejecutarse. No tenemos que lanzar ninguna migración en la base de datos porque MongoDB no necesita esquema y podemos pasar todos los campos que queramos a un documento. Si visitamos la página /articles veremos la conocida página generada automáticamente por el generador de andamiajes y podremos crear un nuevo Article igual que si estuviéramos trabajando con ActiveRecord.

The articles page after a new article has been created.

Más campos

Una de las ventajas de utilizar una base de datos sin esquema es que es muy sencillo añadir campos a un modelo. Supongamos que nos olvidamos de añadir un campo para la fecha de publicación de un Article. Para añadirlo tan sólo tenemos que modificar la clase del modelo.

/app/models/article.rb

class Article
  include Mongoid::Document
  field :name
  field :content
  field :published_on, :type => Date
end

Para poder ver y modificar esta nueva fecha de publicación tendremos que modificar el código de vista y añadir el nuevo campo al formulario.

/app/views/articles/_form.html.erb

 <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.label :published_on %><br />
    <%= f.date_select :published_on %>
  </div>
  <div class="field">
    <%= f.label :content %><br />
    <%= f.text_area :content %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>

También en la vista de la acción show.

/app/views/articles/show.html.erb

<p id="notice"><%= notice %></p>

<p>
  <b>Name:</b>
  <%= @article.name %>
</p>

<p>
  <b>Content:</b>
  <%= @article.content %>
</p>

<p>
  <b>Published:</b>
  <%= @article.published_on %>
</p>


<%= link_to 'Edit', edit_article_path(@article) %> |
<%= link_to 'Back', articles_path %>

Ya podemos editar el artículo que ha sido recién creado y añadir una fecha de publicación.

Edición de la fecha de publicación de un artículo.

Validaciones

Mongoid se apoya en ActiveModel, lo que quiere decir que tenemos disponible mucha de la funcionalidad a la que estamos acostumbrados con ActiveRecord: validaciones, callbacks, seguimiento de atributos modificados, attr_accessible, etc. Esto hace que en Mongoid sea tan fácil añadir validaciones a los modelos como en ActiveRecord.

/app/models/article.rb

class Article
  include Mongoid::Document
  field :name
  field :content
  field :published_on, :type => Date
  validates_presence_of :name
end

Si ahora intentamos crear el artículo sin nombre recibiremos los mismos errores de validación que veríamos en un modelo ActiveRecord equivalente.

Validation errors work just as they do with ActiveRecord models.

Asociaciones

Todo blog necesita comentarios, por lo que crearemos un modelo Comment con su asociación para que cada artículo pueda tener varios comentarios. En Mongoid hay dos maneras de definir asociaciones. La primera es a través de una asociación mediante referencia. Se comporta de forma similar a las relaciones entre tablas en ActiveRecord y las bases de datos relacionales en que hay dos regitros separados que se relacionan mediante una columna id. La otra forma es mediante una asociación embebida que podría significar en nuestro caso que los comentarios se encuentran embebidos dentro del mismo documento que el artículo.

Para decidirnos por uno u otro enfoque deberíamos preguntarnos si alguna vez nos hará falta disponer de los registros asociados por sí mismos, o si siempre accederemos a ellos desde el modelo padre. En este caso sólo vamos a recuperar los comentarios a través del artículo asociado por lo que usaremos una asociación embebida. Definiremos la relación en la clase Article utilizando el método embeds_many.

/app/models/article.rb

class Article
  include Mongoid::Document
  field :name
  field :content
  field :published_on, :type => Date
  validates_presence_of :name
  embeds_many :comments
end

A continuación generaremos el modelo Comment.

$ rails g model comment name:string content:text

En esta nueva clase Comment podemos definir la relación con Article.

/app/models/comment.rb

class Comment
  include Mongoid::Document
  field :name
  field :content
  embedded_in :article, :inverse_of => :comments
end

embedded_in se usa para definir la relación de comentarios con un artículo. La opción inverse_of es necesaria para que Mongoid sepa a través de qué relación debe embeberse el comentario.

Para crear comentarios para cada artículo y verlos deberemos crear un controlador llamado CommentsController y sus vistas, pero antes de eso vamos a alterar el fichero de rutas. En el caso de asociaciones embebidas como estas por lo general nos interesará usar rutas anidadas porque siempre se accede al objeto hijo a través de su padre: anidaremos el recurso de los comentarios debajo de los artículos.

/config/routes.rb

Blog::Application.routes.draw do
  resources :articles do
    resources :comments
  end
end

Luego generaremos el controlador.

$ rails g controller comments

En el controlador vamos a escribir una acción create para que podamos crear nuevos comentarios para un artículo. Esta acción encontrará el artículo basándose en el parámetro article_id, le añadirá el nuevo comentario y luego redirigirá a la página del artículo.

/app/controllers/comments_controller.rb

CommentsController < ApplicationController
  def create
    @article = Article.find(params[:article_id])
    @comment = @article.comments.create!(params[:comment])
    redirect_to @article, :notice => "Comment created!"  
  end
end

Finalmente añadiremos algo de código a la vista de show del artículo para que muestre los comentarios de un artículo y permita añadir comentarios.

/app/views/articles/show.html.erb

<% if @article.comments.size > 0 %>
  <h2>Comments</h2>
  <% for comment in @article.comments %>
    <h3><%= comment.name %></h3>
    <p><%= comment.content %></p>
  <% end %>
<% end %>

<h2>New Comment</h2>

<%= form_for [@article, Comment.new] do |f| %>
  <p><%= f.label :name %> <%= f.text_field :name %></p>
  <p><%= f.text_area :content, :rows => 10 %></p>
  <p><%= f.submit %></p>
<% end %>

Si ahora visitamos la página de un artículo podremos crear un nuevo comentario y tras enviarlo aparecerá debajo del artículo.

Comentarios a un artículo.

Si miramos el log de desarrollo veremos las consultas a MongoDB. Esta es la consulta que se hizo cuando creamos el comentario anterior.

MONGODB blog_development['articles'].update({"_id"=>BSON::ObjectId('4cd01fa4a74209eacc000003')}, 
{"$push"=>{"comments"=>{"_id"=>BSON::ObjectId('4cd04c74a74209ecb4000002'), 
  "name"=>"Eifion", "content"=>"I agree."}}})

La consulta actualiza el modelo del artículo y le añade un atributo de comentario (recordemos que los comentarios no se almacenan de forma separada). Esto quiere decir que si abrimos la consola de Rails y contamos todos los comentarios daremos con resultado inesperado.

> Comment.count
 => 0

Los comentarios que hemos embebido en los artículos no están disponibles a nivel global. Para acceder a ellos tenemos que hacerlo siempre a través del artículo al que están asociados.

>   Article.first.comments.count
 => 1

Como en nuestra aplicación los comentarios son registros embebidos, para llegar a los atributos de un comentario siempre tendremos que recuperar dicho comentario a través de la asociación.

Asociaciones referenciales

Si queremos que los registros asociados también estén disponibles como documentos separados tendremos que crear una asociación basada en referencias. Demostraremos su uso modificando nuestra aplicación para poder asociar cada artículo con su autor. Primero generaremos un andamiaje para el modelo Author.

$ rails g scaffold author name:string

Antes de demostrar el uso de estas asociaciones le vamos a echar un vistazo a una característica interesante de Mongoid. Si añadimos el método key a una clase de modelo de Mongoid, dicha clave se utilizará como el id para identificar dicho modelo. Haremos que el atributo name sea la clave de Author.

/app/models/author.rb

class Author
  include Mongoid::Document
  field :name
  key :name
end

Si ahora creamos un autor seremos redirigidos a la página de dicho autor, donde veremos el id en la URL.

La página de un autor muestra el nombre coo su id.

Si vamos a usar esta funcionalidad tendremos que asegurarnos de que el campo escogido como clave no sea editable de forma que el documento tenga como id una cadena permanente que no cambie durante el período de vida del documento.

Volvamos a las asociaciones. En la clase Author utilizaremos references_many para definir las relaciones con los artículos.

/app/models/author.rb

class Author
  include Mongoid::Document
  field :name
  key :name
  references_many :articles
end

Luego en el modelo Article utilizaremos referenced_in.

/app/models/article.rb

class Article
  include Mongoid::Document
  field :name
  field :content
  field :published_on, :type => Date
  validates_presence_of :name
  embeds_many :comments
  referenced_in :author
end

Ya podemos utilizar dicha asociación igual que haríamos con ActiveRecord. En nuestro formulario para editar un artículo podemos añadir un collection_select para poder escoger un autor cuando creemos o actualicemos un artículo.

/app/views/articles/_form.html.erb

<div class="field">
  <%= f.label :author_id %><br />
  <%= f.collection_select :author_id, Author.all, :id, :name %>
</div>

Si ahora modificamos nuestro artículo y escogemos un autor podremos ver cuando lo examinemos por consola que el id del autor queda embebido en el artículo.

> Article.first
 => #<Article _id: 4cd01fa4a74209eacc000003, name: "Mongoid", content: "it's awesome!", published_on: 2010-11-02 00:00:00 UTC, author_id: "eifion-bedford">

Sin embargo, al contrario que con la asociación de los comentarios, podemos acceder a los autores de manera separada.

> Author.first
 => #<Author _id: eifion-bedford, name: "Eifion Bedford">

Con esto concluimos este episodio dedicado a Mongoid. Por supuesto todavía nos queda mucho por ver, pero la documentación es bastante completa y en ella podremos encontrar todo lo que necesitaremos conocer para utilizar Mongoid y MongoDB en nuestras aplicaciones Rails.