How to make migration to update table RAILS - ruby-on-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

Related

Add new column by comparing values of an existing column in Rails 5

I have a model that has a column its_holiday that is a boolean field in database. Now I want to add another column its_not_a_holiday that has a value opposite to that of the its_holiday column. How can I add this new column to the existing table and populate it with the values opposite to that of its_holiday column?
If your boss insists on having an its_not_holiday boolean (although other answers and comments have pointed out, you don't actually need to do that) you can do a migration as follows...
Create a migration
rails g migration AddItsNotAHolidayToMyModel its_not_a_holiday:boolean
Edit the migration as follows
class AddItsNotAHolidayToMyModel < ActiveRecord::Migration[5.1] # or whatever version you have
def up
add_column :my_model, :its_not_a_holiday, :boolean, default: false
MyModel.where(its_holiday: [false, nil]).update_all(its_not_a_holiday: true)
end
def down
remove_column :my_model, :its_not_a_holiday
end
end
You may want to modify the model to ensure the boolean is always set correctly when you save a record
class MyModel
before_save :update_its_not_a_holiday
private
def update_its_not_a_holiday
self.its_not_a_holiday = !its_holiday
end
end
As mentioned by #Nithin you can use negate value of its_holiday instead of new column. To follow good practice you should use the negate value. But if you still want to do that you can do it like this for existing records.
class AddItsNotAHolidayToModelName < ActiveRecord::Migration
def up
add_column :model_name, :its_not_a_holiday, :boolean
ModelName.find_each do |model_name|
model_name.its_not_a_holiday = !model_name.its_holiday
model_name.save
end
end
def down
remove_column :model_name, :its_not_a_holiday
end
end
and for new records
class ModelName
before_save :update_its_not_a_holiday
private
def add_its_not_a_holiday
self.its_not_a_holiday = !its_a_holiday
end
end
Hope it will work for you.
You can add a custom method in your model file like this;
def its_not_a_holiday
!its_holiday
end

Rails Postgres migration without downtime

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]')

Rails set default DateTime to now

In my app I have teams and each team has a game time every week. I want the game times to be set to 'now' as a default. My table is set up like so
create_table "teams", force: true do |t|
t.datetime "wk1_time"
end
I created a migration and it looks like this:
class ChangeDateTimeDefault < ActiveRecord::Migration
def change
change_column :teams, :wk1_time, :default => DateTime.now
end
end
When I run rake db:migrate I get an error. Is my syntax wrong or am I missing something else?
Since Rails 5 you can make a migration like this:
change_column_default :users, :wk1_time, -> { 'CURRENT_TIMESTAMP' }
In my mind this is the best option because it not database specific answer.
Yes, you are missing the type :
class ChangeDateTimeDefault < ActiveRecord::Migration
def change
change_column :teams, :wk1_time, :datetime, :default => DateTime.now
end
end
But, you need the below not the above one, because you just want to change the default.
class ChangeDateTimeDefault < ActiveRecord::Migration
def change
change_column_default :teams, :wk1_time, DateTime.now
end
end
But none of these are correct approach for your task. The reason is DateTime.now will be evaluated based upon when you ran the migration, instead when the record is created. You need look to into this answer to know how to set the default time.
EDIT: For Rails 5+ there are better answers, like this one: https://stackoverflow.com/a/55357711/252799, though the below still works.
The way I found, was to do a migration on an existing datetime column, like this:
#migration
execute("ALTER TABLE teams ALTER COLUMN wk1_time SET DEFAULT CURRENT_TIMESTAMP")
that produces a schema.rb entry shown like this:
#schema.rb
t.datetime "wk1_time", default: "now()", null: false
The "now()" is a string sent to postgresql and evaluated at runtime, upon each insert.
You're going to run into problems settings the default date time in the migration. This is because DateTime.now will be evaluated based upon when the migrate runs, not when the record is created!
To fix that you'll need to create an ActiveRecord callback in order to set wk1_time like so:
before_create :set_default_wk1_datetime
def set_default_wk1_datetime
self.wk1_time = DateTime.now
end
for Postgresql :
add_column :users, :msgs_seen_at, 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'
but you'll have to use user.reload after user = User.create in order to "see" msgs_seen_at

How do I create a reversible migration helper in rails?

I find myself having to execute very similar sql statements (with maybe 1 param besides the table name) on several tables on a rails app. As a result, I'm getting lots of similarly looking migrations, like this one:
class DoSomeSQLOnUser < ActiveRecord::Migration
def up
execute('some long sql that alters the user.field1')
execute('some long sql that alters the user.field2')
end
def down
execute('some sql that undoes the changes')
end
end
Then I have the same thing for clients, sales, etc.
I would like to extend ActiveRecord::Migration so that I can do this instead:
class DoSomeSQLOnUser < ActiveRecord::Migration
def change
do_custom_thing_on :users, :field1
do_custom_thing_on :users, :field2
end
end
How can I do that? I think I know how to do it when the operations are separated into up and down, like this:
class DoSomeSQLOnUser < ActiveRecord::Migration
def up
do_custom_thing_on :users, :field1
do_custom_thing_on :users, :field2
end
def down
undo_custom_thing_on :users, :field1
undo_custom_thing_on :users, :field2
end
end
But doing it so that the change is "reversible" escapes me.
It doesn't seem to be a official supported way to do this, so probably you'll need to open the class ActiveRecord::Migration::CommandRecorder and record the new method and its inverted version.
Find the definition of the class at activerecord/lib/active_record/migration/command_recorder.rb.
In Rails 4 there is a reversible helper method, which you can use like this:
def change
do_custom_thing_on :users, :field1
do_custom_thing_on :users, :field2
end
def do_custom_thing_on(table, field)
reversible do |dir|
dir.up { execute "some long sql to alter #{field} on #{table}"}
dir.down { execute "some long sql to undo #{field} on #{table}"}
end
end
Not sure if you need to do something else, but at least you should add a inverse_custom_thing method to ActiveRecord::Migration::CommandRecorder
Main purpose of change method it's adding, renaming columns but not removing. Change method 'knows' how to make reverse when the migration is rolled back. So if you want to do something, which is not removing, just do it in change method, ActiveRecord will reverse it itself, I think.
More information you can get from official documentation

Update a column, all rows

I added a new column to my table but I forgot to add the :default option. Now I want to populate that column on every single row.
Is there a way to do with using the console? I've been searching google for the past hour but I can't find anything.
I know how to do it for a single object, but not for all rows of a model.
Foo.find(1).update_attribute(:myattribute, 'value')
Try this:
Foo.update_all(some_column: "bar")
This will generate SQL query to database:
UPDATE "foos" SET "some_column" = "bar";
Since you already created the new field in a previous migration, create a brand new migration:
rails g migration UpdateFoos
Modify the migration:
def self.up
say_with_time "Updating foos..." do
Foo.find(:all).each do |f|
f.update_attribute :myattribute, 'value'
end
end
end
# from command line
Rake db:migrate
Let me know if this works, it might need a few adjustments. See rails docs for more:
you can do like this:
Foo.update_all(new_column: "bar")
Of course you can use smth like Foo.update_all(:myattribute => "value"), but it'll modify only already created data. To set default value for all "future" data it's a good way to create a separate migration like this:
rails generate migration AddDefaultValueToFoo
Modify new migration (for ex. myattribute has a string type) like this:
class AddDefaultValueToFoo < ActiveRecord::Migration
def self.up
change_column :foos, :myattribute, :string, :default => "value"
Foo.update_all(:myattribute => "value")
end
end

Resources