0

I am designing a rails application where users sign in and create a profile. I wish for users to have a choice of 3 profile types (provider, seeker, professional).

I have used the devise gem for user authentication. I have a profile model and have to following associations.

User.rb has_one :profile

Profile.rb belongs_to :user

At the moment users can create a generic profile but i wish to change it so each profile type has different input fields.

what is the most basic way to do this ?

L.P
  • 19
  • 3

2 Answers2

0

You can use STI (Single Table Inheritance) but I usually avoid it - the question you must ask yourself is : "do I really need to define a new Model for each profile type ?"

One of the simplest way to achieve this is to just set a kind attribute and then give each Profile#kind(you cannot use the type keyword reserved for STI) its own association accordingly :

class User
  has_many :profiles
  has_one :provider_profile, -> {where(kind: "provider")}, class_name: "Profile"
  has_one :seeker_profile, -> {where(kind: "seeker")},  class_name: "Profile"
  has_one :professional_profile, -> {where(kind: "professional")},  class_name: "Profile"

end
charlysisto
  • 3,700
  • 17
  • 30
  • 1
    it would be great if you shortly mention pros and cons of STI – Konstantin Jan 22 '16 at 08:30
  • Ditto, I've seen many mentions of how STI's are a bad idea, yet we've used them plenty – Richard Peck Jan 22 '16 at 09:06
  • pros: conceptually easy to grasp, railsish way... cons: composition over inheritance & SRP. In a nutshell if I have to go through the trouble of creating a model & a dedicated UI for each type, I prefer to rethink my domain model and give these models their own data (aka table) rather than sharing it and worry about all the possible antipatterns creeping in... – charlysisto Jan 22 '16 at 09:39
0

The other option you have to STI is enum:

#app/models/profile.rb
class Profile < ActiveRecord::Base
   enum state: [:provider, :seeker, :professional]
end

This gives you an int column (in this case state) which denotes whether the object has a particular property (EG provider? / seeker? etc).

It would provide a similar set of functionality to STI, except giving you a single model to call (rather than the 3 you'd get if using an STI pattern).


STI

STI's are good if you have the need to call multiple models.

Most of the time, you don't. There's a good writeup about it here.

If using STI with your case, you'd end up with:

#app/models/profile.rb
class Profile < ActiveRecord::Base
   belongs_to :user
end

#app/models/seeker.rb
class Seeker < Profile
end

#app/models/professional.rb
class Professional < Profile
   def add_client
      ...
   end
end

The important thing to note is that although this looks pretty in your models, it means your front-end will need to call:

#config/routes.rb
resources :professionals, only: [:new, :create]

#app/controllers/professionals_controller.rb
class ProfessionalsController < ApplicationController
   def index
      @professional = Professional.find params[:id]
   end
end

If you're planning on calling Profile.find_by type: "professional", forget it. That's an antipattern & highly inefficient.

--

The way to determine whether you really need to follow the STI pattern is very simple -- do you need extra methods / attributes for each subclass?

If not, then you can get away with an enum:

#app/models/user.rb
class User < ActiveRecord::Base
   has_one :profile
   before_create :build_profile #-> creates blank profile with each new user
   accepts_nested_attributes_for :profile
end

#app/models/profile.rb
class Profile < ActiveRecord::Base
   belongs_to :user
   enum state: [:provider, :seeker, :professional] #-> defaults to "provider"
end

I would personally use an enum with conditioning in the controller:

#app/controllers/profiles_controller.rb
class ProfilesController < ApplicationController
   def edit
      @profile = current_user.profile
      if @profile.professional?
        ...
      elsif @profile.seeker?
        ...
      end
   end
end

--

The best way to understand how this works is to look up about object orientated programming, which is Ruby/Rails in a nutshell.

OOP uses classes to create "objects"...

enter image description here

Each of these objects are invoked as instances (which is where the terms "instance variable" and "instance method" come from) - these instances being held in memory by your application.

The way OOP programs work is by taking inputs from the user, determining the interaction between the objects, and outputting the result. This is how all modern games work.

Thus, when you look at Rails, you have to look at it from the perspective of which objects you're going to be invoking. Do you really want to be pulling Professional objects, or is it just a Profile that you're working with?

Community
  • 1
  • 1
Richard Peck
  • 76,116
  • 9
  • 93
  • 147