homeASCIIcasts

193: Modelos sin tablas 

(view original Railscast)

Other translations: En Cn It

Written by Juan Lupión

Una pregunta recurrente es cómo crear un formulario en una aplicación Rails sin una tabla por detrás en la base de datos. Mostraremos como hacerlo en este episodio, para lo cual vamos a utilizar la aplicación de blog que hemos estado viendo en los episodios anteriores.

Nuestra aplicación
de blog.

Lo que queremos hacer es añadir un enlace del tipo "Compartir este artículo" en la página de cada artículo. Este enlace llevará al usuario a un formulario en el cual pueden introducir la información necesaria para compartir el artículo por e-mail. No queremos almacenar esta información, tan sólo queremos utilizarla para enviar el e-mail, así que ¿cómo creamos un formulario y un modelo sin su correspondiente tabla en la base de datos?

El enfoque que vamos a seguir será crear un modelo normal con una base de datos y después modificaremos la aplicación para que funcione sin la tabla en la base de datos. Empezaremos generando un scaffold, para lo cual utilizaremos el nifty scaffold generator de Ryan Bates.

Tenemos que crear un nuevo modelo porque enviaremos un formulario y crearemos un nuevo recurso. A este nuevo modelo lo vamos a llamar Recommendation porque al enviar detalles a alguien sobre un artículo en realidad se lo estamos recomendando. El modelo Recommendation tendrá campos para decir de quién es el remitente y el destinatario del e-mail, el identificador del artículo recomendado y un campo para un mensaje. El controlador correspondiente necesitará las acciones new y create. Podemos generar nuestro scaffold de esta manera:

script/generate nifty_scaffold recommendation from_email:string to_email:string article_id:integer message:text new create

Luego tenemos que migrar la base de datos para crear la tabla, aunque realmente no la queremos (luego daremos marcha atrás a esta migración).

rake db:migrate

Ahora que ya tenemos un modelo y un controlador crearemos un enlace a la acción new del controlador RecommendationController, pasando el id del artículo que será reocmendado:

<p>
  <%= link_to "Share this article", new_recommendation_path(:article_id => @article.id) %>
  <%= link_to "Back to Articles", articles_path %>
</p>Cómo añadir el enlace de recomendación a /app/views/articles/show.html.erb

Obsérvese que tenemos que propagar el identificador de artículo del enlace a la nueva Recommendation para que la referencia sea correcta.

A continuación pasaremos a la vista que ha sido generada por el scaffold y la modificaremos para quitar el campo article_id y su etiqueta, reemplazándolos por un campo oculto donde pondremos article_id.

<% title "New Recommendation" %>
<% form_for @recommendation do |f| %>
  <%= f.error_messages %>
  <%= f.hidden_field :article_id %>
  <p>
    <%= f.label :from_email %><br />
    <%= f.text_field :from_email %>
  </p>
  <p>
    <%= f.label :to_email %><br />
    <%= f.text_field :to_email %>
  </p>
  <p>
    <%= f.label :message %><br />
    <%= f.text_area :message %>
  </p>
  <p><%= f.submit "Submit" %></p>
<% end %>
<p><%= link_to "Back to List", recommendations_path %></p>

Modificaciones en /app/views/recommendations/new.html.erb

Cuando recarguemos la página de un artículo, veremos que ahora aparece el enlace "Share this article” y cuando hagamos clic en él veremos el nuevo formulario.

El nuevo formulario
de recomendaciones.

Si rellenásemos este formulario y lo enviásemos crearíamos una nueva recomendación en la base de datos, pero en este caso no queremos guardar nada en la base de datos; tan sólo queremos enviar un e-mail. En las aplicaciones Rails la acción create se usa por lo general para guardar un nuevo modelo en la base de datos pero no hay nada que nos obligue a hacerlo. En su lugar, tan sólo comprobaremos que la nueva Recommendation sea válida.

def create
  @recommendation = Recommendation.new(params[:recommendation])
  if @recommendation.valid?
    flash[:notice] = "Successfully created recommendation."
    redirect_to @recommendation
  else
    render :action => 'new'
  end
end

Daremos marcha atrás a la última migración para elminar la tabla de recomendaciones de la base datos y comprobaremos si nuestro formulario funciona sin ella. También eliminaremos el archivo con la migración.

rm db/migrate/*_recommendations.rb

Cuando recargamos el formulario tras eliminar la tabla recibiremos un error. La aplicación se queja, y no es de extrañar, de que no puede encontrar la tabla de recomendaciones. Esto es así porque ActiveRecord depende de que cada modelo tenga una tabla asociada.

Se eleva una
excepción tras haber eliminado la tabla.

Así pues, ¿cómo vamos a crear un modelo que no tiene su correspondiente tabla en la base de datos? Hay varias soluciones posibles, incluyendo varios plugins, pero vamos a utilizar elmétodo descrito en esta entrada del blog Code Tunes donde se nos meustra un técnica que implica sobrecargar un par de métodos de un modelo ActiveRecord y después definir definir manualmente las columnas en el código del modelo en lugar de tenerlas en la base de datos. En nuestro modelo Recommendation añadiremos un método para definir las columnas de una manera similar a como estaría definidas en un archivo de migración.

class Recommendation < ActiveRecord::Base
  def self.columns() @columns ||= []; end

  def self.column(name, sql_type = nil, default = nil, null = true)
    columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
  end

  column :from_email, :string
  column :to_email, :string
  column :article_id, :string
  column :message, :text  
end

Definición de las columnas en /app/models/recommendation.rb

Cuando recarguemos la página de nueva recomendación veremos de nuevo el formulario en lugar de un error, pero las columnas están ahora definidas en el modelo en lugar de ser recuperadas de una tabla de la base de datos.

Podríamos preguntarnos por qué la clase de recomendación aún sigue heredando de ActiveRecord::Base si ya no estamos usando una base de datos por detrás. Sería muy sencillo crear una clase de modelo que no estuviera basada en ActiveRecord porque Rails en sí está bastante desacoplado de ActiveRecord pero seguir usando ActiveRecord nos proporciona ciertas ventajas. De entrada podemos seguir usando las validaciones, por ejemplo para comprobar el formato de la dirección de correo y la longitud del mensaje.

validates_format_of :from_email, :to_email, :with => /^[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
validates_length_of :message, :maximum => 500

Adición de validación para /app/models/recommendation.rb

Con estas validaciones si enviamos el formulario con direcciones de correo no válidas veremos los mismos mensajes de error de Rails que se habrían producidos si estuviésemos con un modelo respaldado por la base de datos.

Se muestran errores
de validación cuando las direcciones de correo no son válidas.

Otra razón para seguir heredando de ActiveRecord es que podemos utilizar asociaciones. Recommendation tiene un article_id como campo, así que podemos seguir utilizando

belongs_to :article

en el modelo para poder seguir recuperando el Article relacionado siempre que lo necesitemos.

Ahora tenemos un modelo de ActiveRecord que se comporta como cualquier otro pero que no depende de la base de datos porque sus columnas están definidas manualmente con Ruby. Si accidentalmente llamamos a un método que toca la base de datos y requiere la tabla veremos la misma excepción de antes diciendo que dicha tabla no existe y podremos solucionarlo.

La única parte de la aplicación que nos queda por hacer es el código que envía un correo cuando se rellena correctamente el formulario de recomendación. No lo escribiremos ahora porque se sale del alcance de este episodio pero si quieren saber cómo se hace basta con ver el episodio 61 que trata de ello.

Si bien crear modelos sin tabla es una técnica bastante útil, siempre deberíamos preguntarnos si realmente no queremos guardar registros en la base de datas. Es tan fácil guardar los datos generados por los usuarios en una base de datos que tiene que haber una razón muy poderosa para no hacerlo incluso cuando no tengamos un uso inmediato para ellos. Como mínimo en este caso serviría como copia de respaldo si fallase nuestro sistema de correos.