I'm working on my first Rails project and I'm trying create a proper User/Address (or rather User/Location) association. I thought my problem was common and that others had done what I'm trying successfully and that it would be easy to find an answer but it wasn't the case. There are many different questions with extremely different answers and I couldn't figure out how to do things in my case. Here's my scenario:
The location can be a country, State/Region or City, not more specific than that (I'm using Google Places Library's autocomplete feature). So to avoid a lot of duplicate information in the users
table every time one or more users live in the same city (country, state, city, latitude, longitude etc), I figured I should have a locations
table. Even worse, the user should have a "home_location" and "current_location". Now the associations:
has_one
At first I thought: "Well, a user has an address/location, so I should probably use the has_one
association. But it turns out that if I use user has_one location
, the location table will be the one pointing to the user's table, which is not what I want, since many users should be able to reference the same location.
belongs_to/has_many
"OK, then I'll use the belongs_to/has_many association" I thought. Seemed pretty neat at first. Seemed to make a lot of sense. I'm using the nested forms gem, which works pretty well for everything else I've done so far, and the nested form for locations loaded and the information was sent correctly. The location was created and added to the database successfully, but there was one problem: The user.location wasn't saved. Trying to figure out why, I realize there's something very weird about this whole thing. Rails is not treating this design structure the way I need (the way I feel is more intuitive). Indeed, usually this association is used for "user/microposts" (like in Hartl's tutorial) and this kind of thing. But in my case the idea is different. We humans understand that, but Rails doesn't seem to. I want a user to be able to say where he lives in his "edit profile" page. But From what I've been reading it seems like rails is expecting the LOCATION'S edit page to have a nested form for users. Like it's expecting me to create a user like this:
@location.user.create(:email=>"john@example.com")
While what I need is to create a location like this:
@user.location.create(:city=>"Rio de Janeiro", etc)
OK. Maybe what I want is too specific and Rail's standard association won't work. After all this code above would create duplicate cities, so I'd need something like:
@user.location.find_or_create(:city=>"Rio de Janeiro")
(Does this even exist for associated objects?)
Here's the code for this attempt that saved the location successfully but didn't create the association:
model
class User < ActiveRecord::Base
attr_accessible ... :home_location, :home_location_attributes,
:current_location, :current_location_attributes, etc
belongs_to :home_location, :class_name => 'Location'
accepts_nested_attributes_for :home_location
belongs_to :current_location, :class_name => 'Location'
accepts_nested_attributes_for :current_location
...
end
class Location < ActiveRecord::Base
attr_accessible :google_id, :latitude, :longitude,
:administrative_area_level_1, :administrative_area_level_2,
:city, :neighborhood, :country, :country_id
belongs_to :country
has_many :users
end
schema
create_table "users", :force => true do |t|
...
t.integer "home_location"
t.integer "current_location"
t.text "about_me"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
...
end
create_table "locations", :force => true do |t|
t.string "google_id", :null => false
t.string "latitude", :null => false
t.string "longitude", :null => false
t.integer "country_id", :null => false
t.string "administrative_area_level_1"
t.string "administrative_area_level_2"
t.string "city"
t.string "neighborhood"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
But then it seemed what I wanted was too particular so I gave everything up and tried to simply add a form_for @location
. Then I could check in the user controller if the record already existed and save it in the user's home_location attribute. So I removed the following lines from the user model:
accepts_nested_attributes_for :home_location
accepts_nested_attributes_for :current_location
And replaced the f.fields_for
in the edit_profile view with form_for @location
.
Then in the controller I added some code to deal with the location:
class UsersController < ApplicationController
before_filter :signed_in_user, only: [:index, :edit, :update, :destroy]
before_filter :signed_out_user, only: [:new, :create]
before_filter :correct_user, only: [:edit, :update]
...
def edit
if @user.speaks.length == 0
@user.speaks.new
end
if @user.wants_to_learn.length == 0
@user.wants_to_learn.new
end
if @user.home_location.blank?
@user.home_location = Location.new
end
end
def update
logger.debug params
...
@home_location = nil
params[:home_location][:country_id] = params[:home_location].delete(:country)
unless params[:home_location][:country_id].blank?
params[:home_location][:country_id] = Country.find_by_iso_3166_code(params[:home_location][:country_id]).id
#@home_location = Location.where(params[:home_location])
#The line above describe the behavior I actually want,
#but for testing purposes I simplified as below:
@home_location = Location.find(2) #There is a Location with id=2 in the DB
if @home_location.nil?
@home_location = Location.new(params[:home_location])
if !@home_location.save then @home_location = nil end
end
end
if @user.update_attributes(params[:user])
@user.home_location = @home_location
@user.save
flash[:success] = "Profile updated"
sign_in @user
#render :action => :edit
redirect_to :action => :edit
else
render :action => :edit
end
end
...
end
But still it doesn't save the association. In fact even in the console, no matter what I do, the user's home_location attribute never gets saved to the database.
Does anybody have any idea of what may be happening??
Why is the association not being saved? What's is the most correct way of doing this in Rails?
I'm sorry about the length of the question. Hope somebody reads it. I'd be very grateful!
Thanks
EDIT 1
Following RobHeaton's tip, I added a custom setter in my User model:
def home_location= location #params[:user][:home_location]
locations = Location.where(location)
@home_location = locations.first.id unless locations.empty?
end
After fixing some issues that popped up, it started working! The data was finally saved to the database. I still have no idea why it didn't work in the controller though. Does any body have any guess? I'm wondering if the mistake wasn't actually somewhere else and I ended up fixing it without noticing just because the code was better organized in the model.
Still, the problem is not completely solved. Although the value is saved to the database, I can't access the associated model in the standard Rails way.
1.9.3p362 :001 > ariel = User.first
User Load (0.5ms) SELECT "users".* FROM "users" LIMIT 1
=> #<User id: 1, first_name: "Ariel", last_name: "Pontes", username: nil, email: "pontes@ariel.com", password_digest: "$2a$10$Ue8dh/nRh9kL9lUd8JjQU.okD6TtE
i4H1tphmRoNIQ15...", remember_token: "kadybXrh1g0X-BE_HxhA4w", date_of_birth: nil, gender: nil, interested_in: 0, relationship_status: nil, home_location: 3, curr
ent_location: nil, about_me: "", created_at: "2013-05-07 14:28:05", updated_at: "2013-05-09 20:10:41", home_details: nil, current_details: nil>
1.9.3p362 :002 > ariel.home_location
=> nil
The weird thing is that not only isn't Rails able to return the associated object, it won't even return the integer id stored in the object! How does that make any sense? I was hoping for a Location object and, worst case scenario, expecting a 3 in return. But instead I get nil.
I tried to write a custom getter anyway:
def home_location
if @home_location.blank? then return nil end
Location.find(@home_location)
end
But of course it doesn't work. Any more ideas??
Solved!
Thanks to RobHeaton for enlightening me about good practices and to rossta for explaining Rails' conventions.
Here's the configuration I have now:
model
attr_accessible :first_name, :last_name, ...,
:home_location, :home_location_id, :home_location_attributes,
:current_location, :current_location_id, :current_location_attributes, ...
...
belongs_to :home_location, :class_name => 'Location'
accepts_nested_attributes_for :home_location
belongs_to :current_location, :class_name => 'Location'
accepts_nested_attributes_for :current_location
schema
create_table "users", :force => true do |t|
t.string "first_name"
t.string "last_name"
...
t.integer "home_location_id"
t.integer "current_location_id"
...
end
My controller is untouched. Actually even cleaner than before, no more messing around with params.
As for the custom setters, they weren't even necessary for the purposes I described in the question after all. But it helped me organize my code and make it easier to spot mistakes. I'm using them now for other things I had to fix along the way and to keep my controller clean.
It seems Rails does understand both "types" of belongs_to/has_many associations after all! It's funny that didn't find the distinction being made explicitly anywhere though. Ended up assuming Rails couldn't do it when in fact all I had to do was correct some names to respect the conventions.