Serialize AR scope?

I had a similar need although the specific need was for sending a relation to an ActiveJob. Sharing it even if that is not your goal as it might be a useful pointer in the right direction or something you can build on.

The key thing for me was limiting the scope of what can be serialized. There is a method called where_values_hash which returns the where conditions hash. The caveat is it is a public method although nodoc so that makes is pseudo-private.

Since it just provides the where hash this means it cannot serialize things like a joins, includes or even where.not. Also SQL fragments won’t work such as where('name ILIKE ?', 'John%'). But it does still work if you have multiple where conditions such as in a method chain that is built up: Widget.where(foo: 'bar').where(baz: 'boo').

Most of the time this is fine and even if not I can usually work around the limitation in some way. Here is the ActiveJob serializer I wrote:

class ActiveJob::RelationSerializer < ActiveJob::Serializers::ObjectSerializer
  def serialize? argument
    super && where_hash_only?(argument)
  end

  def serialize scope
    super klass: scope.klass, conditions: scope.where_values_hash
  end

  def deserialize serialized
    serialized[:klass].where serialized[:conditions]
  end

  private

  def where_hash_only? scope
    scope.to_sql == scope.klass.where(scope.where_values_hash).to_sql
  end

  def klass = ActiveRecord::Relation
end

Note the check on serialization to see if the scope violates any of my limitations. I take the scope being serialized and then try to re-created it with the where_values_hash. If the SQL string doesn’t match then that means someone tried to serialize a relation that was more complex than this allowed.


If all this is too limiting then my next strategy is just to pluck the ids. Example:

ids = Widget.some_complex_scope.pluck(:id)

Then later when I need the records in that scope I can:

Widget.where id: ids

The caveat here is the records in that set may change between the time it was seralized and the time it was deserialized. Also anything like a pre-load (includes, etc) won’t happen since those are secondary queries.