Rails Postgres migration without downtime - ruby-on-rails

Let's say I want to add a column on my users table running the following migration
class AddVersionHistoryToUsers < ActiveRecord::Migration
def change
add_column :users, :versions, :string, array: true, default: '{}'
User.find_each do |user|
if user.app_version?
user.versions << user.app_version.to_s
user.save!
end
end
end
end
My aim is to insert the current app_version of each user into the versions array. How can I execute a migration without a lock in the users table due to the default value?

To wrap this up: Looping and querying the database for every User is highly inefficient. You should use update_all for these tasks.
And since your data already exists in the same table you can simply get it from there.
User.where.not(app_version: nil).update_all('versions = ARRAY[app_version]')

Related

rails migration setting boolean field doesn't work

I have a migration which adds a boolean column and sets value for some of the rows. The new value doesn't get saved to the database when the model is saved. Here is a simplified version of the code:
class AddSmartToStudent
def change
add_column :students, :smart, :boolean
Student.where(grade: 'A').each do |student|
student.smart = true
student.save!
end
end
end
Thanks in advance!
In your migration, you add a boolean column, and right after that use it in the model. Not sure it is possible - when migration is not ended yet the transaction is not committed. The Student model might not have smart field yet.
As Luis Silva suggests you could use reset_column_information method to refresh info about columns for Student. But the problem is migrations are not for manipulating with data. If you want to change some data it's better to do in a rake task.
If for some reason you HAVE TO do it in migration, you can do it in plain SQL query. For PostgreSQL it will be:
execute "UPDATE students SET smart='t' WHERE grade='A'"
Try to reset the cached information about columns, which will cause them to be reloaded on the next request.
Execute this line before your clause where
Student.reset_column_information
reset_column_information
There are two issues that are clear to me right of the bet.
As stated by others you're trying to use an attribute that you add in that same migration. The safe thing to do is to reset the column information like explained in the answer of Luis Silva.
The second issue has to do with the fact that you use def change where some of the content isn't reversible. Everything in the change method should be reversible. Otherwise def up and def down should be used.
Here are two options that might solve your issue:
Using def up and def down.
class AddSmartToStudent
def up
add_column :students, :smart, :boolean
Student.reset_column_information
Student
.where(grade: 'A')
.find_each { |student| student.update!(smart: true) }
end
def down
remove_column :students, :smart
end
end
Using reversible.
class AddSmartToStudent
def change
add_column :students, :smart, :boolean
reversible do |change|
change.up do
Student.reset_column_information
Student
.where(grade: 'A')
.find_each { |student| student.update!(smart: true) }
end
end
end
end
If you don't care about Rails callbacks, validations, etc. you could also use
Student.where(grade: 'A').update_all(smart: true)
as replacement for
Student.where(grade: 'A').find_each { |student| student.update!(smart: true) }
This updates all the records with a single query but doesn't instantiate the records, meaning Rails callbacks, validations, etc. won't run. For more info see update_all.

migration steps change, up and down

My question is really simple. I created this migration file and my mobile column didn't change BUT def change was created. Is it because rails ignored def up and def down? if so why?
def change
add_column :posts, :address, :string
end
def up
execute 'ALTER TABLE posts ALTER COLUMN mobile TYPE integer USING (mobile::integer)'
end
def down
execute 'ALTER TABLE posts ALTER COLUMN mobile TYPE text USING (mobile::text)'
end
Rails will not run both the change and up methods by design thus ignoring everything after the change method. When you need to run some specific logic like in your Up and Down methods you have two choices. You can either put the stuff in your change method into the up and down methods or you can put the Up and Down stuff into the change method. If you want to do this the "Rails4" way you should use change and the reversible method to get what you need:
class SomeMigration < ActiveRecord::Migration
def change
add_column :posts, :address, :string
reversible do |change|
change.up do
execute 'ALTER TABLE posts ALTER COLUMN mobile TYPE integer USING (mobile::integer)'
end
change.down do
execute 'ALTER TABLE posts ALTER COLUMN mobile TYPE text USING (mobile::text)'
end
end
end

How to make migration to update table RAILS

I am stuck with making migration which will update my table Users. I need to set country_code with 1 everywhere where i have "" or NULL for that column.
Thanks
class UpdateCountryCodeColumnUsers < ActiveRecord::Migration
def up
execute %Q(
UPDATE users
SET country_code = 1
WHERE country_code IS NULL OR country_code = ""
)
end
end
You probably shouldn't alter the data in a migration and only use it to alter the schema.
A lot of devs use rake db:reset which won't run this migration.
A better solution is to create a rake or thor task as a one off or simply just execute the SQL.
Sergei's answer is your best bet and will update the data (which would be needed first - and is very important) and it lets the database do the work. If you also need to set the default for going forward (after the update Sergei proposed) you can make a separate migration (to separate activities) and include the below...
You can also use the rails migration helper method change_column_default
change_column_default :users, :country_code, from: nil, to: 1
If you want to make it reversible just use change_column...
def up
change_column :users, :country_code, :string, default: 1
end
def down
change_column :users, :country_code, :string, default: nil
end
def UpdateCountryCodeForUsers < ActiveRecord::Migration
def up
Users.where("country_code = '' or country_code = NULL")
.update_attributes({country_code: 1})
end
end
On rails 5.2 , I needed to update some text attributes in a column & had to use update_all instead of update_attributes.
class UpdateTableColumn < ActiveRecord::Migration[5.2]
def up
TableName.where("my_column = 'my column old value'")
.update_all({keywords: 'my column new value'})
end
end

Can Rails Migrations be used to convert data?

I'm trying to convert a column in my Rails app, for arguments sake let's pretend I'm trying to change the age column in my users table to a string representation rather than an int.
In my migration I have this;
def.self up
add_column :users, :age_text, :string
users = User.find(:all)
users.each do |u|
u.age_text = convert_to_text(u.age)
u.save
end
end
def self.convert_to_text(number)
#code here to convert 1 to 'one' etc
end
But it doesn't seem to be working, is what I'm attempting here even possible with migrations?
What you're trying to do is possible, and I would say the correct thing to do.
You need, though, to reload the column info for the model classes you're updating in the migration, so that Rails knows about the new columns. Try this:
def.self up
add_column :users, :age_text, :string
User.reset_column_information
users = User.find(:all)
users.each do |u|
u.age_text = convert_to_text(u.age)
u.save
end
end
On a separate note, please note that if your table is large, doing updates one by one will take a looong time.. Be careful with that.
Since I'm new here I can't comment on the above so I'll add my own answer.
GENERALLY manipulating data in migrations is a BAD idea. Migrations with direct model access can get stuck if the model logic changes.
Imagine in your second migration you've added a new column. You want to seed that column with new data.
Let's also say a few weeks later you add a new validation to the model - a validation that operates on a field that does not yet exist in your second migration. if you ever were to construct the database from migration 0, you'd have some problems.
I strongly suggest using migrations to alter columns and other means to manage database data, especially when moving to production.
Here is an example migration I ran to convert data. You can easily convert it to use integers instead of strings. Making the conversion in SQL is much faster than loading each row in Rails.
class ConvertCommentTextToText < ActiveRecord::Migration
def up
add_column :comments, :text_tmp, :text
# copy text column
execute <<-SQL
update comments set text_tmp = text
SQL
remove_column :comments, :text
rename_column :comments, :text_tmp, :text
end
def down
add_column :comments, :text_tmp, :string
# copy text column
execute <<-SQL
update comments set text_tmp = text
SQL
remove_column :comments, :text
rename_column :comments, :text_tmp, :text
end
end
And to test it:
rake db:migrate
rake db:rollback
rake db:migrate
I would say that if you can "undo" the imported data when rolling back the migration version, then it's appropriate to put imports into the migration.
For example, I have a migration which sets up a lot of lookup tables and other meta-data. The data for these tables are populated during this phase. As the data for these lookup tables changes, I create new YAML files storing the meta-data and load those files in subsequent migrations (and un-do those YAMLS, re-loading the previous YAML file when backing out of a migration version). This is pretty clean. I have files (in different well-defined folders in my case) with these files:
002_setup_meta_data.rb
002_meta_data.yaml
007_change_meta_data.rb
007_meta_data.yaml
If you're importing "production" data from another system into transactional (non-static) tables, then I would say using migrations is not appropriate. Then I would follow Brian Hogan's advice of using rake tasks.

Add Rows on Migrations

I'd like to know which is the preferred way to add records to a database table in a Rails Migration. I've read on Ola Bini's book (Jruby on Rails) that he does something like this:
class CreateProductCategories < ActiveRecord::Migration
#defines the AR class
class ProductType < ActiveRecord::Base; end
def self.up
#CREATE THE TABLES...
load_data
end
def self.load_data
#Use AR object to create default data
ProductType.create(:name => "type")
end
end
This is nice and clean but for some reason, doesn't work on the lasts versions of rails...
The question is, how do you populate the database with default data (like users or something)?
Thanks!
The Rails API documentation for migrations shows a simpler way to achieve this.
http://api.rubyonrails.org/classes/ActiveRecord/Migration.html
class CreateProductCategories < ActiveRecord::Migration
def self.up
create_table "product_categories" do |t|
t.string name
# etc.
end
# Now populate the category list with default data
ProductCategory.create :name => 'Books', ...
ProductCategory.create :name => 'Games', ... # Etc.
# The "down" method takes care of the data because it
# drops the whole table.
end
def self.down
drop_table "product_categories"
end
end
Tested on Rails 2.3.0, but this should work for many earlier versions too.
You could use fixtures for that. It means having a yaml file somewhere with the data you want to insert.
Here is a changeset I committed for this in one of my app:
db/migrate/004_load_profiles.rb
require 'active_record/fixtures'
class LoadProfiles < ActiveRecord::Migration
def self.up
down()
directory = File.join(File.dirname(__FILE__), "init_data")
Fixtures.create_fixtures(directory, "profiles")
end
def self.down
Profile.delete_all
end
end
db/migrate/init_data/profiles.yaml
admin:
name: Admin
value: 1
normal:
name: Normal user
value: 2
You could also define in your seeds.rb file, for instance:
Grid.create :ref_code => 'one' , :name => 'Grade Única'
and after run:
rake db:seed
your migrations have access to all your models, so you shouldn't be creating a class inside the migration.
I am using the latest rails, and I can confirm that the example you posted definitely OUGHT to work.
However, migrations are a special beast. As long as you are clear, I don't see anything wrong with an ActiveRecord::Base.connection.execute("INSERT INTO product_types (name) VALUES ('type1'), ('type2')").
The advantage to this is, you can easily generate it by using some kind of GUI or web front-end to populate your starting data, and then doing a mysqldump -uroot database_name.product_types.
Whatever makes things easiest for the kind of person who's going to be executing your migrations and maintaining the product.
You should really not use
ProductType.create
in your migrations.
I have done similar but in the long run they are not guaranteed to work.
When you run the migration the model class you are using is the one at the time you run the migration, not the one at the time you created the migration. You will have to be sure you never change your model in such a way to stop you migration from running.
You are much better off running SQL for example:
[{name: 'Type', ..}, .. ].each do |type|
execute("INSERT INTO product_types (name) VALUES ('#{type[:name]} .. )
end

Resources