Can you use association extensions on an object with custom finder_sql? - ruby-on-rails

I was under the impression that the custom_finder option in ActiveRecord meant that you got association extensions like .where and .order for free, for instance:
class Dog < ActiveRecord::Base
has_many :fleas, class_name: 'Flea',
finder_sql: "select fleas.* from fleas where pet_type = 'dog'"
end
granted this is not a great example as the 'finder_sql' is so trivial however it illustrates the point.
Generated SQL
I would expect the following
#dog = Dog.find(5)
#dog.fleas.where('num_legs > 2')
to generate
"select fleas.* from fleas where pet_type = 'dog' AND num_legs > 2
i.e. custom finder_sql + where clause
however what it actually generates is
"SELECT "base_posts".* FROM "fleas" WHERE "fleas"."dog_id" = 5 AND (num_legs > 2)
i.e. it completely ignores the custom finder_sql and tries to join the fleas to the current dog.
If the custom finder_sql doesn't cause the association extensions to respect it then what's the point in it - it could just be a method on the object...

This is true. I think that custom finder is the legacy of the previous generation of finders in Rails (before Arel && AR::Relation) and its presence in associations today is only for backward compatibility (imho). Anyway, it does not fit the ideology of AR::Relation (extensions).
By now, we have .find_by_sql() and we have :finder_sql as options for associations.
Actually now, when association has :finder_sql option, it is only used for the formation of records collection (using .find_by_sql). Association creates the AR::Relation object, loads collection (.target) and delegating AR::Relation method calls to relation object, which however was not aware of any custom SQL statements and knows only primary SQL expression. This is why:
#dog.fleas.class
#=> Array
#dog.fleas.to_sql # delegated to AR::Relation
returns primary expression "SELECT "base_posts".* FROM..." and
#dog.fleas.where("...") # delegated to AR::Relation
imposes new conditions as if there is no custom finder.
On the other hand, since .find_by_sql always returns an array, to impose new conditions could be possible using Array's methods.
#dog.fleas.select {|r| ... } # delegated to Array

Related

Rails optional association query

i've been trying to solve this issue for some time now with no success :(
i have 2 model classes - ConfigurationKey and ConfigurationItem, as follows:
class ConfigurationKey < ActiveRecord::Base
has_many :configuration_items
# this class also has a 'name' attribute
end
class ConfigurationItem < ActiveRecord::Base
belongs_to :app
belongs_to :configuration_key
end
i would like to fetch all of the ConfigurationKeys that have a specific 'name' attribute, along with a filtered subset of their associated ConfigurationItems, in one single query.
i used the following command:
configuration_key = ConfigurationKey.includes(:configuration_items).where(name: key_name, configuration_items: { app: [nil, app] })
but the ConfigurationKeys that don't have any associated ConfigurationItems are not returned.
i thought the the 'includes' clause, or the explicit use of 'LEFT OUTER JOIN' would make it work, but it didn't :/
is there any possible way to do this, or do i have to use 2 queries - one to get all of the relevant ConfigurationKeys, and another in order to get all of the relevant ConfigurationItems?
thanks ;)
Using includes with where clause in rails 4.2 generates a LEFT OUTER JOIN query.
Please take a look at the generated sql in rails console.
$ rails c
> ConfigurationKey.includes(:configuration_items).where(name: key_name, configuration_items: { app: [nil, app] })
# Sql is displayed...
Probably, you'll see LEFT OUTER JOINed sql, and if so, it's correct.
Mind you, what you get via ActiveRecord is NOT equals to results from sql. It returns you DISTINCT results from its sql.
So, I think it's impossible to make a success in one single query.

Exclude 'nil' row from ActiveRecord query with a HAVING clause where no results are returned

I'm building a series of methods in a model to use as scopes. When one of these uses a having clause and that query returns no results, I get an instance of the model returned where all fields are nil, and that breaks most code I'd like to use these scopes in.
The below is a highly simplified example to demonstrate my issue.
class Widget < ActiveRecord::Base
attr_accessible :name
has_many :components
def without_components
joins(:components).group('widgets.id')having('COUNT(components.id) = 0')
end
def without_components_and_remove_nil
without_components.select{|i| i.id} # Return objects where i.id is not nil
end
end
Calling Widget.without_components if all Widgets have components assigned returns the non-desirable:
[{id: nil, name: nil, user_id: nil}]
But if I call Widget.without_components_and_remove_nil it converts the ActiveRecord::Relation object that would be returned into an Array, so I can't chain it with other scopes as I need to do.
Is there a way of changing the scopes so that either the nil row is excluded if it appears, or is there a modification that could be made to my ActiveRecord query to allow for this to work?
There were two issues I needed to resolve to get that scope working; one not related to my original question, though the scope I presented above wouldn't have been fixed without it:
First, and directly dealing with the question at hand, since Rails 3.1 you can do this:
.
Widget.where(id: Widget.joins(:components).group('widgets.id').having('COUNT(components.id)'))
Second, the joins(:components) part of it wasn't going to work with the having('COUNT(components.id = 0)') because joins performs an inner join, thus there would never be any results for that query. I had to replace the joins with joins("LEFT OUTER JOIN components ON components.widget_id = widgets.id").

Ruby on Rails 4 count distinct with inner join

I have created a validation rule to limit the number of records a member can create.
class Engine < ActiveRecord::Base
validates :engine_code, presence: true
belongs_to :group
delegate :member, to: :group
validate :engines_within_limit, on: :create
def engines_within_limit
if self.member.engines(:reload).distinct.count(:engine_code) >= self.member.engine_limit
errors.add(:engine, "Exceeded engine limit")
end
end
end
The above doesn't work, specifically this part,
self.member.engines(:reload).distinct.count(:engine_code)
The query it produces is
SELECT "engines".*
FROM "engines"
INNER JOIN "groups"
ON "engines"."group_id" = "groups"."id"
WHERE "groups"."member_id" = $1 [["member_id", 22]]
and returns the count 0 which is wrong
Whereas the following
Engine.distinct.count(:engine_code)
produces the query
SELECT DISTINCT COUNT(DISTINCT "engines"."engine_code")
FROM "engines"
and returns 3 which is correct
What am I doing wrong? It is the same query just with a join?
After doing long chat, we found the below query to work :
self.member
.engines(:reload)
.count("DISTINCT engine_code")
AR:: means ActiveRecord:: below.
The reason for the "wrong" result in the question is that the collection association isn't used correct. A collection association (e.g. has_many) for a record is not a AR::Relation it's a AR::Associations::CollectionProxy. It's a sub class of AR::Relation, and e.g. distinct is overridden.
self.member.engines(:reload).distinct.count(:engine_code) will cause this to happen:
self.member.engines(:reload) is a
AR::Associations::CollectionProxy
.distinct on that will first
fire the db read, then do a .to_a on the result and then doing
"it's own" distinct which is doing a uniq on the array of records
regarding the id of the records.
The result is an array.
.count(:engine_code) this is doing Array#count on the array which is returning
0 since no record in the array equals to the symbol :engine_code.
To get the correct result you should use the relation of the association proxy, .scope:
self.member.engines(:reload).scope.distinct.count(:engine_code)
I think it's a little bit confusing in Rails how collection associations is handled. Many of the "normal" methods for relations works as usual, e.g. this will work without using .scope:
self.member.engines(:reload).where('true').distinct.count(:engine_code)
that is because where isn't overridden by AR::Associations::CollectionProxy.
Perhaps it would be better to always have to use .scope when using the collection as a relation.

Texticle and ActsAsTaggableOn

I'm trying to implement search over tags as part of a Texticle search. Since texticle doesn't search over multiple tables from the same model, I ended up creating a new model called PostSearch, following Texticle's suggestion about System-Wide Searching
class PostSearch < ActiveRecord::Base
# We want to reference various models
belongs_to :searchable, :polymorphic => true
# Wish we could eliminate n + 1 query problems,
# but we can't include polymorphic models when
# using scopes to search in Rails 3
# default_scope :include => :searchable
# Search.new('query') to search for 'query'
# across searchable models
def self.new(query)
debugger
query = query.to_s
return [] if query.empty?
self.search(query).map!(&:searchable)
#self.search(query) <-- this works, not sure why I shouldn't use it.
end
# Search records are never modified
def readonly?; true; end
# Our view doesn't have primary keys, so we need
# to be explicit about how to tell different search
# results apart; without this, we can't use :include
# to avoid n + 1 query problems
def hash
id.hash
end
def eql?(result)
id == result.id
end
end
In my Postgres DB I created a view like this:
CREATE VIEW post_searches AS
SELECT posts.id, posts.name, string_agg(tags.name, ', ') AS tags
FROM posts
LEFT JOIN taggings ON taggings.taggable_id = posts.id
LEFT JOIN tags ON taggings.tag_id = tags.id
GROUP BY posts.id;
This allows me to get posts like this:
SELECT * FROM post_searches
id | name | tags
1 Intro introduction, funny, nice
So it seems like that should all be fine. Unfortunately calling
PostSearch.new("funny") returns [nil] (NOT []). Looking through the Texticle source code, it seems like this line in the PostSearch.new
self.search(query).map!(&:searchable)
maps the fields using some sort of searchable_columns method and does it ?incorrectly? and results in a nil.
On a different note, the tags field doesn't get searched in the texticle SQL query unless I cast it from a text type to a varchar type.
So, in summary:
Why does the object get mapped to nil when it is found?
AND
Why does texticle ignore my tags field unless it is varchar?
Texticle maps objects to nil instead of nothing so that you can check for nil? - it's a safeguard against erroring out checking against non-existent items. It might be worth asking tenderlove himself as to exactly why he did it that way.
I'm not completely positive as to why Texticle ignores non-varchars, but it looks like it's a performance safeguard so that Postgres does not do full table scans (under the section Creating Indexes for Super Speed):
You will need to add an index for every text/string column you query against, or else Postgresql will revert to a full table scan instead of using the indexes.

Rails 3 - Expression-based Attribute in Model

How do I define a model attribute as an expression of another attribute?
Example:
Class Home < ActiveRecord::Base
attr_accessible :address, :phone_number
Now I want to be able to return an attribute like :area_code, which would be an sql expression like "substr(phone_number, 1,3)".
I also want to be able to use the expression / attribute in a group by query for a report.
This seems to perform the query, but does not return an object with named attributes, so how do I use it in a view?
Rails Console:
#ac = Home.group("substr(phone_number, 1,3)").count
=> #<OrderedHash {"307"=>3, "515"=>1}>
I also expected this to work, but not sure what kind of object it is returning:
#test = Home.select("substr(phone_number, 1,3) as area_code, count(*) as c").group("substr(phone_number, 1,3)")
=> [#<Home>, #<Home>]
To expand on the last example. Here it is with Active Record logging turned on:
>Home.select("substr(phone_number, 1,3) as area_code, count(*) as c").group("substr(phone_number, 1,3)")
Output:
Home Load (0.3ms) SELECT substr(phone_number, 1,3) as area_code, count(*) as c FROM "homes" GROUP BY substr(phone_number, 1,3)
=> [#<Home>, #<Home>]
So it is executing the query I want, but giving me an unexpected data object. Shouldn't I get something like this?
[ #<area_code: "307", c: 3>, #<area_code: "515", c: 1> ]
you cannot access to substr(...) because it is not an attribute of the initialized record object.
See : http://guides.rubyonrails.org/active_record_querying.html "selecting specific fields"
you can workaround this this way :
#test = Home.select("substr(phone_number, 1,3) as phone_number").group(:phone_number)
... but some might find it a bit hackish. Moreover, when you use select, the records will be read-only, so be careful.
if you need the count, just add .count at the end of the chain, but you will get a hash as you already had. But isn't that all you need ? what is your purpose ?
You can also use an area_code column that will be filled using callbacks on create and update, so you can index this column ; your query will run fast on read, though it will be slower on insertion.

Resources