问题
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