I have two types of classes:
BaseUser < ActiveRecord::Base
and
User < BaseUser
which acts_as_authentic using Authlogic's authentication system. This inheritance is implemented using Single Table Inheritance
If a new user registers, I register him as a User. However, if I already have a BaseUser with the same email, I'd like to change that BaseUser to a User in the database without simply copying all the data over to the User from the BaseUser and creating a new User (i.e. with a new id). Is this possible? Thanks.
You can just set the type field to 'User' and save the record. The in-memory object will still show as a BaseUser but the next time you reload the in-memory object will be a User
>> b=BaseUser.new
>> b.class # = BaseUser
# Set the Type. In-Memory object is still a BaseUser
>> b.type='User'
>> b.class # = BaseUser
>> b.save
# Retrieve the records through both models (Each has the same class)
>> User.find(1).class # = User
>> BaseUser.find(1).class # User
Steve's answer works but since the instance is of class BaseUser
when save
is called, validations and callbacks defined in User
will not run. You'll probably want to convert the instance using the becomes method:
user = BaseUser.where(email: "user@example.com").first_or_initialize
user = user.becomes(User) # convert to instance from BaseUser to User
user.type = "User"
user.save!
Based on the other answers, I expected this to work in Rails 4.1:
def update
@company = Company.find(params[:id])
# This separate step is required to change Single Table Inheritance types
new_type = params[:company][:type]
if new_type != @company.type && Company::COMPANY_TYPES.include?(new_type)
@company.becomes!(new_type.constantize)
@company.type = new_type
@company.save!
end
@company.update(company_params)
respond_with(@company)
end
It did not, as the type change would not persist. Instead, I went with this less elegant approach, which works correctly:
def update
@company = Company.find(params[:id])
# This separate step is required to change Single Table Inheritance types
new_type = params[:company][:type]
if new_type != @company.type && Company::COMPANY_TYPES.include?(new_type)
@company.update_column :type, new_type
end
@company.update(company_params)
respond_with(@company)
end
And here are the controller tests I used to confirm the solution:
describe 'Single Table Inheritance (STI)' do
class String
def articleize
%w(a e i o u).include?(self[0].to_s.downcase) ? "an #{self}" : "a #{self}"
end
end
Company::COMPANY_TYPES.each do |sti_type|
it "a newly assigned Company of type #{sti_type} " \
"should be #{sti_type.articleize}" do
post :create, { company: attributes_for(:company, type: sti_type) },
valid_session
expect(assigns(:company)).to be_a(sti_type.constantize)
end
end
Company::COMPANY_TYPES.each_index do |i|
sti_type, next_sti_type = Company::COMPANY_TYPES[i - 1],
Company::COMPANY_TYPES[i]
it "#{sti_type.articleize} changed to type #{next_sti_type} " \
"should be #{next_sti_type.articleize}" do
company = Company.create! attributes_for(:company, type: sti_type)
put :update, { id: company.to_param, company: { type: next_sti_type } },
valid_session
reloaded_company = Company.find(company.to_param)
expect(reloaded_company).to be_a(next_sti_type.constantize)
end
end
end
来源:https://stackoverflow.com/questions/3262415/changing-type-of-activerecord-class-in-rails-with-single-table-inheritance