Validating ActiveRecord Backlinks exist

Whenever a new association is added usually we also need the opposite association to ensure things get cleaned up properly during deletion.
To never forget this and audit the current state, these two tests can help.

  def all_models
    models = Dir["app/models/**/*.rb"].grep_v(/\/concerns\//)
    models.size.must_be :>, 20
    models.each { |f| require f }
    ActiveRecord::Base.descendants
  end

  it "explicity defines what should happen to dependencies" do
    bad = all_models.flat_map do |model|
      model.reflect_on_all_associations.map do |association|
        next if association.is_a?(ActiveRecord::Reflection::BelongsToReflection)
        next if association.options.key?(:through)
        next if association.options.key?(:dependent)
        "#{model.name} #{association.name}"
      end
    end.compact
    assert(
      bad.empty?,
      "These associations need a :dependent defined (most likely :destroy or nil)\n#{bad.join("\n")}"
    )
  end

  it "links all dependencies both ways so dependencies get deleted reliably" do
    bad = all_models.flat_map do |model|
      model.reflect_on_all_associations.map do |association|
        next if association.name == :audits
        next if association.options.fetch(:inverse_of, false).nil? # disabled on purpose
        next if association.inverse_of
        "#{model.name} #{association.name}"
      end
    end.compact
    assert(
      bad.empty?,
      <<~TEXT
        These associations need an inverse association.
        For example project has stages and stage has project.
        If automatic connection does not work, use `:inverse_of` option on the association.
        If inverse association is missing AND the inverse should not destroyed when dependency is destroyed, use `inverse_of: nil`.
        #{bad.join("\n")}
      TEXT
    )
  end

Running karma js with rails asset pipeline / sprockets

# test/karma.conf.js
...
    basePath: '<%= Bundler.root %>',
...
    files: [
      '<%= resolve_asset('vis.js') %>',
      'app/assets/javascripts/app.js',
      'test/**/*_spec.js'
    ],

# Rakefile
namespace :test do
  task js: :environment do
    with_tmp_karma_config do |config|
      sh "./node_modules/karma/bin/karma start #{config} --single-run"
    end
  end

  private

  def with_tmp_karma_config
    Tempfile.open('karma.js') do |f|
      f.write ERB.new(File.read('test/karma.conf.js')).result(binding)
      f.flush
      yield f.path
    end
  end

  def resolve_asset(file)
    Rails.application.assets.find_asset(file).to_a.first.pathname.to_s
  end
end

Cached .all(:include=>[:xxx]) on associations

When fetching all associations with includes they are not cached, but could be, since they are still the same records(unlike with :select/:conditions etc)

user = User.first
user.comments.length # hits db
user.comments.length # cached

user = User.first
user.commens.all(:include=>:comenter).length  # hits db
user.commens.all(:include=>:comenter).length  # hits db
user.comments.length # hits db

Cached find all with includes
This can save requests when performing repetitive calls to the same record.

user = User.first
user.comments.load_target_with_includes([:commenter, :tags]).length # hits db
user.comments.load_target_with_includes([:commenter, :tags]).length # cached
user.comments.length # cached

Code

# do not load an association twice, when all we need are includes
# all(:include=>xxx) would always reload the target
class ActiveRecord::Associations::AssociationCollection
  def load_target_with_includes(includes)
    raise if @owner.new_record?

    if loaded?
      @target
    else
      @loaded = true
      @target = all(:include => includes)
    end
  end
end

Big updates block database, use slow_update_all

Sometimes big updates that affect millions of rows kill our database (all queries hang/are blocked).
Therefore we built a simple solution:

class ActiveRecord::Base
  def self.slow_update_all(set, where, options={})
    ids_to_update = find_values(:select => :id, :conditions => where)
    ids_to_update.each_slice(10_000) do |slice|
      update_all(set, :id => slice)
      sleep options[:sleep] if options[:sleep]
    end
    ids_to_update.size
  end
end


This needs ActiveRecord find_values extension