Search code examples
ruby-on-railsrakeruby-on-rails-5

Rails 5 - generate and run migrations inside a rake task


So I've got an app that when I create a new user, I setup columns in some other databases related to that specific user. I realize this isn't best practice, but for my use-case it's much faster than serializing an array which holds all user info for that table.

What I'm trying to do is setup a rake task which creates the User, as well as doing the necessary migrations to the tables.

Here's what I have so far:

  desc "Adds User and creates correct DB entries."
  task add_user: :environment do
    username = ENV['username'].to_s
    email = ENV['email'].to_s
    password = ENV['password'].to_s
    initials = ENV['initials'].to_s
    if username and email and password and initials
      User.create! :username => username, :email => email, :password => password, :password_confirmation => password, :initials => initials
      Rake::Task['generate migration AddPay' + initials + 'ToShoppingLists pay' + initials + ':decimal'].invoke
      Rake::Task['generate migration AddPay' + initials + 'ToPayments pay' + initials + ':decimal'].invoke
      Rake::Task['db:migrate'].invoke
    end
  end

My issues is that in Rails 5, I have to run rails g migration not rake g migration so I'm unsure how to call rails commands from inside a rake task.

Additionally, is there a way to check if a migration has already been created? For example, if I run this in development mode, I don't need to recreate the migration in production mode, just perform db:migrate.


Solution

  • You can use Rake's sh method and just call the rails shell commands.

    sh "rails g migration AddPay#{initials}ToShoppingLists pay#{initials}:decimal"
    sh "rails g migration AddPay#{initials}ToPayments pay#{initials}:decimal"
    

    When using sh as opposed to ruby's built-in backtick delimiters for shell commands, if the command has an exit status other than 0 it will raise an exception and abort the task.

    To see if your migration has already been created, you can just check for the existence of a migration file matching the naming pattern.

    files = Dir.glob Rails.root.join('db/migrate/*')
    
    migration_patterns = { 
      /add_pay_#{initials.downcase}_to_shopping_lists/ => "rails g migration AddPay#{initials}ToShoppingLists pay#{initials}:decimal",
      /add_pay_#{initials.downcase}_to_payments/ => "rails g migration AddPay#{initials}ToPayments pay#{initials}:decimal"
    }
    
    migration_patterns.each do |file_pattern, migration_command|
      if files.none? { |file| file.match? file_pattern }
        sh migration_command
      end
    end
    
    Rake::Task['db:migrate'].invoke
    

    This assumes you won't have any migration naming collisions that raise false positives in none?. But Rails won't let you have migration naming collisions anyway, so the check might not be necessary. It seems like you are bound to run into this problem eventually given the way you're naming the migrations and columns. What if two users have the same initials?

    Might there be a way you can accomplish what you need to by using an additional database table (maybe a polymorphic join table?) instead of adding columns for every user? Something along these lines could work:

    class CreateDisbursements < ActiveRecord::Migration[5.1]
      def change
        create_table :disbursements do |t|
          t.decimal :amount
          t.integer :payable_id
          t.string :payable_type
          t.integer :receivable_id
          t.string :receivable_type
    
          t.timestamps
        end
    
        add_index :disbursements, [:payable_type, :payable_id]
        add_index :disbursements, [:receivable_id, :receivable_type]
      end
    end
    
    class Disbursement < ApplicationRecord
      belongs_to :payable, polymorphic: true
      belongs_to :receivable, polymorphic: true
    end
    
    class ShoppingList < ApplicationRecord
      has_many :disbursements, as: :payable
      has_many :users, through: :disbursements, source: :receivable, source_type: 'User'
    end
    
    class Payment < ApplicationRecord
      has_many :disbursements, as: :payable
      has_many :users, through: :disbursements, source: :receivable, source_type: 'User'
    end
    
    class User < ApplicationRecord
      has_many :disbursements, as: :receivable
      has_many :payments, through: :disbursements, source: :payable, source_type: 'Payment'
      has_many :shopping_lists, through: :disbursements, source: :payable, source_type: 'ShoppingList'
    end
    
    user = User.find params[:user_id]
    payment = Payment.find params[:payment_id]
    amount = params[:amount]
    
    payment.disbursements.create(amount: amount, receivable: user)
    user.disbursements.create(amount: amount, payable: payment)
    Disbursement.create(amount: amount, payable: payment, receivable: user)
    user.payments
    payment.users