How a chaining interface like Rails ActiveRecord is built? - ruby-on-rails

I'm intrigued. How User.where(name: 'Foo').where(age: 20) works with only one database call?
Does the method where know if it is the last of the chain and change its behavior? And if so, how that can be done?

It returns self after each time it adds the query to its internal query builder.
where doesn't necessarily know where it's at; ActiveRecord waits for itself to be enumerated on before even querying the database. Example:
users = User.where(active: true)
users.loaded? # false
users.each { }
users.loaded? # true
each, map, first, last, etc. all will trigger the query to be loaded.
Here's an example of a super-naive query builder:
class FakeRecord
include Enumerable
def self.all_args
#all_args ||= []
end
def self.where(*args)
all_args << args
self
end
def self.each
puts "Executing sql #{all_args.join(", ")}"
yield [1, 2, 3]
end
end
FakeRecord.where(potato: true).where(dinosaur: false).each do |thing|
puts thing
end

Related

Monkey patching ActiveRecord "where" method

I'm trying to add some additional functionality to where method in ActiveRecord. I reached half way by doing monkey patch but facing problem with chain queries.
Ex:
User.where(id: 10, name: 'Blob')
When I execute above code my new functionality is working as expected.
It triggers modified where method and gives query params as
query_params = { id: 10, name: 'Blob' }
User.where(id: 10).where(name: 'Blob')
In this case i'm getting only
query_params = { id: 10 }
I'm just printing the query params and delegating to super class:
def where(query, *values)
query.each do |key, value|
if value.is_a?(Array) and value.flatten != value
Rails.logger.debug "Where Clause Params"
Rails.logger.debug "#{query}"
Rails.logger.debug "#{caller.join("\n")}"
break
end
end
super
end
This is how I extend modified where file into ApplicationRecord
ApplicationRecord.extend CoreExtensions::ApplicationRecord::WhereLog
It would be great if anyone helps.

RSpec testing model method

I have this method in my models/images.rb model. I am starting with testing and having a hard time coming up with tests for it. Would appreciate your help.
def self.tags
t = "db/data.csv"
#arr = []
csvdata = CSV.read(t)
csvdata.shift
csvdata.each do |row|
row.each_with_index do |l, i|
unless l.nil?
#arr << l
end
end
end
#arr
end
First off a word of advice - CSV is probably the worst imaginable data format and is best avoided unless absolutely unavoidable - like if the client insists that manipulating data in MS Excel is a good idea (it is not).
If you have to use CSV don't use a method name like .tags which can confused for a regular ActiveRecord relation.
Testing methods that read from the file system can be quite difficult.
To start with you might want to alter the signature of the method so that you can pass a file path.
def self.tags(file = "db/data.csv")
# ...
end
That way you can pass a fixture file so that you can test it deterministically.
RSpec.describe Image do
describe "tags" do
let(:file) { Rails.root.join('spec', 'support', 'fixtures', 'tags.csv') }
it 'returns an array' do
expect(Image.tags(file)).to eq [ { foo: 'bar' }, { foo: 'baz' } ]
end
end
end
However your method is very ideosyncratic -
def self.tags
t = "db/data.csv"
#arr = []
self.tags makes it a class method yet you are declaring #arr as an instance variable.
Additionally Ruby's enumerable module provides so many methods for manipulating arrays that using an outer variable in a loop is not needed.
def self.tags(file = "db/data.csv")
csv_data = CSV.read(file)
csv_data.shift
csv_data.compact # removes nil elements
end

using print inside def having yield statement

I am trying to print inside a function.
The function is used for invoking a block.
But I don't see the print happening in the function definition.
Please shed a light on this. Basically I am not clear with the control flow.
def find_all
matching_items = []
self.each do |item|
if yield(item)
puts "after yield" #print not happening
matching_items << item
end
end
matching_items
end
p ['a', 'b','c','c'].find_all { |item|
if item == 'a'
true
end
}
If your code is exactly as written, you are defining and independent method find_all defined on main. When you type [1,2,3,4].find_all, you are calling the find_all method on Array, which is defined in the Enumerable method. So you are not calling your method at all.
What you are probably trying to do is
class Array
def find_all
...
end
end
This way, [1,2,3,4].find_all will call this method.
However, note that this is probably a bad idea: you're overriding a core method that in a class that isn't yours, so that could have consequences in other code that you are not able to anticipate if any other code uses the find_all method.
What you might try instead is to define a method that takes the array in as an argument. You might move this to a module, but for now:
def find_all(array)
matching_items = []
array.each do |item|
if yield(item)
puts "after yield" #print not happening
matching_items << item
end
end
matching_items
end
Of course, this is basically what Enumerable#find_all already does, but less efficiently: so perhaps this is just an academic exercise, in which case, great!, but otherwise, why not just use the existing method?
If you are trying to re-open the class Array then, this is how you can do it
class Array
def find_all(matching_items = [])
self.each do |item|
if yield(item)
puts "after yield" #print not happening
matching_items << item
end
end
matching_items
end
end
p ['a', 'b', 'c', 'c'].find_all { |item|
if item == 'a'
true
end
}
Output
after yield
["a"]

Why group calculation fields do not show up in query result?

I have query like this:
query = Link.select('url, max(created_at) as created_at, count(*) as url_count').group(:url).order('url_count desc, created_at asc')
Sample results of query.results.first:
2.2.0 :002 > query.first
=> #<Link id: nil, url: "http://1", created_at: "2015-03-10 16:43:54">
Why there is no url_count here, even though I know it is.
2.2.0 :003 > query.first.url_count
=> 17
The count is there all along but the model to_s method does not know about it.
The to_s method which is used when your console logs the result from query.first is defined somewhere in activerecord and it uses the attributes defined for the model in the database. Since your attribute is only defined for this particular instance of Link not for the model Link.
I found this quite interesting. Below is a description of how the message displayed in your console is constructed. It starts out in the gem active_attr with these 3 methods displayed below:
def inspect
attribute_descriptions = attributes.sort.map { |key, value| "#{key}: #{value.inspect}" }.join(", ")
separator = " " unless attribute_descriptions.empty?
"#<#{self.class.name}#{separator}#{attribute_descriptions}>"
end
# ...
def attributes
attributes_map { |name| send name }
end
# ...
def attributes_map
Hash[ self.class.attribute_names.map { |name| [name, yield(name)] } ]
end
the method attributes_names is defined in the gem activerecord
def attribute_names
#attribute_names ||= if !abstract_class? && table_exists?
column_names
else
[]
end
end
# ...
def column_names
#column_names ||= #connection.columns(#table_name).collect { |c| c.name }
end
And that is why your count does not show up.
If you really want it to show up in your console you could override the inspect method and add it there.

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).
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.)
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.
These days (ruby >= 2.5) you can use Object.yield_self:
def self.search(options = {})
all.yield_self do |s|
s = s.where(first_name: options[:query]) if options[:query]
s = s.where(graduated: options[:graduated]) if options[:graduated]
s
end
end

Resources