« Back

🛤

some Rails tips

on programming
last updated 7/23/21

batch processing

#find_each is an Active Record method for efficiently handling big collections of database records.

This is a simplistic comparison, but something like Customer.all.each... would fetch an entire table all at once whereas Customer.find_each... would process records in batches. The Rails docs explain much better than me.

When we're certain the collection returned will be smaller than 1,000 records, #all works just fine. Otherwise, and especially with production data (!), use #find_each.


# for each customer with an :email attribute present, set :emailable to true
# efficient 👍
Customer.find_each do |customer|
  customer.update!(emailable: true) if customer.email.present?
end

# relatively inefficient 👎
Customer.all.each do |customer|
  customer.update!(emailable: true) if customer.email.present?
end
        

writing decent migrations

Running a Rails migration to add a column that already exists can trigger an error:


# when the customers table already has a birthday column
class AddBirthdayToCustomers < ActiveRecord::Migration[5.2]
  def change
    add_column :customers, :birthday, :date
  end
end

rake db:migrate #=>
# StandardError: An error has occurred...
# PG::DuplicateColumn: ERROR:  column...
      

Instead, we can write something like this to check for pre-existing columns and avoid DuplicateColumn errors:


class AddBirthdayToCustomers < ActiveRecord::Migration[5.2]
  def change
    unless column_exists?(:customers, :birthday)
      add_column :customers, :birthday, :date
    end
  end
end

# and with Rails 6.1, we have if_exists / if_not_exists
class AddBirthdayToCustomers < ActiveRecord::Migration[6.1]
  def change
    add_column :customers, :birthday, :date, if_not_exists: true
  end
end
      

There's a decent argument to avoid using .reset_column_information — shoutout this guy I don't know for making a thoughtful case — but as the Rails API docs explain, it can be useful for populating new columns or tables with default values.

As an example, the acts_as_list gem is handy for implementing sorting functionality. Adding this functionality to existing models requires us to set default position values:


# form definitions have many questions, a question belongs to a form definition
# we want to allow sorting questions for form definitions
class AddPositionToFormQuestions < ActiveRecord::Migration[6.1]
  def change
    unless column_exists?(:form_questions, :position)
      add_column :form_questions, :position, :integer

      # after adding a :position column to the form_questions table
      # we ensure the table is reloaded on our next request and :position exists...
      Form::Question.reset_column_information

      # ...so we can set a default :position value for each form question
      Form::Definition.find_each do |form_definition|
        form_definition.questions.order(:updated_at).each.with_index(1) do |question, index|
          question.update_column :position, index
        end
      end
    end
  end
end