Because I didn’t. Or at least I didn’t think much about it. And dammit, I should’ve.
As you can probably tell, I spent a long time trying to figure out a very simple problem. This is one of those posts that is inspired by the (vain) hope that maybe, just maybe, I can spare someone else a few hours of confusion so they can do better things, like go outside and eat ice cream or something.
Here’s the (simplified) situation. The user can select a variety of widgets via checkboxes in a form. The information gets passed as widget ids to the controller, which triggers a method that updates the assocation of a user’s widgets:
### WidgetsController ### def updatewidgets @user.update_active_widgets(params[:widget_ids]) if request.post? redirect "/" end ### User Model ### has_many :widgets def update_active_widgets(new_widget_ids) current_widget_ids = self.widget_ids # return an array of widget ids that the user no longer wants # unbound_widget_ids = current_widget_ids - new_widget_ids # Delete all unbound widgets... save new widgets....etc...etc...# end
Ok, who can see why this deletes all of the user’s widgets every time we run this script? Hint: I already told you in the title of this post.
Since the params[:widget_ids] is coming from a POST request, every item in the array is going to be a string, even if it was listed as a number. So, if the user selected widget_ids of 1, 2, and 3, the incoming params[:widget_ids] array would look like this:
params[:widget_ids] = ["1","2","3"]
In my method, update_active_widgets, I take a set difference between two arrays to find which of the current widget_ids associated with the user are no longer selected by the user. But the when I pull out the current widget ids using self.widget_ids, I get an array of integers:
self.widget_ids = [1,2,4,6]
So when I take the set difference between an array of strings and an array of integers, Ruby understands them to be totally different, “1″ != 1. So what I was doing was this:
unbound_widget_ids = [1,2,4,6] - ["1","2","3"] # returns the original set [1,2,4,6]
Thus, I’ve just told Rails that every current widget_id is unbound, and should be deleted. Since Rails is smart enough to automatically convert a number-string to a number in ActiveRecord, there was never a problem adding widgets. My unit tests were even passing fine, because I kept entering my widget_ids as integer arrays.
Obviously, the solution is to convert everything to an integer before doing a set difference, something like:
new_widget_ids = new_widget_ids.collect {|w_id| w_id.to_i}
Now, when I do the set difference, what happens is this:
unbound_widget_ids = [1,2,4,6] - [1,2,3] # returns the difference set [4,6]
And that fixes it. Widgets with ids 4 and 6 get removed from this user, and we go ahead and add the widget with id 3. Finally.
Regardless, POST values get passed as strings. I should really know things like this by now.