DEV Community

agandaur-ii
agandaur-ii

Posted on

A Little Rails Magic

Still new to the game here and loving how rails really makes your life just so much easier. I feel like I run into a lot of those moments while learning rails and find myself feeling like a wizard, as whatever rails is doing must be magic! Although it feels that way (and objectively would be much cooler if that was actually happening. I mean, how cool would it be to have your job title be "Ruby Wizard"!), rails is doing some pretty neat stuff in the background. I ran into the example below the other day and thought it would be beneficial to break down the magic so I can really understand whats going on so I can better utilize it in the future.

Let's start off with our schema:

create_table "pokemonabilities", force: :cascade do |t|
  t.integer "pokemon_id"
  t.integer "ablity_id"
end

create_table "pokemons", force: :cascade do |t|
  t.string "name"
  t.string "type"
end

create_table "abilities", force: :cascade do |t|
  t.string "name"
  t.string "description"
end

We've got a many-to-many relationship with Pokemon and abilities, so we have our join table to track those relationships. Once you have your models set up, the ActiveRecord relationships would look like this:

class Pokemon < ApplicationRecord
    has_many :pokemonabilities
    has_many :abilities, through: :pokemonabilities
end 

class Ability < ApplicationRecord
    has_many :pokemonabilities
    has_many :pokemons, through: :pokemonabilities
end

class Pokemonability < ApplicationRecord
    belongs_to :pokemon
    belongs_to :ability
end

All smooth sailing so far, but a small magic shoutout to rails being able to automatically change "ability" to "abilities" when necessary. Great, so we have our associations set up, so we can start making stuff. Better yet, let's have our users start making things. We can set up a basic controller that looks like this:

  def new
    @pokemon = Pokemon.new
  end

  def create  
      @pokemon = Pokemon.create(pokemon_params)
      redirect_to @pokemon
  end

  private

  def pokemon_params
      params.require(:pokemon).permit(:name, :type)
  end

Looking good. Now, let's create a form where our Pokemon-loving-users can create there very own Pokemon! We could start with something like this:

  <%= form_for @pokemon do |f| %>
      Name: <%= f.text_field :name %>
      Type: <%= f.text_field :type %>
      <%= f.submit %>
  <% end %>

Great! Now many fun new Pokemon can be born into the world. But now we have to go make something else to make sure these Pokemon have abilities. Why not just do it in the same form? We'll use the collection select function to give an easy list for our user to pick from. It's a pretty nifty option, which you can read more about here: https://guides.rubyonrails.org/form_helpers.html. And here's our new form:

  <%= form_for @pokemon do |f| %>
      Name: <%= f.text_field :name %>
      Type: <%= f.text_field :type %>
      Ability: <%= f.collection_select :ability_ids, Ability.all, :id, 
      :name%>
      <%= f.submit %>
  <% end %>

However, if we try to submit our form at this point, something odd happens. It will successfully create our new Pokemon, but it won't have any ability associated with it. What gives? Here is where the magic comes in! It comes down to updating our params. That is what the collection_select is giving us, so we just have to use it. Here's what the updated pokemon_params method will look like:

  def pokemon_params
      params.require(:pokemon).permit(:name, :type, :ability_ids)
  end

But if you are like me and got confused at this point, how on earth does just adding :ability_ids allow a Pokemon to be associated with those abilities? After all, we are passing our pokemon_params through our model to create a new instance, which looks essentially like this: Pokemon.create(:name, :type, :ability_ids). Name and type are present to make a Pokemon because that is how we set it up in our migration, but ability_ids aren't even on our Pokemon table. In fact, ability_ids doesn't show up on any of our tables, only ability_id. What's going on here?

Rails and ActiveRecord are doing something pretty cool here. When you create the new instance of a Pokemon, and passing in ability_ids, things are started to get checked in the background. When ability_ids is read, ActiveRecord is going to start checking "does this exist somewhere else?", since it recognizes it is not present on the Pokemon class. It will then check your associations to see if the value is present in a different class, and in this case, it will find that ability_id is associated with the Pokemonabilities class. Since it now has the Pokemon id and ability id, and we took the create action earlier, it creates a new instance of the Pokemonabilities class, which is now automatically associated with both our Pokemon and our chosen abilities!

Note that it is important, in the way these associations where set up, that you update your strong params to be the plural of the id you want to pass through. Even if you are only adding one ability at a time, since a Pokemon has the potential to have many abilities, ActiveRecord will be looking for a plural ids when creating things in this way.

So that's the magic! Although it is hopefully more clear how it's working in the background now, it's still fun to see how much work our app is doing for us so we can get on to more interesting things.

Top comments (0)