0

I have a rails app as follows: A location model which stores some geo-stuff (a location basically), a post model and a user model. A post model can have a location. A user model can have a location as home location and another one as remote location:

class Location < ApplicationRecord
  belongs_to :locationable, polymorphic: true
end

class Post < ApplicationRecord
  has_one :location, as: :locationable
  accepts_nested_attributes_for :location
end

class User < ApplicationRecord
  has_one :homelocation, as: :locationable, class_name: 'Location'
  has_one :remotelocation, as: :locationable, class_name: 'Location'
  accepts_nested_attributes_for :homelocation, :remotelocation
end

The post and location stuff works great. If I delete one of the ´has_one´ lines from the user model and rename homelocation to location, everything works great too. If I want a user to have two different locations though, I get an 'Unpermitted parameters: homelocation, remotelocation' error when trying to save changes.

My users_controller has a

def user_params
  params.require(:user).permit(:admin, :name, :motto, homelocation_attributes: [:id, :address], remotelocation_attributes: [:id, :address])
end

just as the posts_controller has a

def post_params
  params.require(:post).permit(:title, :content, location_attributes: [:id, :address])
end

My forms look like this:

.form-group.string.required.user_homelocation_address
  label.control-label.string.required for="user_homelocation_address"
    abbr title="required"
    | Home Location
  input#user_homelocation_address.form-control.string.required name="user[homelocation][address]" type="text"

.form-group.string.required.user_remotelocation_address
  label.control-label.string.required for="user_remotelocation_address"
    abbr title="required"
    | Remote Location
  input#user_remotelocation_address.form-control.string.required name="user[remotelocation][address]" type="text"

So why does this work for one 'has_one', but not for two?

1
  • Great explanation to all this stuff is here, btw: stackoverflow.com/questions/23814903/… by @sirramongabriel – only sadly it seems he also couldn't figure out my problem; read the last paragraph. Commented Mar 20, 2017 at 20:45

1 Answer 1

2

The issue is really that the Location does not know if it is the User's homelocation or remotelocation. The solution to this is to make the User belong to the Location

class Location < ApplicationRecord
end

class Post < ApplicationRecord
  belongs_to :location
  accepts_nested_attributes_for :location
end

class User < ApplicationRecord
  belongs_to :homelocation,   class_name: 'Location'
  belongs_to :remotelocation, class_name: 'Location'
  accepts_nested_attributes_for :homelocation, :remotelocation
end

And, obviously, change the tables to match.

There is no easy way to navigate back from a Location to it's owner. Is this a requirement?

Update 1

Something I didn't say initially, and obviously with a vague understanding of the requirements, is that I consider polymorphic belongs_to to be one of those 'considered evil' topics. It is almost always a bad code smell, it means you can't implement foreign keys which I consider an essential practice and there are other ways to solve the problems it is trying to solve. My instinct would be to create 2 models and 2 tables, UserLocation and PostLocation.

As I said initially the problem remains, how do you know if a Location is a home location or a remote location, or in other words what does location.locationable = some_user set? There is no way for Rails to know and this is really what you need to solve.

Given the model above, there are ways to navigate from the Location to it's owner but to make it perform decently I would suggest that you add a type field to the Location table so that you know if it is a post, home or remote location. You could then write:

class Location < ApplicationRecord
  def owner
    case type
    when 'post'   Post.where(location_id: self).first
    when 'home'   User.where(homelocation_id: self).first
    when 'remote' User.where(remotelocation_id: self).first
  end
  # or # 
  def owner
    case type
    when 'post'   Post.where(location_id: self).first
    else          User.where('homelocation_id = ? OR remotelocation_id = ?', self, self).first
  end
end

You could in theory do the same thing using STI wih a Location class and UserLocation and PostLocation subclasses.

Option 2

Having thought about it, I might implement this would be:

class Location < ApplicationRecord
  belongs_to :locationable, polymorphic: true
  ## Again add a `type` field ##
end

class Post < ApplicationRecord
  has_one :location, as: :locationable
  accepts_nested_attributes_for :location
end

class User < ApplicationRecord
  has_many :locations, as: :locationable, class_name: 'Location'
  has_one  :homelocation,  class_name: 'Location', foreign_key: 'locationable_id', -> { where(type: 'home') }
  has_one  :remotelocation,  class_name: 'Location', foreign_key: 'locationable_id', -> { where(type: 'remote') }
  accepts_nested_attributes_for :homelocation, :remotelocation
end

Though my thoughts about belongs_to polymorphic still stand :)

In all cases, you might also need to write code to create the homelocation and remotelocation instances rather than using accepts_nested_attributes. Also these options would not perform well if you are retrieving many records and trying to solve the n+1 problem.

Sign up to request clarification or add additional context in comments.

1 Comment

That is not what I wanted initially, because I need to navigate back from a Location to a User ... but maybe your solution is still better for what I want anyway: I can use one of those geo-functions in a SQL-query to get all posts or users in a certain radius around a location.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.