Rails: Has many through associations -- find with AND condition, not OR condition - sql

I have the following query method in my ActiveRecord model:
def self.tagged_with( string )
array = string.split(',').map{ |s| s.lstrip }
select('distinct photos.*').joins(:tags).where('tags.name' => array )
end
So, this finds all records that have tags taken from a comma separated list and converted into an array.
Currently this matches records with ANY matching tags -- how can I make it work where it matches ALL tags.
IE: if currently if I input: "blue, red" then I get all records tagged with blue OR red.
I want to match all records tagged with blue AND red.
Suggestions?
-- EDIT --
My models are like so:
class Photo < ActiveRecord::Base
...
has_many :taggings, :dependent => :destroy
has_many :tags, :through => :taggings
...
def self.tagged_with( string )
array = string.split(',').map{ |s| s.lstrip }
select('distinct photos.*').joins(:tags).where('tags.name' => array )
end
...
end
class Tag < ActiveRecord::Base
has_many :taggings, :dependent => :destroy
has_many :photos, :through => :taggings
end
class Tagging < ActiveRecord::Base
belongs_to :photo
belongs_to :tag
end
A tag has two attributes: ID and Name (string).

This should work:
def self.tagged_with( string )
array = string.split(',').map{ |s| s.lstrip }
select('distinct photos.*').
joins(:tags).
where('tags.name' => array).
group("photos.id").
having("count(*) = #{array.size}")
end
Above will match photos that have tags red and blue at least. So that means if a photo has red, blue and green tags, that photo would match too.

You could change your select statement to the following:
select('distinct photos.*').joins(:tags).where('tags.name = ?', array.join(' OR '))
Which will properly create the OR string in the where clause.
ian.

LOL the solution for this is not a simple task--I thought through it from a SQL standpoint and it was UGLY. I figured somebody else has to have tried this so I did some searching and found this post that should help you:
HABTM finds with "AND" joins, NOT "OR"

Related

Rails 3: has_many through query

I have these three Active Record models:
class Event < ActiveRecord::Base
has_many :event_categories, inverse_of: :event
has_many :categories, through: :event_categories
end
class EventCategory < ActiveRecord::Base
belongs_to :event
belongs_to :category
end
class Category < ActiveRecord::Base
has_many :event_categories
has_many :events, through: :event_categories
end
I think the relations are good.
If I want to know what Events have a Category, for example id=5.. I do:
Category.find(5).events
But, if I want to know all Events for more than one category, for example:
Category.where(:id => [3,5]).events
It isn't working. Any ideas?
Please note, when you do has_many :events in a model, Active Record defines a method of name events for that class.
When you do Category.find(5).events, you get events associated with one object (i.e. Category.find(5)) , however Category.where(:id => [3,5]) returns an array of Category objects, so you can't use events function on an array, Only way to get events for all search results is iterate over them and access them individually, something like following:
all_events = Category.where(:id => [3,5]).inject([]) {|res,cat| res << cat.events}
Above code will do one query per iteration, to avoid this, we can include events, in the first query itself, like following, which will provide result in only one query:
all_events = Category.includes(:events).where(:id => [3,5]).inject([]) {|res,cat| res << cat.events}

Group By multiple column for sql count statement , in rails environment

I have two Relations (class)
class RecommendedForType < ActiveRecord::Base
attr_accessible :description, :name
has_many :recommended_for_type_restaurants
end
And
class RecommendedForTypeRestaurant < ActiveRecord::Base
attr_accessible :recommended_for_type_id, :restaurant_id, :user_id
belongs_to :restaurant
belongs_to :user
belongs_to :recommended_for_type
def self.get_count(rest_id)
r = RecommendedForTypeRestaurant.where(restaurant_id: rest_id)
#result = r.includes(:recommended_for_type)
.select("recommended_for_type_id, recommended_for_types.name")
.group ("recommended_tor_type_id, recommended_for_types.name")
.count
end
end
if I call
RecommendedForTypeRestaurant.get_count(1) # get stat of restaurant_id: 1
I get
{"dinner"=>1, "fast food"=>1, "lunch"=>3, "romantic dinner"=>1}
My goal is to get both id and name of RecommendTypeFor in the return result as well. currently i can only make it return either id or name. Something like this
{{"id" => 1, "name" => "dinner", "count" => 1}, {"id" =>2, "name" => "lunch", "count" => 3} }
For sure i can do another round of sql once i get id or name to create that hash but i think that not the efficient way. Appreciate any help or suggestion :)
You need to group separately, try the following group.
.group ("recommended_tor_type_id", "recommended_for_types.name").count
I believe that if you remove the .count at the end and change the select to:
.select("count(*), recommended_for_type_id, recommended_for_types.name")
You will get an array of models that will have the attributes you need and the count.
You should be able to test it out in the console by do something like this:
r = RecommendedForTypeRestaurant.where(restaurant_id: rest_id)
#result = r.includes(:recommended_for_type)
.select("recommended_for_type_id, recommended_for_types.name, count(*)")
.group ("recommended_tor_type_id, recommended_for_types.name")
#result.each do |r|
puts r.recommended_for_type_id
puts r.name
puts r.count
end
Hope it helps!

Why are individual SELECT queries running when an all-encompassing SELECT already ran? (Rails/ActiveRecord)

I have the following code (note the includes and the .each):
subscribers = []
mailgroup.mailgroup_members.opted_to_receive_email.includes(:roster_contact, :roster_info).each { |m|
subscribers << { :EmailAddress => m.roster_contact.member_email,
:Name => m.roster_contact.member_name,
:CustomFields => [ { :Key => 'gender',
:Value => m.roster_info.gender.present? ? m.roster_info.gender : 'X'
} ]
} if m.roster_contact.member_email.present?
}
subscribers
Correspondingly, I see the following in my logs (i.e. select * from ROSTER_INFO ... IN (...)):
SELECT `ROSTER_INFO`.* FROM `ROSTER_INFO` WHERE `ROSTER_INFO`.`ID` IN ('1450', '1000', '1111')
Yet immediately after that there are select * from ROSTER_INFO for each ID already specified in the IN list of the previous query:
RosterInfo Load (84.8ms) SELECT `ROSTER_INFO`.* FROM `ROSTER_INFO` WHERE `ROSTER_INFO`.`ID` = '1450' LIMIT 1
RosterInfo Load (59.2ms) SELECT `ROSTER_INFO`.* FROM `ROSTER_INFO` WHERE `ROSTER_INFO`.`ID` = '1000' LIMIT 1
RosterInfo Load (56.8ms) SELECT `ROSTER_INFO`.* FROM `ROSTER_INFO` WHERE `ROSTER_INFO`.`ID` = '1111' LIMIT 1
If select * had already been done on ROSTER_INFO on all IDs of interest (IN (...)), why is another select * being done again for each of the same IDs? Doesn't ActiveRecord already know all the ROSTER_INFO columns for each ID?
(Meanwhile, there are no individual queries for ROSTER_CONTACT, yet if I remove :roster_contact from the includes method, then ROSTER_INFO is not queried again, but ROSTER_CONTACT is.)
RosterInfo model (abridged)
class RosterInfo < ActiveRecord::Base
self.primary_key = 'ID'
end
RosterContact model (abridged)
class RosterContact < ActiveRecord::Base
self.primary_key = 'ID'
has_many :mailgroup_members, foreign_key: 'rosterID'
has_many :mailgroups, through: :mailgroup_members
has_one :roster_info, foreign_key: 'ID' # can use this line
#belongs_to :roster_info, foreign_key: 'ID' # or this with no difference
def member_name # I added this method to this
roster_info.member_name # question only *after* having
end # figured out the problem.
end
RosterWeb model (abridged)
class RosterWeb < ActiveRecord::Base
self.primary_key = 'ID'
end
Mailgroup model (abridged)
class Mailgroup < ActiveRecord::Base
self.primary_key = 'ID'
has_many :mailgroup_members, foreign_key: 'mailCatID'
has_one :mailing_list, foreign_key: :legacy_id
end
MailgroupMember model (abridged)
class MailgroupMember < ActiveRecord::Base
self.primary_key = 'ID'
belongs_to :mailgroup, foreign_key: 'mailCatID'
belongs_to :roster_contact, foreign_key: 'rosterID'
belongs_to :roster_info, foreign_key: 'rosterID'
belongs_to :roster_web, foreign_key: 'rosterID'
scope :opted_to_receive_email, joins(:roster_web).where('ROSTER_WEB.receiveEmail=?', 1)
end
The issue turned out to be related to m.roster_contact.member_name -- unfortunately I made member_name a method of roster_contact that itself (indirectly) queried roster_info.member_name. I resolved this by changing the line
:Name => m.roster_contact.member_name,
to directly query roster_info as follows
:Name => m.roster_info.member_name,
I am sorry for the trouble!
I'm going to stick my neck out and say that this is probably an in-flight optimization by your query engine. The 'IN' is typically used to compare large sets of keys, the most efficient way of resolving three keys (assuming ID is the key) would be to retrieve each row by key, as has happened.
class RosterInfo < ActiveRecord::Base
has_one :roster_contact, foreign_key: 'ID'
end
class RosterContact < ActiveRecord::Base
has_one :roster_info, foreign_key: 'ID'
end
I don't know what is the premise for having bi-directional has_one, but I suspect it will turn out badly. Probably change one of them to belongs_to. Do the same for the other bi-directional has_one associations.
Another thing is that you are using 'ID' for the foreign_key column, where the usual practice is roster_contact_id or whichever class you are referencing.
Edit:
On closer examination, RosterInfo, RosterContact, RosterWeb look like separate tables for what should be a single record since they are all having the same set of mutual has_one associations. This is something that should be addressed on the schema level, but right now you should be able to drop the has_one associations from one of the three models to solve your immediate problem.

Get the most voted with Mongoid and rails (something like count and group by)

I have three documents
class User
include Mongoid::Document
has_many :votos
...
...
end
class Picture
include Mongoid::Document
has_many :votos
belongs_to :user
...
...
end
class Voto
include Mongoid::Document
belongs_to :picture
belongs_to :user
field :value => :type => Integer
...
...
end
In the Voto document, the field value is a number between 1 and 5
So i need to get the most voted pictures of all to show...
How can i achieve this???
Thanks
You can do it by querying also but it will take hell of a time and performance will decrease. The other solution is create a field total_votos in model Picture and whenever a vote is given to a picture add the field value into total_votes
class Picture
include Mongoid::Document
has_many :votos
belongs_to :user
field :total_votes,:type => Integer
...
...
end
class Voto
include Mongoid::Document
belongs_to :picture
belongs_to :user
field :value => :type => Integer
after_create do
picture = self.picture
picture.total_votes += self.value
picture.save
end
...
...
end
and you can find the maximum value just by running the query
Picture.where(:max_votes => Picture.all.max(:total_votes))

Ruby ActiveRecord has_one with conditions

Let's say I have some Items for sale, and I'm keeping track of their Cost historically:
class Cost < ActiveRecord::Base
belongs_to :item
# eg: costs.amount = 123.45; costs.item_id = 1; costs.created_at = 2011-08-11 16:28
end
class Item < ActiveRecord::Base
has_many :costs
# eg: items.id = 1; items.name = Cheese Sandwich
end
This code works, I can pull out all the previous costs for the item I'm selling.
I feel like it should be possible to have a second clause for Item so that I can pull out the current price directly:
class Item < ActiveRecord::Base
has_many :costs
has_one :current_cost, :class_name => :costs, :conditions => 'MAX(created_at)'
end
my_item.current_cost # => <£123.45, 45 minutes ago>
Any ideas how to achieve this?
class Item < ActiveRecord::Base
has_many :costs
def current_cost
self.costs.order("created_at DESC").first
end
end
my_item.current_cost
has_one :current_cost, :class_name => :costs, :order => 'create_at DESC'
You can use the scope:
class Item < ActiveRecord::Base
has_many :costs
scope :current_cost, limit(1).order("created_at DESC")
end
usage:
my_item.current_cost