How does the migration work in Rails? - ruby-on-rails

This is a newbie question. Is the migration (rake db:migrate) feature something that one would using during development or it's strictly a production tool for bringing the database up to date?

Ideally you want to use migrations only during development, and then load schema and seed the database in production. In reality, they'll allow you to make some changes and then deploy to production without any harm done.
Migrations allow you to work in iterations even on your database. You don't have to worry about forgetting to add something. When you start, just create the table as you think it's right, and you can fix it with another migration later. That's basically the idea. It takes away the one db script rules them all kind of thing.
A little example, if you have a User model with username and password and you need to add an email field, simply do this
rails generate migration AddEmailToUser # this is a convention, but you can name it however you want
class AddEmailToUser < ActiveRecord::Migration
def change
add_column :users, :email, :string
end
end
the change method will work both ways, when you apply the migration, and also when you need to revert it. It's kind of a neat Rails 3.1 magic.
The old version of migrations would look like this
class AddEmailToUser < ActiveRecord::Migration
def up
add_column :users, :email, :string
end
def down
remove_column :users, :email
end
end
Once you've added the migration, just run rake db:migrate and everything should work just fine. A big advantage of migrations is that if you manually mess up your database, you can easily just do
rake db:drop
rake db:create
rake db:migrate
or
rake db:migrate:reset # this might not work if you messed up your migrations
And you have the correct version of the database created

Migrations keep track of changes in your database schema. All changes with it (renaming column, changing tables, adding indexes etc.) should be done via migration. Thanks to that is really easy to deploy changes across multiple production servers.

Related

How to know that a rake task has been run in rails?

I've a migration in my Rails app, that I'd like to run only if a particular rake task has been run, otherwise I'll lose a bunch of data. Following is something that I'd like to do:
if has_rake_task_been_run?
remove_column :transactions, :paid_by
end
Currently, I couldn't find anyway, instead of assuring this thing manually. Is there any work around for it?
Using rake task for data migration is an extremely risky idea. Couple of reasons not to do this:
Even if you manage to find out whether your rake task has finished or not, your migration will still be marked as completed and you won't be able to replay it. Only way around is raising an exception in your migration.
No, you won't be able to rollback that migration neither. If rake task finishes after the migration has run, rollback will try to add already existing column.
Setting up your database from scratch by new devs will become painful as hell, as they will need to know which rake tasks are to be run when. Not to mentioned that rake db:migrate executes all migrations.
You're polluting your rake task list with non-reusable tasks
It seems that what you're doing is just a regular data migration, so all the stuff done by your rake task should be in fact part of your migration. That will even allow you to make a reversible data migration (in majority of cases).
Note however that data migrations are not that simple as regular scheme-only migrations. Because your migration should be completely independent on your code (as they are to work in the future, even when migrated model is completely removed from your codebase), so it is a common practice to redefine the models you are about to use in your migrations (only the bits required fro the migration). This is not that simple as it sounds, unfortunately, and honestly I am still looking for a perfect solution to that. The best I've seen so far is simple (I'm assuming that paid_by used to be string and you changed it paid_by_id, which references the user):
class YOURMIGRATIONNAME < ActiveRecord::Migration
class Transaction < ActiveRecord::Base
belongs_to :paid_by, class_name: "User"
end
class User < ActiveRecord::Base
end
def up
add_column :transaction, :paid_by_id, :integer
Transaction.transaction do # for speed
Transaction.find_each do |t|
t.paid_by_id = User.find_by(username: t[:paid_by])
t.save! # Always banged save in migration!
end
end
remove_column :paid_by
end
def down
add_column :transaction, :paid_by, :string
Transactions.transaction do
Transaction.find_each do |t|
t[:paid_by] = t.paid_by && t.paid_by.username
t.save!
end
end
remove_column :transactions, :paid_by_id
end
The only downfall of using the code above is that it won't work well if any of those models is using STI (I've made that mistake once, took a while to find out what's wrong). The work around is to define it outside of the migration class, but then those classes are available across all migrations and can be affected with your actual model code (especially in production when all the models are preloaded). In short, data migration with STI is something I am still looking into at the moment.
In case somebody would come here, We succcessfully used after_party rails library. With a simple mechanism, the library maintains rake tasks that have been executed and it becomes easy to perform migration tasks.

Keeping test and development databases in sync with rake

Using Rails 4.2, I want to add an age field to my User ActiveRecord class in a Postgres database using this line in a migration:
add_column :users, :age, :integer
then I obviously ran bin/rake db:migrate. Later I decided to make this a string field so rolled the migration back and changed the migration like so:
add_column :users, :age, :string
and migrated again. All was fine in my development database, but my test database seemed to have retained the old datatype. This is what I got using psql and running select column_name, data_type from information_schema.columns where table_name = 'users';
In development:
age | character varying
In test:
age | integer
Is this intentional or a bug?
First it's seldom a good idea to change a migration after it has already been applied.
But since you're already there, there are two possible ways to go about this: either rollback and migrate the test database or just load the schema for the test database. I recommend the second solution: just RAILS_ENV=test rake db:schema:load and you're good to go.
Aha! The issue is that the ActiveRecord feature below does not support rolling back of migrations:
ActiveRecord::Migration.maintain_test_schema!
and is detailed in this issue https://github.com/rails/rails/issues/15787, it seems this will be fixed in Rails 5.
When you run a db:migrate, if you don't specify environment, it'll only run the migrations for your development database. If you do a db:migrate RAILS_ENV=test, it'll run the migrations against your test database.

How can I properly drop an Active Record Migration?

Among my migrations, I have 3 that pertain to this question. In order, there is CreateEvents < ActiveRecord::Migration. The next down the line is CreateYears < ActiveRecord::Migration. Then the last is AddYearIdToEvents < ActiveRecord::Migration. The last one looks like this...
class AddYearIdToEvents < ActiveRecord::Migration
def change
add_column :events, :year_id, :integer, null: false, index: true
end
end
Now the problem is, whenever I try to drop the entire database (not near deploying to production), I get an obvious error of
ERROR: cannot drop table years because other objects depend on it
DETAIL: constraint events_year_id_fk on table events depends on table seasons
HINT: Use DROP ... CASCADE to drop the dependent objects too.
Now, I'm not a rails expert, but I believe I need to define a def down in that last migration. Rather than def change, do I need a def up and def down? The def down to drop this particular column.
If so, how can I do this. This migration is 20 migrations old. You can't just edit a migration like that can you? Do I add a new migration and just specify a def down? Or is the answer something completely different?
Have you tried rolling back your migrations? This undoes your migrations in reverse order, i.e. starting with the most recent migration.
$ rake db:rollback
If you have 20 migrations and you'd like to rollback all of them, you can use STEP=20
$ rake db:rollback STEP=20
Once you rollback past a migration that you'd like to change, you can change it.
In addition, you should be able to use rake db:drop, which drops the entire database rather than going through each migration in reverse order. If you'd like to drop the database, then recreate it and re-run all the migrations, you can run rake db:reset. Keep in mind this also runs rake db:seed if you have a db/seed.rb file.
And in response to your question of "Rather than def change, do I need a def up and def down?", the answer is no. Newer versions of Rails use def change since (among other reasons) it makes it easier to edit migrations once they are generated, i.e. if you spell a column name incorrectly when adding a column to a table.

adding a new column in a rails database migration

So I've been going through a lot of rails tutorial, and I get that the default for adding a new column to a database is , for example,
rails generate migration add_reset_to_users reset_digest:string reset_sent_at:datetime
The above will add a reset_digest in the form of a string and reset_sent_at in the form of a date to the migration add_reset_to_users
My questions is what if I am clumsy one night at 4 AM and only call the following
rails generate migration add_reset_to_users reset_digest:string
I completely forgot about reset_sent_at but want to implement it the next morning. I made the mistake of adding the link directly to the db file, which was a huge mistake.
In this case what should I do? Do I simply call a new migration such as
rails generate migration add_reset_sent_to_users reset_sent_at:datetime
or is there an even better way?
first, if you have not run your migration, you can directly open the migration file, and add your column to the file, as
def change
add columns :table_name :column_name :column_type
end
In your case, you will modify the file as,
def change
add columns :users :reset_digest :string
add columns :users :reset_sent_at :datetime
end
and then run
rake db:migrate
if you have already ran your migration, and you have not run any other migration after that, you can undo it, by
rake db:rollback STEP=1
and then edit the migration file, and run your migration
Rule of thumb for migrations in Rails is that you always create a new migration file unless you have not already shared your code with others, i.e. pushed to remote repository, otherwise you can just change the old migration after running $ rake db:rollback and everything will be fine, and nobody will know about it, and won't affect other developers work(since it's still on your local repository).
So, I'd encourage you to create a new migration if you have already committed and pushed the code onto remote repository, and changing the old migration file again will hurt other developers productivity. In case of any confusion, always create a new migration:
rails generate migration add_reset_sent_to_users reset_sent_at:datetime
I think it depends what state your rails app is in.
If you were working on a production app then editing migrations is not advisable due to data loss and changes should be made with a new migration.
If your working locally in development then I would edit the migration directly and add the missing column and rerun your migration.
Don't be worried about editing you migrations, just remember to rake db:rollback the migration you are editing before making changes or you will encounter errors.
This is where changing your migration from:
def change
add_column('users', 'reset_digest', :string)
add_column('users', 'reset_sent_at', :datetime) # Would have to perform rollback before adding this line
end
to:
def up
add_column('users', 'reset_digest', :string)
add_column('users', 'reset_sent_at', :datetime) # Added after migration
**rake db:migrate
end
def down
remove_column('users', 'reset_digest', :string)
remove_column('users', 'reset_sent_at', :datetime) # Add this after rollback
**rake db:rollback
end
Allows you to make changes to your migrations before you rake db:rollback
This requires a bit more code but I find it easier when I'm building a new app and things are changing frequently.

Writing database migration to reverse complex migration

I have a pretty old migration on a legacy app by a friend that contains this snippet:
class MakeChangesToProductionDbToMatchWhatItShouldBe < ActiveRecord::Migration
def self.up
# For some reason ActiveRecord::Base.connection.tables.sort().each blows up
['adjustments',
'accounts',
...## more rows of classes here ###...
'product_types'].each do |table|
t = table.singularize.camelize.constantize.new
if t.attributes.include?('created_at')
change_column( table.to_sym, :created_at, :datetime, :null => false ) rescue puts "#{table} doesnt have created_at"
end
if t.attributes.include?('updated_at')
change_column( table.to_sym, :updated_at, :datetime, :null => false ) rescue puts "#{table} doesnt have updated_at"
end
end
This old migration is now conflicting with a new migration I wrote to remove two of the tables mentioned in this long list, which is now causing any deployment to error upon rake db:migrate.
What's the correct kind of migration or down action to write to address this migration and get db:migrate working again?
There are a few different best practices that can help, but at the end of the day there's no good way to always upgrade a database from an arbitrary point without stepping through the codebase along as you run migrations (speaking of which, why is there not already a rake task to do this?).
Always include a migration-namespaced copy of the models you're working on. Example below.
When building a database from scratch, do not run migrations…use db:schema:load which will re-create the last snapshot of the database.
Don't give your migrations ridiculous and aggression fueled titles like MakeChangesToProductionDbToMatchWhatItShouldBe.
Avoid making assumptions, when writing migrations, about the environment they will be run in. This includes specifying table names, database drivers, environment variables, etc.
Write down actions when you write up actions whenever a down action is feasible. It's usually much easier (especially on esoteric or complex migrations) when the series of transformations is fresh in your head.
For this specific case, there's an argument to be made for declaring “Migration Bankruptcy” — clearing out some or all existing migrations (or refactoring and coalescing into a new one) to achieve the desired database state. When you do this you are no longer backwards compatible so it is not to be taken lightly, but there are times it is the appropriate move.

Resources