User's attributes depend on user's type: best way to implement in Rails

本秂侑毒 提交于 2019-12-12 06:19:07

问题


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


回答1:


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.




回答2:


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.



来源:https://stackoverflow.com/questions/32010998/users-attributes-depend-on-users-type-best-way-to-implement-in-rails

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!