193: Tableless Model 

(view original Railscast)

Other translations: Es Cn It

One question that’s asked quite frequently is how to create a form in a Rails application without a database table behind it. We’ll show you how to do just that in this episode and to do so we’ll use the blog application that we’ve used in the previous couple of episodes.

Our blog application.

What we want to do is add a “Share this article” link on each article’s page. This link will take a user the a form on which they can enter inormation so that they can share an article via email. We don’t want to keep this information, just use it to send the email so how do we create a form and a model without a corresponding database table?

The approach we’ll take will be to create a normal model with a database table behind it and then modify the application to remove the table. We’ll start by generating a scaffold and for this we’ll use Ryan Bates’ nifty scaffold generator.

We have to create a new model as we’ll be submitting a form and creating a new resource. We’ll call the new model Recommendation as by emailing details about an article to someone we’re recommending it to them. The Recommendation model will have fields to hold who the email is from and to, the id of the article that’s being recommended and a field for a message. The related controller will need new and create actions. We can generate the scaffold with:

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

Next we’ll need to migrate the database to create the table, even though we don’t actually want the table (we’ll roll the migration back later).

rake db:migrate

Now that we have a model and a controller we’ll create a link to the new action in the RecommendationController, passing the id of the article that will be recommended.

  <%= link_to "Share this article", new_recommendation_path(:article_id => @article.id) %>
  <%= link_to "Back to Articles", articles_path %>

Adding the recommendation link to /app/views/articles/show.html.erb

Note that we need to pass the article’s id from the link to the new Recommendation so that it stays referenced.

Next we’ll move to the view that was generated by the scaffold and modify it to remove the article_id field and its label and replace them with a hidden field that holds the article_id.

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

Modifying /app/views/recommendations/new.html.erb

When we reload the article’s page now the “Share this article” link will be visible and when we click on it we’ll see the new form.

The new recommendation form.

If we were to fill in this form and submit it we’ll create a new recommendation in the database, but in this case we don’t want to save this form to the database just send an email. In Rails applications the create action is generally used to save a new model to the database but there’s nothing forcing us to do that. Instead we’ll just check to see if the new Recommendation is valid.

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

We’ll roll back the last migration now to drop the recommendations table and see if our form works without it. We’ll remove the migration file too.

rm db/migrate/*_recommendations.rb

Without the database table removed we get an error when we reload the form, the application complaining, not surprisingly, that it can’t find the recommendations table. This is because ActiveRecord relies on every model having an associated table.

The exception raised when we remove the table.

So how are we going to create a model that doesn’t have a database table behind it? There are several potential solutions including various plugins but we’re going to use the method described in an entry on the Code Tunes blog. This shows a techinque that involves overriding a couple of methods in an ActiveRecord model and then manually defining the columns in the model file rather than in the database table. In our Recommendation model we’ll add in the two overridden methods and then use the column class method to define the columns in a similar way to how they’re defined in a migration file.

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)
  column :from_email, :string
  column :to_email, :string
  column :article_id, :string
  column :message, :text  

Defining the columns in /app/models/recommendation.rb

When we reload the new recommendation page now we’ll see the form again instead of an error, but the columns are now defined in the model class rather than fetched from a database table.

You might be wondering why the recommendation class still inherits from ActiveRecord::Base when we’re not using a database backend. We could quite easily create a model class that isn’t based on ActiveRecord and have it work as Rails is quite decoupled from ActiveRecord, but there are advantages to keeping our model class inheriting from ActiveRecord. For one thing it means we can use its other features such as validations. We can then validate the format of the email addresses and the length of the message using ActiveRecord validations in the model.

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

Adding validation to /app/models/recommendation.rb

With these validations added we’ll see the same Rails error messages we’d see if we had a normal database-backed model if we submit the form with invalid email addresses.

The validation errors are shown when the email addresses are invalid.

Another reason to keep our tableless model inheriting from ActiveRecord is that we can still make use of associations. Recommendation has an article_id as one of its fields and so we can still use

belongs_to :article

in the model so that we can still fetch the related Article object whenever we need it.

We now have an ActiveRecord model that behaves much like any other but which doesn’t rely on a database backend as its columns are defined manually in Ruby. If we do accidentally call a method that hits the database and requires a database table we’ll see the same exception raised that we saw earlier saying that the table doesn’t exist and can work around it.

The one part of the application that remains to be done is the code that sends an email when the recommendation form has been fillled in correctly. We won’t write that now as its out of the scope of this episode but if you want to know how it’s done take a look at episode 61 which covers this topic.

While being able to create tableless models is a useful technique you should ask yourself whether you really don’t want to store records in the database. It’s so easy to store user-submitted data like this in a database table that there has to be a good reason not to do so even if you don’t have any immediate use for it. If nothing else it serves as a backup if our emailing system fails.