I have the following two models in an application
class City
end
class Suburb
end
a city has exactly 5 suburbs, north, east, south, west and center. I want to reference each suburb through a correspondent method so that, north is accessed through city.north south is accessed through city.south
I can add a foreign key for each suburb in the cities
table and use belongs_to
and has_one
to define each association. But I find it not as intuitive as it should be. This is because a Suburb belongs_to a City and the not the inverse. so the following definition is not intuitive.
class City
belongs_to :north, class_name: 'Suburb'
belongs_to :east, class_name: 'Suburb'
belongs_to :south, class_name: 'Suburb'
belongs_to :west, class_name: 'Suburb'
belongs_to :center, class_name: 'Suburb'
end
class Suburb
has_one :city
end
this works as expected. but when you read it, its the inverse. a Suburb belongs_to City and a City has_one :north, has_one :east, has_one :south, has_one :west and has_one :center.
I tried also to define a has_many :suburbs
on the city
model, and add an enum property direction
to the suburb model than define a method, using define_method' for each direction, but I see it over engineered.
Is there a way to model this properly.
There's nothing inherently wrong with your schema, but for the sake of discussion, let me present an alternative that satisfies your modelling concerns:
Let's have Suburb
belong to City
, as you propose. In order to enforce the uniqueness of suburbs with respect to their city, we add a direction
column to our suburbs
table, along with a unique composite index that combines city_id
and direction
. This way a Suburb
belongs to exactly one city, and a city cannot have more than one Suburb
in a given direction
.
db/migrate/...create_suburbs.rb
class CreateDeviseUsers < ActiveRecord::Migration
def self.change
create_table(:suburbs) do |t|
# no need for `index: true` here because of composite key below
t.references :city, null: false
t.text :direction, null: false
t.index [:city_id, :direction], unique: true
end
end
end
app/models/city.rb
class City < ActiveRecord::Base
has_many :suburbs
end
Our Suburb
model is now a little more complex. We need a validation for direction
, and a scope for each possible value. I like to add a getter that ensures direction
is always a symbol as well.
app/models/suburb.rb
class Suburb < ActiveRecord::Base
DIRECTIONS = [:north, :east, :south, :west]
belongs_to :city
validates :city, presence: true
validates :direction, inclusion: { in: DIRECTIONS }
# define a scope for each direction
DIRECTIONS.each { |d| scope d, -> { where(direction: d) } }
# convenience getter so that we can reason about direction using symbols
def direction
self[:direction].try(:to_sym)
end
end
Now we can access and drill down on cities suburbs using the scopes:
# all north suburbs
north_suburbs = Suburb.north
# northern suburbs of a city (there can only be one!)
north_suburbs = city.suburbs.north
# as a model
north_suburb = city.suburbs.north.first
If you really don't like the first
bit, you can define convenience accessors:
app/models/city.rb
class City < ActiveRecord::Base
has_many :suburbs
def north
suburbs.north.first
end
# ...
end