As you've probably guessed by the title of my article, I still consider Ruby on Rails as a relevant technology that offers a lot of value, especially when combined with ReactJS as it's frontend counterpart. Here's how I approach the topic.
I think that everyone who uses Rails knows what a migration is and how to use it. If you are not familiar with Rails migrations, I’ll try to introduce you into the subject.
Migrations are not only used in Rails, but you can find them in Django, Laravel or even in .NET MVC – they look pretty similar, and they have the same responsibility and goal.
Migrations are files, which are responsible for making any changes in the database. They can create or drop a table, add or remove a column, modify it, add an index or rename it or do plenty of different thing. They can also contain a code which makes some changes in the existing data, for example, update it or remove records which are invalid.
They help you to evolve your database schema over time without a problem. They convert a Ruby code into a SQL code, so you don’t even write a single line of SQL code.
As I stated before, migrations help to manage with a database schema. But what is the database schema? The schema is a file, which keeps information about your database, tables structure, information about indexes, foreign keys and added extensions.
An actual database schema could be find the db/schema.rb file, it looks something like:
ActiveRecord::Schema.define(version: 2018_07_31_134814) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" create_table "authors", force: :cascade do |t| t.string "first_name" t.string "last_name" t.datetime "created_at", null: false t.datetime "updated_at", null: false end end
In Rails, when you want to create a table (probably a model), you can do it like:
$ rails g model author first_name last_name
You probably noticed, that not only a model is generated when you request to create it but Rails also adds a few more files:
Running via Spring preloader in process 38927 invoke active_record create db/migrate/20180731134814_create_authors.rb create app/models/author.rb invoke rspec create spec/models/author_spec.rb
If you use RSpec, it creates for you a model’s spec file. ActiveRecord also creates a migration, which will create a new table in the existing database, normally it looks something like that:
class CreateAuthors < ActiveRecord::Migration[5.2] def change create_table :authors do |t| t.string :first_name t.string :last_name t.timestamps end end end
When you’ll run the migration, Rails returns an output which tells how much time took to run it:
$ rake db:migrate == 20180731134814 CreateAuthors: migrating ==================================== -- create_table(:authors) -> 0.0658s == 20180731134814 CreateAuthors: migrated (0.0659s) ===========================
If you used an older version of Rails previously, for example, version 3.0, you probably remember, that migration files looked a bit different, by default, there were two methods instead of one – change method. There was an up method and a down method. The up method was executed when you run a migration file, but the down method was executed when you reversed your changes by running the rake db:rollback command.
Yes, migrations are reversible! What does it mean? When you run a migration, it runs by a default the up method which is hidden under the change method, so when you want to create a table, or add a column, it will do this change, but when you revert this change, it’ll drop this table or remove the column – in this case, the down method is called.
But let’s be real, you don’t want only to add a new table or a new column, sometimes you also want to remove a column or a table. In this case, the up method will remove it, but the down method will add it again to the table – but without the old data, it’s imperative!
We should keep in mind, that when you remove a table and later revert this change, it will be created again, but it will be empty!
Rails are not able to restore data, because they convert a Ruby code into SQL queries and run them, like:
DROP table authors;
So if you want to convert our migration file into the old migration styled file, it should look like:
class CreateAuthors < ActiveRecord::Migration[5.2] def up create_table :authors do |t| t.string :first_name t.string :last_name t.timestamps end end def down drop_table :authors end end
Ok, now let’s try to revert our changes, let’s run the rollback method:
$ rake db:rollback == 20180731134814 CreateAuthors: reverting ==================================== -- drop_table(:authors) -> 0.0258s == 20180731134814 CreateAuthors: reverted (0.0381s) ===========================
As you can see, our changes have been reverted successfully.
Reversible migrations are a pretty cool feature! When we run a migration by mistake or it’s incorrect, we can revert it and start from scratch – but we should remember that our data won’t be restored – it’s vital!
More advanced examples
Previously, we focused on straightforward examples, now let’s move to something more complicated.
Let’s imagine, that we need to make a complex and complicated migration and we aren’t able to write this code in ActiveRecord, we need to execute pure SQL query, so we need to run the execute method like:
def change execute(‘AN ADVANCED SQL QUERY’) end
What will happen when you try to run a migration and then revert it later? Let’s try it:
$ rails g migration complex_sql_query
class ComplexSqlQuery < ActiveRecord::Migration[5.2] def change execute("INSERT INTO authors(first_name, last_name, created_at, updated_at) VALUES ('Foo', 'Bar', '2018-07-31', '2018-07-31');") end end
$ rake db:migrate == 20180731152957 ComplexSqlQuery: migrating ================================== -- execute("INSERT INTO authors(first_name, last_name, created_at, updated_at) VALUES ('Foo', 'Bar', '2018-07-31', '2018-07-31');") -> 0.0015s == 20180731152957 ComplexSqlQuery: migrated (0.0016s) ========================= $ rake db:rollback == 20180731152957 ComplexSqlQuery: reverting ================================== rake aborted! StandardError: An error has occurred, this and all later migrations canceled: This migration uses execute, which is not automatically reversible. To make the migration reversible, you can either: 1. Define #up and #down methods in place of the #change method. 2. Use the #reversible method to define reversible behavior. Caused by: ActiveRecord::IrreversibleMigration
Yes, as you can see, there is no possibility to revert this migration, automatically Rails thrown ActiveRecord::IrreversibleMigration exception.
Now let’s imagine, that you want to revert your migration on production, because it turned out, that somehow, it’s invalid (but anyway, we shouldn’t do that, this situation shouldn’t ever happen), or you just pulled the newest changes from git to your branch and you just want to undo them but you’ve already run the migration.
Now, you see that it’s vital to write reversible migrations, because they can save a lot of time and nerves.
Probably there is a lot of different cases where reversible migrations could help you, it’s important to think about them. If a migration can be processed, we also should be able to revert it!
If we want to have an irreversible migration, it should be well documented and included in the down method by throwing an ActiveRecord::IrreversibleMigration exception.
Now, we have a few possibilities and options how we can rewrite this migration.
In the first example, we’ll rewrite the update method to up and down and raise an exception, like:
class ComplexSqlQuery < ActiveRecord::Migration[5.2] def up execute("COMPLEX SQL QUERY") end def down raise ActiveRecord::IrreversibleMigration end end
Here, we throw an exception and notify a user, that this migration shouldn’t be reverted. That shows a user that he shouldn’t revert it because we did something which can’t be restored.
Reversible migration – not the good way
Another thing which we can do is to modify the down method and allow to revert this migration and prompt a message:
class ComplexSqlQuery < ActiveRecord::Migration[5.2] def up execute("COMPLEX SQL QUERY") end def down puts 'This migration makes some breaking changes, should not be reverted.' end end
In this particular example, we allow a user to revert the migration, but we should think if we want to do it. Let’s imagine, that this migration will be reverted and the user will rerun it. Maybe it’ll break some data in our database!
Another useful method is the reversible block. In this block, we can define which part of our migration should be reverted and how should it be handled. Let’s check more complex example:
class ReversibleMigration < ActiveRecord::Migration[5.2] def change create_table :authors do |t| t.string :first_name t.string :last_name end reversible do |migration| migration.up do execute <<-SQL SOME SQL QUERY SQL end migration.down do execute <<-SQL SOME SQL QUERY SQL end end add_column :authors, :age, :integer rename_column :authors, :last_name, :surname end end
As you can see, we don’t even need to write the up and down methods. We only need to include them only in the reversible block, so in this case, only something more complex will be run separately in these blocks. When we roll back our changes, Rails knows how to run create_table, add_column and rename_column methods and restore everything, so we don’t need to include them in separate blocks!
I think that this migration is more clear and easier to read, we define which changes are handled by us manually, and which by Rails.
The revert method
There is one more cool thing in migrations; if you want to revert one migration by including it in another one, you can do it too! Let’s imagine, that you want to revert one migration which added one field into a table and also add another field to the same table or you want to do something completely different.
There are two ways how can we do it:
- write two separate migrations
- use revert method in a migration file with a migration name as a parameter
Let’s assume that we want to revert a migration which is called MyMigrationNumberOne and create another one, in this case, we can create something like this:
require_relative '20180101101112_my_migration_number_one' class NewMigration < ActiveRecord::Migration[5.2] def change revert MyMigrationNumberOne create_table(:new_table) do |t| t.string :some_field end end end
Now, when we run this migration, the MyMigrationNumberOne migration will be reverted, and at the same time, we’ll create a new table – pretty impressive, isn’t it?