Original Code
Migration:
class CreateUsers < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.string :first_name
t.string :last_name
t.string :login
t.string :password
t.string :salt
t.string :time_zone
t.integer :friends_count
t.integer :messages_count
t.integer :visits_count
t.string :email
t.string :gender
t.string :city
t.string :state
t.string :zip_code
t.string :country
t.timestamps
end
end
def self.down
drop_table :users
end
end
Model:
class User < ActiveRecord::Base
end
Now let's say you want to create 2 entities to represent an User where all the user's personal info will be and User where it shows has all the info that's public. Also, Accounts will be on another database in case you want to make a seperate app to manage Accounts. We can start off by making a new entry in database.yml for the new database and a constant to refer to this database:
In database.yml add:
accounts_development:
adapter: mysql
encoding: utf8
database: dbs_tutorial_accounts_development
username: root
password:
socket: /tmp/mysql.sock
In Development.rb:
ACCOUNTS_DB=YAML::load(open(File.join(RAILS_ROOT, "config/database.yml"),"r"))["accounts_#{RAILS_ENV}"]['database'] unless Object.const_defined?('ACCOUNTS_DB')
Here we setup rails to check database.yml for the location of accounts database when the server is started and store as ACCOUNTS_DB. Next, we make a migration and model for Account.
Account Migration:
class CreateAccounts < ActiveRecord::Migration
def self.up
create_table "#{ACCOUNTS_DB}.accounts" do |t|
t.string :login
t.string :password
t.string :salt
t.string :time_zone
t.string :gender
t.string :city
t.string :state
t.string :zip_code
t.string :country
t.integer :user_id
t.timestamps
end
end
def self.down
drop_table "#{ACCOUNTS_DB}.accounts"
end
end
Account Model:
class Account < ActiveRecord::Base
belongs_to :user
def self.table_name
"#{ACCOUNTS_DB}.accounts"
end
end
So, our Account will have the user's login, password, location info, and user_id. We Change the table_name also makes it so it will append the database to accounts every time it gets reference. Now we need to make some migrations to transform the database. If this were a table of millions of Users, it would take Active Record a while to transform. So, we will be using straight SQL to do the transformation.
class ConvertUsersToNewFormat < ActiveRecord::Migration
def self.up
self.copy_data_to_accounts
self.convert_users_table
end
def self.down
self.revert_users_table
self.copy_data_to_users
end
def self.copy_data_to_accounts
User.connection.execute("
INSERT INTO #{Account.table_name} (id,
user_id,
first_name,
last_name,
login,
password,
salt,
time_zone,
email,
gender,
city,
state,
zip_code,
country,
updated_at,
created_at)
SELECT users.id,
users.id,
users.first_name,
users.last_name,
users.login,
users.password,
users.salt,
users.time_zone,
users.email,
users.gender,
users.city,
users.state,
users.zip_code,
users.country,
users.updated_at,
users.created_at
FROM users;
")
end
def self.convert_users_table
User.connection.execute("ALTER TABLE users DROP COLUMN first_name,
DROP COLUMN last_name,
DROP COLUMN login,
DROP COLUMN password,
DROP COLUMN salt,
DROP COLUMN time_zone,
DROP COLUMN email,
DROP COLUMN gender,
DROP COLUMN city,
DROP COLUMN state,
DROP COLUMN zip_code,
DROP COLUMN country;
")
end
def self.revert_users_table
User.connection.execute("ALTER TABLE users ADD COLUMN first_name VARCHAR(255),
ADD COLUMN last_name VARCHAR(255),
ADD COLUMN login VARCHAR(255),
ADD COLUMN password VARCHAR(255),
ADD COLUMN salt VARCHAR(255),
ADD COLUMN time_zone VARCHAR(255),
ADD COLUMN email VARCHAR(255),
ADD COLUMN gender VARCHAR(255),
ADD COLUMN city VARCHAR(255),
ADD COLUMN state VARCHAR(255),
ADD COLUMN zip_code VARCHAR(255),
ADD COLUMN country VARCHAR(255);
")
end
def self.copy_data_to_users
User.connection.execute("
INSERT INTO users (
first_name,
last_name,
login,
password,
salt,
time_zone,
email,
gender,
city,
state,
zip_code,
country)
SELECT #{Account.table_name}.first_name,
#{Account.table_name}.last_name,
#{Account.table_name}.login,
#{Account.table_name}.password,
#{Account.table_name}.salt,
#{Account.table_name}.time_zone,
#{Account.table_name}.email,
#{Account.table_name}.gender,
#{Account.table_name}.city,
#{Account.table_name}.state,
#{Account.table_name}.zip_code,
#{Account.table_name}.country
FROM #{Account.table_name};
")
end
end
The migration just copies data from users to accounts and transforms the Users table. The trick here is to make the user.id and account.id the same. If you have an another table that references users, but now accounts, it would take hours if you have millions of records to the transform but minutes to just rename the user_id column to account_id.
Now that we have the database all worked out, the next part is make sure the controllers and models are updated. We can go through every reference of user.login, user.password, etc... and change them to user.account.login, user.account.password, etc..., but that would not be fun for a well established code base with thousands of lines of code. So the lazy way is to use delegate method:
delegate :login, :profiles
delegate :login=, :profiles
But this is Ruby, let's do some metaprogramming to get more lazier and create a method that takes in a list of attributes and delete it. Here's a method Ryan Graver wrote:
config/initializers/core_extension.rb
class ActiveRecord::Base
def self.delegate_attrs(*atts)
opt = atts.pop
atts.each do |att|
delegate att, opt
delegate "#{att}?", opt
delegate "#{att}=", opt
end
end
end
Now that we have that method setup, we modify User to use it, and might as well create a new account when a user is created.
New User.rb
class User < ActiveRecord::Base
has_one :account
delegate_attrs :login,
:password,
:salt,
:login,
:first_name,
:last_name,
:time_zone,
:email,
:gender,
:city,
:state,
:zip_code,
:country,
:to => :account
after_update :make_account
protected
def make_account
self.account.save
end
end
After that, you should not have to modify anything else to get the app working like before. user.login or user.account.login should work. Now you have a rails app using 2 databases and not much controller and view code to change to get it working. I had the code for this tutorial below, one as a blank and another as the result.

Comments
Interesting post? Show some love and post a comment!Using pieces of this code? Post a link!
Add a Comment