Postal address as a requirement.
Like many of you, I have several models in my current project, such as Customer, Trainer, Building, etc...
A client came recently with a new and very common requirement:
"I want to store and show the postal address of the customers."
Take into account.
- One Customer has only one postal address.
- One postal address can be shared by many customers.
I know. You know. He will come shortly with the second, new and very common requirement:
"I want to store and show the postal address of the trainers."
I googled how to solve this problem and after do not find out about this I decided to write it down for sharing with the readers my solution for this very common but not new problem.
Polymorphism
My solution needs to cover the second requirement from the beginning. Let's start:
class CreatePostalAddresses < ActiveRecord::Migration[6.0]
def change
create_table :postal_addresses do |t|
t.string :street
t.string :city
t.string :country
t.string :zipcode
t.string :number
t.string :country_ISO2
t.timestamps
end
end
end
The postal_addresses table is unavoidable and if we want to make this reusable and robust you can see no explicit relations are involved in this table.
Keeping in mind we want this postal address in a very clean way available for many other models. Then WE ARE NOT GOING TO do this:
class PostalAddress < ApplicationRecord
has_many :customers
end
class Customer < ApplicationRecord
belongs_to :postal_address
end
This requires to include the postal_address_id in every customer. This solution will drive us to include another postal_address_id column in every table we would like in the future to support postal_address.
So are going to create the join model/table between postal_address and whatever model, using the polymorphic relations provided by ActiveRecord:
class Collocation < ApplicationRecord
belongs_to :collocable, polymorphic: true
belongs_to :postal_address
end
The corresponding migration looks like:
class CreateCollocations < ActiveRecord::Migration[6.0]
def change
create_table :collocations do |t|
t.references :collocable, polymorphic: true, index: true
t.references :postal_address, foreign_key: true
t.timestamps
end
end
end
Finally just setup the relations in the model asked in the first requirement:
class Customer < ApplicationRecord
has_one :collocation, as: :collocable
end
Reusability via Concerns
With this approach, we already can use PostalAddress in other models like Trainer doing only this:
class Trainer < ApplicationRecord
has_one :collocation, as: :collocable
end
But we would like to write things like:
trainer.postal_address
instead of
trainer.collocation.postal_address
The solution is:
class Trainer < ApplicationRecord
has_one :collocation, as: :collocable, dependent: :destroy
has_one :postal_address, through: :collocation
end
now we can write:
trainer.postal_address
BUT we have another line that should be repeated in every Collocable model.
has_one :postal_address, through: :collocation
Concerns to the rescue
Let's create the file for the Collocable concern
app/models/concerns/collocable.rb
module Collocable
extend ActiveSupport::Concern
included do
has_one :collocation, as: :collocable, dependent: :destroy
has_one :postal_address, through: :collocation
end
end
And use (include) it in Customer model
class Customer < ApplicationRecord
include Collocable
end
In case we need in the future to have a postal address for Trainer we can:
class Trainer < ApplicationRecord
include Collocable
end
After this we can:
customer.postal_address
and
trainer.postal_address
Now we are using polymorphism and concerns and we are in a very comfortable situation for adding more functionalities to our postal_address features without touching our collocable models.
Imagine we want to have postal_addres.full_addres method and be able to write trainer.full_address.
Adding method to our PostalAddress model
class PostalAddress < ApplicationRecord
def full_address
"#{number} #{street}, #{zipcode} #{city}, #{country_iso2}"
end
end
Adding features to our Collocable concern
module Collocable
extend ActiveSupport::Concern
included do
has_one :collocation, as: :collocable, dependent: :destroy
has_one :postal_address, through: :collocation
delegate :full_addres, to: :postal_address, allow_nil: true
end
end
We can improve now our collocable feature as much as we want. When you add functionalities to collocable you should avoid including dependencies from the models who pretend to be collocable.
And in case of the next week my client comes with:
"I want to store and show the postal address of the Building."
I know and you know:
class Building < ApplicationRecord
include Collocable
end
git commit -am 'adding the postal address to buildings'; git push origin master
Top comments (1)
Thank you for sharing your detailed solution for handling postal addresses in various models. Your use of polymorphism and concerns is a smart approach to keep the codebase Postal Codes clean and reusable. This will certainly help others facing similar challenges when implementing postal addresses for different entities. Well-explained and insightful post!