Darkstarone
Darkstarone

Reputation: 4730

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.

Upvotes: 2

Views: 1797

Answers (1)

m. simon borg
m. simon borg

Reputation: 2575

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

Upvotes: 1

Related Questions