Code, hit the error, find a workaround, and watch your time

Posted by Nick Solony on June 1, 2020

Code, hit the error, find a workaround, and watch your time

I loved working on Rails project, as I come across rails code quite often at work.

My biggest challenge in working with this project was time and my own stubbornness. When I come across an issue, I can’t move on, until I resolve it and it’s almost impossible for me to give up. Well, lesson learned that’s a bad habit.

While working on my project I got an error while trying to use find_or_create_by and I’ll tell you about it in this post.

So as one part of my project, I had to use nested forms that require a class to accept nested attributes and separate writers for those attributes. As I was building my app (movie_db), my main class Movie had 3 belongs_to associations and each of those required nested_attributes.

class Movie < ApplicationRecord

  belongs_to :genres
  belongs_to :director, class_name:"Person", optional: true
  belongs_to :writer, class_name:"Person", optional: true

It’s always so much easier to build code, based on something you’ve used or seen before. But I wasn’t fortunate enough since I didn’t come across an example of writer method for nested_attributes for belong_to association.

I first started by building nested attributes in the form:

 <tr>
   <td><h3>Genres:</h3></td>
   <td><%= f.fields_for :genre do |genre_attributes|%></td>
     <td><%= genre_attributes.label :id, "Genre"%></td>
     <td><%=  genre_attributes.collection_select :id, @genres,:id,:name, include_blank: true %></td>
   <td>Or create a new genre:</td>
   <%= genre_attributes.label :name, "Genre Name:" %><td>
     <td><%= genres_fields.text_field :name %></td><%= genre_attributes.text_field :name %>    
   <% end %>
     </td>
     </tr>

Based on that form I was building custom writer for genre_attributes and tried using find_or_create_by and it looked like this:

def genres_attributes=(genres_attributes)
     if genres_attribute["name"].present?
        genre=Genre.find_or_create_by(genres_attributes)
        self.genres<genre
     end
end

My logic for the writer was pretty simple if the new Genre name field had any information, use find_or_create_by to find existing Genre in case the user didn’t realize it already exists or creates a new one. Create part worked like a charm and new Genre was created and dynamically assigned to the movie, but it could never find the existing genre and I was hitting an error or creating duplicate records. Side note: duplicates in the database drive me crazy! I struggled for hours trying to find a way to make my code work, and all that time yielded nil results. Eventually, I decided that instead of wasting time on figuring this mess out, I would find a workaround. I did some research and figured the easiest way around my problem would be to change the movie genre relationship and add a joint table. So I went ahead and created movie_genres table and in my movie class removed belongs_to :genres and added

has_many :movie_genres
has_many :genres, through: :movie_genres

From there it was simple few tweeks to the form:

<tr>
  <td><h3>Genres:</h3></td>
  <td><%= f.select :genre_ids, options_from_collection_for_select(@genres, :id, :name, f.object.genre_ids), {}, {:multiple => true} %></td> 
  <td>Or create a new genre:</td>
  <%= f.fields_for :genres, movie.genres.build do |genres_fields| %>
    <td><%= genres_fields.text_field :name %></td>
  <% end %>
</tr>

and genres_attributes writer

def genres_attributes=(genre_attributes)
   genre_attributes.values.each do |genre_attribute|
     if genre_attribute["name"].present?
       genre = Genre.find_or_create_by(genre_attribute)
      # self.genres << genre
       self.movie_genres.build(genre: genre)
     end
   end
 end

This code worked, and I had no issues with find_or_create_by here. I honestly don’t know the right answer, but it seems find_or_create_by doesn’t like belongs_to relationships.

Well, I was happy and ready to move on to the next step of new movie form, as hit the same roadblock, nested attributes that require custom writer, and it should make sense to use find_or_create_by. Well, I tried but got the same problem as before, yes to create, no to find.

This time, I knew I need a workaround and I found it. I broke find_or_create_by in 2 pieces and used find_by and then build_association methods. Along the way, I actually created a separate method for person_find_by(attributes) and used it in multiple places. But custom writer looked like this:

 def director_attributes=(director_attribute)
   if director_attribute[:name].present?
     if person=person_find_by(director_attribute)
       self.director=person
     else
      self.build_director(director_attribute)
     end
   end
 end

That worked, and I didn’t waste hours trying to figure out why find_or_create_by didn’t work. Hopefully one day, I’ll know the answer why it didn’t work, but in the end, my app is working without it.

When coding, if you can’t figure something out, don’t spend hours on it, make it work first, and then fix it later, but don’t stop on one thing and don’t waste hours as i did.