Setting default value for reference in Rails migration

For context, let’s create a new User model and add some user to the database.

$ bundle exec rails generate model User name:string
$ bundle exec rails db:migrate
$ echo 'User.create(name: "Joe")' | bundle exec rails console

Our task is to introduce a new Group model. Each User is associated with one Group and each Group is associated with many Users.

$ bundle exec rails generate model Group name:string
$ bundle exec rails generate migration AddGroupRefToUsers group:references

Ready! It’s time to run migrations.

$ bundle exec rails db:migrate
== 20201007213907 CreateGroups: migrating =====================================
-- create_table(:groups)
   -> 0.0291s
== 20201007213907 CreateGroups: migrated (0.0293s) ============================

== 20201007213916 AddGroupRefToUsers: migrating ===============================
-- add_reference(:users, :group, {:null=>false, :foreign_key=>true})
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:

PG::NotNullViolation: ERROR:  column "group_id" contains null values
/Users/kamilszubrycht/Documents/Development/my_super_app/db/migrate/20201007213916_add_group_ref_to_users.rb:3:in `change'
bin/rails:4:in `<main>'

Caused by:
ActiveRecord::NotNullViolation: PG::NotNullViolation: ERROR:  column "group_id" contains null values
/Users/kamilszubrycht/Documents/Development/my_super_app/db/migrate/20201007213916_add_group_ref_to_users.rb:3:in `change'
bin/rails:4:in `<main>'

Caused by:
PG::NotNullViolation: ERROR:  column "group_id" contains null values
/Users/kamilszubrycht/Documents/Development/my_super_app/db/migrate/20201007213916_add_group_ref_to_users.rb:3:in `change'
bin/rails:4:in `<main>'
Tasks: TOP => db:migrate
(See full trace by running task with --trace)

Oops. That’s where the problem begins. We are mainly interested in second migration which adds group reference to the users table.

class AddGroupRefToUsers < ActiveRecord::Migration[6.0]
  def change
    add_reference :users, :group, null: false, foreign_key: true
  end
end

We can’t add a group reference to the users table, because users table is not empty and group_id column is not allowed to be null. So, we need to make sure that every user is associated to the group. For this we have to create a new group and use its id as a defult value for the group_id column. Once reference is added we are free to change the default group_id value to null.

class AddGroupRefToUsers < ActiveRecord::Migration[6.0]
  def up
    default_group_id = Class.new(ApplicationRecord)
                            .tap { |c| c.table_name = :groups }
                            .find_or_create_by(name: 'Default group')
                            .id
    add_reference :users, :group, null: false, foreign_key: true, default: default_group_id
    change_column_default :users, :group_id, nil
  end

  def down
    remove_reference :users, :group
  end
end

It’s worth to mention that instead of using Group model, I created a new anonymous class which inherits from the ApplicationRecord and then I explicitly set the table name to groups. In general, using the model classes in migrations is considered to be an anti-pattern. Let’s run migrations.

$ bundle exec rails db:migrate
== 20201007213916 AddGroupRefToUsers: migrating ===============================
-- add_reference(:users, :group, {:null=>false, :foreign_key=>true, :default=>1})
   -> 0.0147s
-- change_column_default(:users, :group_id, nil)
   -> 0.0045s
== 20201007213916 AddGroupRefToUsers: migrated (0.0432s) ======================

Success! \ (•◡•) /

#ruby #rails #postgres