Meta_Search: Search on number of associated entries - ruby-on-rails

I have the following Rails models:
class Entry < ActiveRecord::Base
has_and_belongs_to_many :tags, :uniq => true
end
And
class Tag < ActiveRecord::Base
has_and_belongs_to_many :entries, :uniq => true
end
Just so it's clear, an 'entry' can have many 'tags' associated with it.
Using the plugin Meta_Search I would like to be able to perform a search (via a form) that returns 'entries' that have more than 0 tags associated with it.
I've tried several techniques including (named) scopes and methods but I've not been able to achieve this.
Does anyone have an idea on how to perform this?
Thanks.

Something like
Entry.joins(
:tags
).select(
"entries.*, count(tags.id) as tags_count"
).order(
"tags_count DESC"
).group(
"entries.id"
).where(
"tags_count != 0"
)

This is similar to sorting by a count of associated records: Rails meta_search gem: sort by count of an associated model One of those answers recommends using counter_cache, but a comment suggests it won't work for HABTM.
Use a named scope that can select the records you are interested in (a la #mark's answer) and make it a search_method.

Related

Rails HABTM association not clearing using .clear, thoughts?

High guys. This isn't behaving the way I think it should which means I'm doing it wrong;
class Tag < ActiveRecord::Base
has_and_belongs_to_many :properties
end
class Property < ActiveRecord::Base
has_and_belongs_to_many :tags
def amenities
tags.where(:classification => :amenity)
end
end
So I have Properties and Tags. They have a HABTM relationship with a pivot table.
When I do a .tags on a property, I get the full list and if I do a .clear on that full list it correctly removes the associations from the database.
When I do a .amenities I get only those tags that are flagged with the classification of amenity correctly, but if I do a .clear on those results it fails to remove them but rather just does the .amenities query again in the console with an output of [].
So this means it's just .clear'ing the result array.. not the association which is what I actually want.
So the question then is; what is the correct way to .clear an association from a HABTM relationship while giving it essentially a where clause to limit which associations are being removed?
Thanks guys. Hope that wasn't too confusing..
Instead of defining a method querying tags, you could add another tag association with conditions, like:
class Property < ActiveRecord::Base
has_and_belongs_to_many :tags
# this will be just like the tags association, except narrow the results
# to only tags with the classification of 'amenity'
has_and_belongs_to_many :amenities,
:class_name => 'Tag',
:conditions => { :classification => 'amenity' }
end
clear, and any other habtm assocation methods, should work as expected.

Rails - combine multiple has_many throughs

I'm just started with Rails, and have a problem I can't solve myself:
User.rb:
has_many :bid_listings, through: :bids, source: :listing, uniq: true
has_many :offer_listings, through: :offers, source: :listing, uniq: true
Both of these return listings, and using methods/scopes from Listing model individually works perfectly. However, when I'm trying to combine these, I'm getting an Array, where i can't apply Listing model's methods and scopes.
I've tried multiple ways, but stuck. Please help.
P.S. User has many bids, User has many offers, bid belongs to listing, offer belongs to listing
You're calling an instance method on an Array object, as opposed to on an ActiveRecord object. Therefore, object of type Array has no idea what the search method is. Try this out:
Edit
user = User.first
listings = Listing.joins(:bids).joins(:offers).where(:bids => {:user_id => user.id}, :offers => {:user_id => user.id})
listings.search('a')
I had a similar problem, and the best solution I came up with was something like:
def buying_listings
Listing.find_by_sql(bid_listings.union(offer_listings).to_sql)
end
This way should still allow you continue scoping, but is less efficient as it will execute an extra query.

Rails app using STI -- easiest way to pull these records?

I'm learning my way around Rails and am working on a sample app to keep track of beer recipes.
I have a model called Recipe which holds the recipe name and efficiency.
I have a model called Ingredient which is using STI - this is subclassed into Malt, Hop, and Yeast.
Finally, to link the recipes and ingredients, I am using a join table called rec_items which holds the recipe_id, ingredient_id, and info particular to that recipe/ingredient combo, such as amount and boil time.
Everything seems to be working well - I can find all my malts by using Malt.all, and all ingredients by using Ingredient.all. I can find a recipe's ingredients using #recipe.ingredients, etc...
However, I'm working on my recipe's show view now, and am confused as to the best way to accomplish the below:
I want to display the recipe name and associated info, and then list the ingredients, but separated by ingredient type. So, if I have a Black IPA # 85% efficiency and it has 5 malts and 3 hops varieties, the output would be similar to:
BLACK IPA (85%)
Ingredient List
MALTS:
malt 1
malt 2
...
HOPS:
hop 1
...
Now, I can pull #recipe.rec_items and iterate through them, testing each rec_item.ingredient for type == "Malt", then do the same for the hops, but that doesn't seem very Rails-y nor efficient. So what is the best way to do this? I can use #recipe.ingredients.all to pull all the ingredients, but can't use #recipe.malts.all or #recipe.hops.all to pull just those types.
Is there a different syntax I should be using? Should I using #recipe.ingredient.find_by_type("Malt")? Doing this in the controller and passing the collection to the view, or doing it right in the view? Do I need to specify the has_many relationship in my Hop and Malt models as well?
I can get it working the way I want using conditional statements or find_by_type, but my emphasis is on doing this "the Rails way" with as little DB overhead as possible.
Thanks for the help!
Current bare-bones code:
Recipe.rb
class Recipe < ActiveRecord::Base
has_many :rec_items
has_many :ingredients, :through => :rec_items
end
Ingredient.rb
class Ingredient < ActiveRecord::Base
has_many :rec_items
has_many :recipes, :through => :rec_items
end
Malt.rb
class Malt < Ingredient
end
Hop.rb
class Hop < Ingredient
end
RecItem.rb
class RecItem < ActiveRecord::Base
belongs_to :recipe
belongs_to :ingredient
end
recipes_controller.rb
class RecipesController < ApplicationController
def show
#recipe = Recipe.find(params[:id])
end
def index
#recipes = Recipe.all
end
end
Updated to add
I'm now unable to access the join table attributes, so I posted a new question:
Rails - using group_by and has_many :through and trying to access join table attributes
If anyone can help with that, I'd appreciate it!!
It's been a while since I've used STI, having been burned a time or two. So I may be skipping over some STI-fu that would make this easier. That said...
There are many ways of doing this. First, you could make a scope for each of malt, hops, and yeast.
class Ingredient < ActiveRecord::Base
has_many :rec_items
has_many :recipes, :through => :rec_items
named_scope :malt, :conditions => {:type => 'Malt'}
named_scope :hops, :conditions => {:type => 'Hops'}
...
end
This will allow you to do something line:
malts = #recipe.ingredients.malt
hops = #recipe.ingedients.hops
While this is convenient, it isn't the most efficient thing to do, database-wise. We'd have to do three queries to get all three types.
So if we're not talking a ton of ingredients per recipe, it'll probably be better to just pull in all #recipe.ingredients, then group them with something like:
ingredients = #recipe.ingredients.group_by(&:type)
This will perform one query and then group them into a hash in ruby memory. The hash will be keyed off of type and look something like:
{"Malt" => [first_malt, second_malt],
"Hops" => [first_hops],
"Yeast" => [etc]
}
You can then refer to that collection to display the items however you wish.
ingredients["Malt"].each {|malt| malt.foo }
You can use group_by here.
recipe.ingredients.group_by {|i| i.type}.each do |type, ingredients|
puts type
ingredients.each do |ingredient|
puts ingredient.inspect
end
end
The utility of STI in this instance is dubious. You might be better off with a straight-forward categorization:
class Ingredient < ActiveRecord::Base
belongs_to :ingredient_type
has_many :rec_items
has_many :recipes, :through => :rec_items
end
The IngredientType defines your various types and ends up being a numerical constant from that point forward.
When you're trying to display a list this becomes easier. I usually prefer to pull out the intermediate records directly, then join out as required:
RecItem.sort('recipe_id, ingredient_type_id').includes(:recipe, :ingredient).all
Something like that gives you the flexibility to sort and group as required. You can adjust the sort conditions to get the right ordering. This might also work with STI if you sort on the type column.

In Rails 3 how can I select items where the items.join_model.id != x?

In my Rails models I have:
class Song < ActiveRecord::Base
has_many :flags
has_many :accounts, :through => :flags
end
class Account < ActiveRecord::Base
has_many :flags
has_many :songs, :through => :flags
end
class Flag < ActiveRecord::Base
belongs_to :song
belongs_to :account
end
I'm looking for a way to create a scope in the Song model that fetches songs that DO NOT have a given account associated with it.
I've tried:
Song.joins(:accounts).where('account_id != ?', #an_account)
but it returns an empty set. This might be because there are songs that have no accounts attached to it? I'm not sure, but really struggling with this one.
Update
The result set I'm looking for includes songs that do not have a given account associated with it. This includes songs that have no flags.
Thanks for looking.
Am I understanding your question correctly - you want Songs that are not associated with a particular account?
Try:
Song.joins(:accounts).where(Account.arel_table[:id].not_eq(#an_account.id))
Answer revised: (in response to clarification in the comments)
You probably want SQL conditions like this:
Song.all(:conditions =>
["songs.id NOT IN (SELECT f.song_id FROM flags f WHERE f.account_id = ?)", #an_account.id]
)
Or in ARel, you could get the same SQL generated like this:
songs = Song.arel_table
flags = Flag.arel_table
Song.where(songs[:id].not_in(
flags.project(:song_id).where(flags[:account_id].eq(#an_account.id))
))
I generally prefer ARel, and I prefer it in this case too.
If your where clause is not a typo, it is incorrect. Code frequently uses == for equality, but sql does not, use a single equals sign as such:
Song.joins(:accounts).where('account_id = ?', #an_account.id)
Edit:
Actually there is a way to use activerecord to do this for you, instead of writing your own bound sql fragments:
Song.joins(:accounts).where(:accounts => {:id => #an_account.id})

Eager-loading association count with Arel (Rails 3)

Simple task: given that an article has many comments, be able to display in a long list of articles how many comments each article has. I'm trying to work out how to preload this data with Arel.
The "Complex Aggregations" section of the README file seems to discuss that type of situation, but it doesn't exactly offer sample code, nor does it offer a way to do it in two queries instead of one joined query, which is worse for performance.
Given the following:
class Article
has_many :comments
end
class Comment
belongs_to :article
end
How can I preload for an article set how many comments each has?
Can't you use counter cache for this?
belongs_to :article, :counter_cache => true
You also need to have a migration that adds the column comments_count
You can do something nasty using SQL like:
default_scope :select => 'articles.*, (select count(comments.id) from comments where comments.article_id = articles.id) as count_comments'
and then you would have access to Article.first.count_comments.
Another (nicer) method to do it is to use the 'counter_cache' feature/option from belongs_to association.

Resources