Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Building a Rails scope using `tap`

I have a method that looks something like

class Student < ActiveRecord::Base
  def self.search(options = {})
    all.tap do |s|          
      s.where(first_name: options[:query])     if options[:query]
      s.where(graduated:  options[:graduated]) if options[:graduated]

      # etc there are many more things that can be filtered on...
    end
  end
end

When calling this method though, I am getting back all of the results and not a filtered set as I would expect. It seems like my tap functionality is not working as I expect. What is the correct way to do this (without assigning all to a variable. I would like to use blocks here if possible).

like image 951
Kyle Decot Avatar asked Feb 15 '23 00:02

Kyle Decot


2 Answers

tap will not work for this.

  • all is an ActiveRecord::Relation, a query waiting to happen.
  • all.where(...) returns a new ActiveRecord::Relation the new query.
  • However checking the documentation for tap, you see that it returns the object that it was called on (in this case all) as opposed to the return value of the block.

    i.e. it is defined like this:

    def tap
      yield self # return from block **discarded**
      self
    end
    

    When what you wanted was just:

    def apply
      yield self # return from block **returned**
    end
    

    Or something similar to that.

This is why you keep getting all the objects returned, as opposed to the objects resulting from the query. My suggestion is that you build up the hash you send to where as opposed to chaining where calls. Like so:

query = {}
query[:first_name] = options[:query]     if options[:query]
query[:graduated]  = options[:graduated] if options[:graduated]
# ... etc.

all.where(query)

Or a possibly nicer implementation:

all.where({
  first_name: options[:query],
  graduated:  options[:graduated],
}.delete_if { |_, v| v.empty? })

(If intermediate variables are not to your taste.)

like image 189
amnn Avatar answered Feb 23 '23 12:02

amnn


You can easily create a let function:

class Object
  def let
    return yield self
  end
end

And use it like this:

all.let do |s|          
  s=s.where(first_name: options[:query])     if options[:query]
  s=s.where(graduated:  options[:graduated]) if options[:graduated]

  # etc there are many more things that can be filtered on...

  s
end

The difference between tap and let is that tap returns the object and let returns the blocks return value.

like image 38
Idan Arye Avatar answered Feb 23 '23 13:02

Idan Arye