As I built my first Rails project with multiple models and “many-to-many” relationships, I became best friends with the ActiveRecord Associations Class Methods documentation. There were a few situations where I knew I set up the associations correctly, but I was still getting errors like, “Did you mean #users_trips
, not #users_trip
?” I needed a better understanding of the methods my objects and classes had access to through their ActiveRecord associations. I quickly learned that #build
just doesn’t work for everything, so I dug into the docs to figure out why and what other method I could use instead.
In this case, I was concerned about three models (Trips, DestinationsTrip, and Destinations). They were related via a many-to-many relationship, with DestinationsTrip as the join model. The problem arose when I was building the form for creating a new trip with :destination_ids
nested in the form.
I wanted to add a collection_select
field that allowed users to select multiple destinations for the new trip. I pre-populated the Destinations table with data from Atlas Obscura’s website, so I didn’t need to create new destinations from scratch.
I tried the code below in my trips controller.
def create
@trip = @user.trips.build(trip_params)
@trip.destinations.build(trip_params)
if @trip.save!
. . .
end
private
def trip_params
params.require(:trip).permit(:title, :start_date, :end_date, user_ids:[], destination_ids: [])
end
However, my app was returning errors, such as “undefined attribute :title
for Destination,” or “No Method Errors” related to :destination_ids
.
In response, I started digging through the “has_many” association class methods, which the Trips class inherits from ActiveRecord. I stumbled upon the following method:
#collection_singular_ids=ids
In my app, the method is represented as:
@trip.destination_ids=(trip_params[:destination_ids])
I plugged it into the #create action, and. . . .
def create
@trip = @user.trips.build(trip_params)
if @trip.save!
@trip.destination_ids=(trip_params[:destination_ids])
. . .
end
private
def trip_params
params.require(:trip).permit(:title, :start_date, :end_date, user_ids:[], destination_ids: [])
end
. . . it worked! But, why?
#collection_singular_ids=ids
replaces the destinations collection with destination objects. How do we get destination objects? They are identitied by the primary keys in the :destination_ids
array.
Then, behind the scenes, ActiveRecord calls upon another has_many association class method, #collection=objects
. This method creates the association between the trip and the destination objects. In other words, records are created in the join table using the trip’s id and each destination object’s id. #collection=objects
adds or removes destinations from the trip.destinations
collection, by comparing the objects passed in vs. any existing objects in the trip.destinations
collection.
Understanding all of the methods your classes have access to is just one way you can demystify ActiveRecord/Rails “magic!”