0

I have a user with basic infos and activity specific infos. Let's say User is a Footballer, its profile will look like :

User.rb #common infos
  firstname
  lastname
  email
  sex
  address_id

Footballer.rb #or something accurate
  position
  team
  age
  weight
  shoe_size
  ...

However, if the user is as Boxer, its profile will be composed of :

#User.rb #same as footballer
  firstname
  lastname
  email
  sex
  address_id
  avatar

#Boxer.rb
  weight
  handicap
  league
  ...

What would be the best way to integrate this logic in rails? The profile would be rendered in users#show.html My app has easy registration (only email) and teams composed of multiple profiles, so it seems complicated to invert the logic (by creating a Footballer has_one :basic_infos and Boxer has_one :basic_infos), that would require a call on each model for basic infos highlight (such as complete name and avatar)

I'm stucked on this, so any help would be very welcome

2 Answers 2

2

I see the following options here.

1. Single table inheritance (STI)

Just dump all the fields in one table, and make class Boxer < User, class Footballer < User.

Pros: Simplicity of implementation.

Cons: Table become bloated. Fields are shared, i.e. your footballer will have weight and other fields and vice versa.

2. Moving base information to another table

That's the option you already outlined.

Pros: Clean tables, proper separation of fields.

Cons: Complex implementation. You will have to ensure that there is always one and only one basic profile for each instance. So you will have to pay attention to :delete_all/:destroy_all options of association macroses, and maybe before_create/after_destroy as well.

Here is an example on how to organize that in your code:

# models

class Profile < ActiveRecord::Base
  # you need to create
  #   t.integer :user_id
  #   t.string  :user_type
  # in a migration for 'profiles' table

  belongs_to :user, polymorphic: true
end

class User < ActiveRecord::Base
  # This will disable STI
  self.abstract_class = true
  has_one :profile, as: :user, dependent: :destroy

  def some_common_user_method
    # ...
  end

  def pick_profile
    self.profile && return self.profile
    if self.persisted?
      self.create_profile
    else
      self.build_profile
    end
  end
end

class Footballer < User
end

# controllers

class UsersController < ApplicationController
  before_action :set_klass
  before_action :set_user, except: [:index, :create]

  # ...

  def create
    @user = @klass.new
    @user.pick_profile
    # etc...
  end

  def destroy
    @user.destroy
  end

  private

  def set_klass
    # white-list search for class
    @klass = User.descendants.find{|k| k == params[:type].camelize.constantize}
  end

  def set_user
    @user = @klass.find(params[:id])
  end
end

# routes

# for URLs like /users/1?type=footballer
resources :users

# for URLs like /users/footballer/1
resources :users, path_prefix: '/users/:type'

3. Adding a serialized field

Just add details field with JSON/YAML of specific sportsmen details, and have common fields as separate DB fields.

Pros: Simplicity of implementation. Database structure stays simple as well.

Cons: You won't be able to make effective queries on specific sportsman's fields, i.e. you will need to fetch each record to know its details.

4. Using Postgresql-specific serialized columns

Same as above, only without "cons" part. You will be able to make efficient queries on serialized data.

Cons: You will need to learn how to use that fields and query them. Not possible with MySQL.

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

9 Comments

Thank you very much for all this 4 solutions. I still hesitate between the two first. STI seems very easy to implement but will generate a lot of empty cells. Second solution seems cleaner, but I will have to create a lot of controllers and models (I have 14 different profiles and that will grow), even if I can render inside a single view It sounds heavy. Could you develop a little about the delete_all and before_create/after_destroy. The email only sign-up seems to be a problem too unless giving a temporary default role for account creation.
@user3181644 I elaborated the second option in my answer. You usually only need one controller for such polymorphic model. In your 'user.html.erb' view you could render specific partial for each concrete user (e.g. render partial: @user.class.name.underscore). I didn't understand the problem with email-only sign-up, if you want further help with it please elaborate a bit.
I am not familiar with self.abstract_class = true, so I will have to dig on this. My first though was to create STI on User (like Footballer < User), then FootballerProfile, BoxerProfile etc (explained in RailsCast Pro 394). I will take the time to understand the logic and go back to you for further precisions if you mind. Thank you a lot for helping me this far, It's really appreciated
OK, so I think I understood the logic : No more User table and everything is now stored in Profile, am I right ? (still editing comment)
Yep. Still, you'll have user class for common functions.
|
1

Looks like a good fit for STI. Here's a solid tutorial about single table inheritance.

Essentially, you can create subclasses for each of your different user profiles that all inherit from the parent class User.

# app/models/user.rb
class User < ActiveRecord::Base
  # validations, associations, methods, etc.
end

# app/models/footballer.rb 
class Footballer < User
  # custom validations, methods, etc.
end

# app/models/boxer.rb
class Boxer < User
  # custom validations, methods, etc.
end

I would recommend using the annotate_models gem to add a comment summarizing the current schema in each of your models (among other files). It looks like this:

# == Schema Info
#
# Table name: users
#
#  id                  :integer(11)    not null, primary key
#  firstname           :string(255)
#  lastname            :string(255)
#  email               :string(255)
#  sex                 :string(255)
#  address_id          :integer(11)
#  avatar              :string(255)
#  weight              :integer(11)
#  handicap            :string(255)
#  league              :string(255)
#  position            :string(255)
#  team                :string(255)
#  age                 :integer(11)
#  weight              :float
#  shoe_size           :float
#  weight              :float
#  handicap            :float
#  league              :string(255)
#  type                :string(255)
#

class User < ActiveRecord::Base
  ...
end

Notice how the User model annotation contains all the columns, including those for the Footballer and Boxer subclasses. This is because you need to add all columns to the users table, including a reserved Rails column named type, which will be the name of the subclass for each record being created.

Using STI will give you flexibility with handling subclass-specific logic. I recommend you check out the tutorial I linked, or any other documentation about STI within Rails.

1 Comment

Hello Michael, thank you for your detailed answer. I'm aware of STI that I use for my images (ex types : avatar, article, etc). The problem I see with using STI with profile is that my differents profiles are very differents, so I will have a lot of empty cells in my database, which is, for what i know, a bad thing.

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.