Ruby
Article

How to Save Multiple Checkbox Values to a Database in Rails

By James Hibbard

Imagine you have a form in your Rails app which is backed by an ActiveRecord model. In this form there are a bunch of checkboxes, the values of which you want to persist to your database. How do you go about handling this scenario?

As ever, the code for this article can be found on our GitHub repo.

The Anti-Pattern

Well, an initial reaction might be to create a string column in the database to hold all of the checkbox data. You could then use a before_save hook in the model to build the string and use check_box_tag helpers in the view to display the check boxes.

Let’s have a quick look at what this might look like. To do so, we’ll create a demo app into which you can enter the name of a professor and select their various areas of expertise.

rails new cb-demo && cd cb-demo
rails g scaffold professor name:string expertise:string
rake db:migrate

After that open up /app/views/professors/_form.html.erb and replace:

<%= f.label :expertise %><br>
<%= f.text_field :expertise %>

with:

<%= label_tag 'expertise_physics', 'Physics' %>
<%= check_box_tag 'professor[expertise][]', 'Physics', checked('Physics'), id: 'expertise_physics' %>

<%= label_tag 'expertise_maths', 'Maths' %>
<%= check_box_tag 'professor[expertise][]', 'Maths', checked('Maths'), id: 'expertise_maths' %>

In /app/controllers/professors_controller.rb alter:

params.require(:professor).permit(:name, :expertise)

to:

params.require(:professor).permit(:name, expertise:[])

Then in /app/models/professor.rb add:

before_save do
  self.expertise.gsub!(/[\[\]\"]/, "") if attribute_present?("expertise")
end

And in /app/helpers/professors_helper.rb add:

def checked(area)
  @professor.expertise.nil? ? false : @professor.expertise.match(area)
end

Finally, run rails s and navigate to http://localhost:3000/professors

And as you can see, it works. But unfortunately, that’s about all it does. Saving checkbox data to the database this way will just cause problems further down the road. For example as the number of professors and the number of areas of expertise grow, queries to find out which profs are assosciated with which areas will become a horrific mess.

Also what happens if you want to delete or rename an area of expertise? In this case you’d have to manipulate the database directly, which is almost never a good thing (not to mention time consuming and error-prone).

The Right Way

Luckily, there is a much better way to accomplish this — namely by moving Expertise into its own model and declaring a has_and_belongs_to_many association between Expertise and Professor. This will create a direct many-to-many connection between the models (by means of a join table — a database table that maps two or more tables together by referencing the primary keys of each data table).

As the Rails guide states:

A has_and_belongs_to_many association creates a direct many-to-many connection with another model, with no intervening model. For example, if your application includes assemblies and parts, with each assembly having many parts and each part appearing in many assemblies, you could declare the models this way:

You can visualize it like so (where expertises_professors is the join table):

Diagram illustrating HABTM association

The join table does not have a primary key or a model associated with it and must be manually generated.

To demonstrate this, we’ll recreate the same small project as before:

rails new cb-demo-1 && cd cb-demo-1
rails g scaffold professor name:string
rails g scaffold expertise name:string
rails g migration CreateJoinTableExpertiseProfessor expertise professor
rake db:migrate

This will create the necessary models and database tables. You might also notice the generator which produces the join table for us (provided JoinTable is part of the name). Nifty, huh?

Next we need to declare the associations in the respective models:

In /app/models/professor.rb add:

has_and_belongs_to_many :expertises

In /app/models/expertise.rb add:

has_and_belongs_to_many :professors

Declaring the has_and_belongs_to_many association puts a bunch of new methods at our fingertips, for example: Professor#expertises, Professor#expertises.find(id), Professor#expertises<< and Professor#expertises.delete. You can read more about these in the api documentation.

After that we need to whitelist expertise_ids in /app/controllers/professors_controller.rb:

params.require(:professor).permit(:name, expertise_ids:[])

Finally add the following to /app/views/professors/_form.html.erb:

<div class="field">
  <%= f.label "Area of Expertise" %><br />
  <%= f.collection_check_boxes :expertise_ids, Expertise.all, :id, :name do |b| %>
    <div class="collection-check-box">
      <%= b.check_box %>
      <%= b.label %>
    </div>
  <% end %>
</div>

Here we are making use of a special form options helper, which was introduced in Rails 4, called collection_check_boxes. It behaves similarly to collection_select, but instead of a single select field it renders a checkbox and a label for each item in the collection.

You can also customize the output (as we are doing here) by passing it a block. The block will be called with a special builder object that itself has a handful of special methods.

And that’s really all there is to it. If you fire up WEBrick and navigate to http://localhost:3000/expertises, you’ll be able to enter a few areas of expertise and save them to the database. After that, you can head to http://localhost:3000/professors and everything should work as expected — that is, you’ll be able to create professors and assign them to whichever areas of expertise you created previously. After that, if you try and edit a professor, you’ll see that the areas of expertise have been persisted.

Everyone’s a Winner, Baby!

With things set up this way, ascertaining which professors are associated with which areas of expertise is a doddle:

rails c
e = Expertise.first

e.professors
=> #<ActiveRecord::Associations::CollectionProxy [#<Professor id: 1, name: "Jim", created_at: "2015-08-20 20:04:51", updated_at: "2015-08-20 20:04:51">]>

e.professors.count
=> 1

Sorted!

And it’s quite straight-forward to add, rename or delete areas of expertise, via our simple web interface. Which brings me on to a final point: the chances are that if you delete an area of expertise, then you probably no longer want any professors to be associated with it. With our original method, this would have been messy to implement, however with this approach, all we need do is set dependent: :destroy on the association:

In /app/models/expertise.rb add:

has_and_belongs_to_many :professors, dependent: :destroy

Now, if you delete an area of expertise, no professors will be associated with it. The same thing obviously works the other way round.

Taking it Further

To further demonstrate the flexibility of this method, let’s finish with a demo showing how to use our checkboxes to filter database search results. To do that we’ll need some kind of search functionality for which we’ll use the Ransack gem. This gem provides excellent helpers and builders for handling searches on your models. To find out more about Ransack, check out: Advanced Search with Ransack

This demo builds on the code from the previous demo.

First add Ransack to your gemfile:

gem 'ransack'

and run:

bundle install

With that done, we’ll need to alter the index action in ProfessorsController, which is where we want to add our search functionality. We can make a search object here by calling Professor.search and passing in the q parameter, which will contains a hash of the search parameters submitted by the user. To get any professors matching our search, we can just call result on this object. Specifying distinct: true avoids returning duplicate rows.

In /app/controllers/professors_controller.rb:

def index
  @search = Professor.search(params[:q])
  @professors = @search.result(distinct: true)
end

Next we need to make the search form. Ransack provides a form builder for doing this called search_form_for. Just like Rails form_for this method takes a block in which we can define the fields we want to search against. Naming the text field in the form :name_cont means that Ransack will search for professors whose name contains the value entered into this field.

<%= search_form_for @search do |f| %>
  <%= f.label :name_cont, "Name contains" %>
  <%= f.text_field :name_cont %>
<% end %>

Next we need to add the ability to filter by area of expertise:

<%= f.label "Area of Expertise" %><br />
<%= f.collection_check_boxes :expertises_id_in_any, Expertise.all, :id, :name do |b| %>
  <div class="collection-check-box">
    <%= b.check_box %>
    <%= b.label %>
  </div>
<% end %>

You can see that we are again making use of the collection_check_boxes form helper, but this time we are passing in :expertises_id_in_any as a second parameter. This is what Ransack refers to as a predicate which it will use in its search query when determining what information to match. You can read more about predicates in the Ransack wiki.

Here’s the complete form. Add this to /app/views/professors/index.html.erb:

<fieldset class="search-field">
  <legend>Search Our Database</legend>
  <%= search_form_for @search do |f| %>
    <div class="field">
      <%= f.label :name_cont, "Name contains" %>
      <%= f.text_field :name_cont %>
    </div>

    <div class="field">
      <%= f.label "Area of Expertise" %><br />
      <%= f.collection_check_boxes :expertises_id_in_any, Expertise.all, :id, :name do |b| %>
        <div class="collection-check-box">
          <%= b.check_box %>
          <%= b.label %>
        </div>
      <% end %>
    </div>

    <div class="actions"><%= f.submit "Search" %></div>
  <% end %>
</fieldset>

While we’re at it, we can also alter the table (in the same file) to include a professor’s areas of expertise:

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Expertises</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @professors.each do |professor| %>
      <tr>
        <td><%= professor.name %></td>
        <td><%= professor.expertises.map(&:name).sort.join(", ") %></td>
        <td><%= link_to 'Show', professor %></td>
        <td><%= link_to 'Edit', edit_professor_path(professor) %></td>
        <td><%= link_to 'Destroy', professor, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

Now we can search for professors by name and filter them by their areas of expertise. Can you imagine how complicated this would have been if had the checkbox values stored in a string column in the database?

Note: a slight caveat is that Ransack doesn’t produce the correct results for _all queries on a HABTM association. In the above example expertises_id_in_all would return an empty result set (meaning you cannot match only those professors whose area of expertise corresponds exactly to the boxes you have checked). For more information on this, you can read this Stackoverflow thread and this issue on the project’s homepage.

Conclusion

With that, I hope I have demonstrated how to correctly store checkbox values in a database in Rails and the numerous of advantages this approach brings.

If you have any questions or comments, I’d be glad to hear them below and don’t forget, the code for this article can be found on our GitHub repo.

  • soulcutter

    I always prefer `has_many through: ` to HABTM relationships. Many times the join table ends up being a good place to store additional data about the relationship (e.g. years_of_experience for expertises), but also it gives you a handle to the join table that can be used to do interesting queries without resorting to writing SQL in your rails app.

    • James Hibbard

      Yup, fair point. My original version used a has_many through association, but I changed it to HABTM as I thought it made things simpler. If you want to store additional data about the relationship then has_many through is a better choice.

      • http://careersreport.com anderd_oliver

        Allow me) to show you a fantastic ways to earn a lot of extra CASH by finishing basic tasks from your house for few short hours a day — See more info by visiting >MY_DISQUS_ACCOUNT

      • http://careersreport.com Cynthia Pullman

        Allow +me to show you a fantastic ways to earn a lot of extra money by finishing basic tasks from your house for few short hours a day — See more info by visiting >MY___{DISQUS}___ID::

    • Alexander Maslov

      In this case HABTM easily could be changed at any time in future to `has_many through` relationship.

      • soulcutter

        HABTM doesn’t have a unique id column IIRC, which is annoying to add on large datasets. But ehh, I guess. I just find it easier to go with has_many through from the outset – I just don’t ever have to think about it again.

  • Jesus

    How can I convert from the stored string into an array again?

    • James Hibbard

      Are you storing the checkbox values as a string?That’s not the best idea.

      You could do this (using String#split):

      rails c

      p = Professor.last
      =>

      p.expertise
      => "Physics, Maths"

      p.expertise.split(",")
      => ["Physics", " Maths"]

  • http://mt-davis.github.io/ mtdavis

    Great article! Would this method work with radio buttons as well?

    • James Hibbard

      It wouldn’t make much sense to use this method for radio buttons, as although they behave like checkboxes as far as parameters are concerned (i.e. they send through a name and value when checked), checking one radio button in a group will uncheck any previous selection. As such, you’ll not be able to have multiple values from the same group of radio buttons associated with a model.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.