23 May 2014

In Rails 3 any association could be accessed inside a model instance and be modified to various extents. Accessing it worked similar to this:

class Foo < ActiveRecord::Base
  has_many :bars
end

assoc = Foo.new.association(:bars).scoped
puts assoc.to_sql

A pretty useful operation on an association was fiddling with the where_values to delete, inject or replace certain expressions in the sql.

As of Rails 4 the Squeel gem introduces bindings to internal sql calls generated by associations.

SELECT * FROM bars WHERE foo_id = $1 --{ $1 => 1 }

In the rails console queries now look similar to the above example. While it has certainly a few advantages, working with the associations inside rails has become slightly more difficult.

To replace values inside bindings, they have to be bound manually now. A brief example shows how: (Please note that Rails 4 has replaced scoped with scope)

class Foo < ActiveRecord::Base
  has_many :bars
end

assoc = Foo.new.association(:bars).scope
bindings = assoc.bind_values
old_where_vals = assoc.where_values

A part of the snipplet is copied from the old example above which worked in Rails 3 and the last 2 lines are new. bind_values offers read-only access to the bindings and the last line just stores the old values away so we can restore them later on.

In the next step, the where-values are loops and bound by hand.

assoc.where_values = assoc.where_values.map do |w|
  begin # w can be a proxy class that does not care about .duplicable?
    node = w.dup
  rescue TypeError
    node = w
  end

  if node.is_a?(Arel::Nodes::Binary) && node.right =~ /^\$\d+$/
    value = bindings.detect{ |bind, _| bind.name == node.left.name.to_s }
    if value.present?
      node.right = value.last.to_s
    end
  end
  next node
end

All where_values are nodes from arel or proxy objects. Unfortunately it is not possible to duplicate a proxy object which requires special handling in the loop.

Nodes in arel usually inherit from a binary type that has properties for left and right, traditionally with the column name on the left and either another node or a binding on the right.

Squeel stores bindings by remembering the column name from the binary node’s left side. For nodes with a binding on the right, the bind value is detected and inserted. This emulates the behaviour before squeel.

puts assoc.to_sql
assoc.where_values = old_where_vals

In the last step, the modified association can be transformed to sql and the original Squeel values are restored. Without this step, the association stops working properly. It is shared between all model instances.