Writing a named scope in rails - ruby-on-rails-3

I have three models: Products, Placements, Collections
I'm trying to write a name scope that only chooses products NOT in a certain collection.
products has_many :collections, :through => :placements
collections has_many :products, :through => :placements
I got about this far:
scope :not_in_front, joins(:collections).where('collections.id IS NOT ?', 4)
But that generated the opposite of what I expected in the query:
Product Load (0.3ms) SELECT "products".* FROM "products" INNER JOIN "placements" ON "products"."id" = "placements"."product_id" WHERE "placements"."collection_id" = 4
Any idea how to write this to only select the products not in that particular collection?

Instead of collections.id IS NOT 4 try collections.id != 4

The named scope was getting too ugly, so I went with this. Not sure it's the best way, but, it works...
def self.not_on_top_shelf
top_shelf = Collection.find_by_handle('top-shelf')
products = Product.find(:all, :order => "factor_score DESC")
not_on_top_shelf = products.map {|p| p unless p.collections.include?(top_shelf)}
not_on_top_shelf.compact #some products may not be in a collection
end

Related

Rails multiple joins condition query for exact tags with has many through relationships

I have a problem with filtering products by exact tags as my current query does not return exact matches and I can't seem to get the right query conditions.
Example
Area = ["small","big"] ,Surface = ["smooth","rough"]
Product A has only ["small","smooth","rough"] as tags
If I filter products using ["small","big","smooth","rough"] as the tags, I get product A in my search results but ideally, it should not return any search results.
I have three models, Product,Area and Surface. Area & Surface are linked to Product by a has_many through relationship.
class Product < ActiveRecord::Base
has_many :product_areas
has_many :areas, :through => :product_areas
has_many :product_surfaces
has_many :surfaces, :through => :product_surfaces
class Area < ActiveRecord::Base
#Surface model looks exactly the same as Area model
has_many :product_areas,dependent: :destroy
has_many :products, :through => :product_areas
My Query
area_ids = params[:area_ids]
surface_ids = params[:surface_ids]
#products = Product.where(nil)
#products = #products.joins(:areas).where('areas.id' => area_ids).group('products.id').having("count(areas.id) >= ?",area_ids.count) unless area_ids.blank?
#products = #products.joins(:surfaces).where('surfaces.id' => surface_ids).group('products.id').having("count(surfaces.id) >= ?",surface_ids.count) unless surface_ids.blank?
I solved this problem just now with this solution.
First I used the names of the models for Area & Surface as their unique identifer as they can have conflicting ids and added them to an array.
Next I looped through the products and created an array of the name identifers and compared the two arrays to check if they intersect. Intersection would mean that the search filters were a correct match and we add the product ID to a third array which stores all the product_ids before doing a query to get the products with those product ids.
#area = Area.all
area_ids = params[:area_ids]
#uniq_names = #area.where(id: area_ids).collect { |m| m.name }
#products.each do |product|
#names = product.areas.map { |m| m.name }
# if intersect, then we add them to filtered product
if (#uniq_names - #names).empty?
product_ids << product.id
end
end
#products = Product.where(id: product_ids)

Query a 3-way relationship in Active Record

I'm trying to figure out how to query this relationship without using find_by_sql
class User < ActiveRecord::Base
has_many :lists
end
class List < ActiveRecord::Base
has_many :list_items
belongs_to :user
end
class ListItem < ActiveRecord::Base
belongs_to :list
belongs_to :item
end
class Item < ActiveRecord::Base
has_many :list_items
end
this should be what we are using but How would I do this not by find_by_sql
in user.rb
def self.find_users_who_like_by_item_id item_id
find_by_sql(["select u.* from users u, lists l, list_items li where l.list_type_id=10 and li.item_id=? and l.user_id=u.id and li.list_id=l.id", item_id])
end
I've tried several different includes / joins / merge scenarios but am not able to get at what I'm trying to do.
thx
It's a bit difficult to tell exactly what query you're trying to do here, but it looks like you want the user records where the user has a list with a particular list_type_id and containing a particular item. That would look approximately like this:
User.joins(:lists => [:list_items]).where('lists.list_type_id = ? and list_items.item_id = ?', list_type_id, item_id)
This causes ActiveRecord to execute a query like the following:
SELECT "users".* FROM "users" INNER JOIN "lists" ON "lists"."user_id" = "users"."id" INNER JOIN "list_items" ON "list_items"."list_id" = "lists"."id" WHERE (lists.list_type_id = 10 and list_items.item_id = 6)
and return the resulting collection of User objects.

Can I push this rails calculation into the database?

I'm trying to increase my app's efficiency by doing work in the database rather than in the app layer, and I'm wondering if I can move this calculation into the database.
Models:
class Offer < ActiveRecord::Base
has_many :lines
has_many :items, :through => :lines
end
class Line < ActiveRecord::Base
belongs_to :offer
belongs_to :item
# also has a 'quantity' attribute (integer)
end
class Item < ActiveRecord::Base
has_many :lines
has_many :offers, :through => :lines
# also has a 'price' attribute (decimal)
end
What I want to do is calculate the price of an offer. Currently I have a price method in the Offer class:
def price
self.lines.inject(0) do |total, line|
total + line.quantity * line.item.price
end
end
I suspect it may be possible to do a Offer.sum calculation instead that would get the answer directly from the DB rather than looping through the records, but the Calculations section of the ActiveRecord query guide doesn't have enough detail to help me out. Anybody?
Thanks!
You're correct that you can do this with sum. Something like this:
class Offer < ActiveRecord::Base
# ...
def price
self.lines.sum 'lines.quantity * items.price', :joins => :item
end
end
When you call e.g. Offer.find( some_id ).price the above will construct a query something like this:
SELECT SUM( lines.quantity * items.price ) AS total
FROM lines
INNER JOIN items ON items.id = lines.item_id
WHERE lines.offer_id = <some_id>
;
Sometimes you're better off with SQL.
SELECT SUM( lines.quantity * items.price ) AS total
FROM offers
INNER JOIN lines ON offers.id = lines.offer_id
INNER JOIN items ON items.id = lines.item_id
WHERE offers.id = 1
;

Problems with :uniq => true/Distinct option in a has_many_through association w/ named scope (Rails)

See updates at bottom of question.
I had to make some tweaks to my app to add new functionality, and my changes seem to have broken the :uniq option that was previously working perfectly.
Here's the set up:
#User.rb
has_many :products, :through => :seasons, :uniq => true
has_many :varieties, :through => :seasons, :uniq => true
has_many :seasons
#product.rb
has_many :seasons
has_many :users, :through => :seasons, :uniq => true
has_many :varieties
#season.rb
belongs_to :product
belongs_to :variety
belongs_to :user
named_scope :by_product_name, :joins => :product, :order => 'products.name'
#variety.rb
belongs_to :product
has_many :seasons
has_many :users, :through => :seasons, :uniq => true
First I want to show you the previous version of the view that is now breaking, so that we have a baseline to compare. The view below is pulling up products and varieties that belong to the user. In both versions below, I've assigned the same products/varieties to the user so the logs will looking at the exact same use case.
#user/show
<% #user.products.each do |product| %>
<%= link_to product.name, product %>
<% #user.varieties.find_all_by_product_id(product.id).each do |variety| %>
<%=h variety.name.capitalize %></p>
<% end %>
<% end %>
This works. It displays only one of each product, and then displays each product's varieties. In the log below, product ID 1 has 3 associated varieties. And product ID 43 has none.
Here's the log output for the code above:
Product Load (11.3ms) SELECT DISTINCT `products`.* FROM `products` INNER JOIN `seasons` ON `products`.id = `seasons`.product_id WHERE ((`seasons`.user_id = 1)) ORDER BY name, products.name
Product Columns (1.8ms) SHOW FIELDS FROM `products`
Variety Columns (1.9ms) SHOW FIELDS FROM `varieties`
Variety Load (0.7ms) SELECT DISTINCT `varieties`.* FROM `varieties` INNER JOIN `seasons` ON `varieties`.id = `seasons`.variety_id WHERE (`varieties`.`product_id` = 1) AND ((`seasons`.user_id = 1)) ORDER BY name
Variety Load (0.5ms) SELECT DISTINCT `varieties`.* FROM `varieties` INNER JOIN `seasons` ON `varieties`.id = `seasons`.variety_id WHERE (`varieties`.`product_id` = 43) AND ((`seasons`.user_id = 1)) ORDER BY name
Ok, so everything above is the previous version which was working great. In the new version, I added some columns to the join table called seasons, and made a bunch of custom methods that query those columns. As a result, I made the following changes to the view code that you saw above so that I could access those methods on the seasons model:
<% #user.seasons.by_product_name.each do |season| %>
<%= link_to season.product.name, season.product %>
#Note: I couldn't get this loop to work at all, so I settled for the following:
#<% #user.varieties.find_all_by_product_id(product.id).each do |variety| %>
<%=h season.variety.name.capitalize %>
<%end%>
<%end%>
Here's the log output for that:
SQL (0.9ms) SELECT count(DISTINCT "products".id) AS count_products_id FROM "products" INNER JOIN "seasons" ON "products".id = "seasons".product_id WHERE (("seasons".user_id = 1))
Season Load (1.8ms) SELECT "seasons".* FROM "seasons" INNER JOIN "products" ON "products".id = "seasons".product_id WHERE ("seasons".user_id = 1) AND ("seasons".user_id = 1) ORDER BY products.name
Product Load (0.7ms) SELECT * FROM "products" WHERE ("products"."id" = 43) ORDER BY products.name
CACHE (0.0ms) SELECT "seasons".* FROM "seasons" INNER JOIN "products" ON "products".id = "seasons".product_id WHERE ("seasons".user_id = 1) AND ("seasons".user_id = 1) ORDER BY products.name
Product Load (0.4ms) SELECT * FROM "products" WHERE ("products"."id" = 1) ORDER BY products.name
Variety Load (0.4ms) SELECT * FROM "varieties" WHERE ("varieties"."id" = 2) ORDER BY name
CACHE (0.0ms) SELECT * FROM "products" WHERE ("products"."id" = 1) ORDER BY products.name
Variety Load (0.4ms) SELECT * FROM "varieties" WHERE ("varieties"."id" = 8) ORDER BY name
CACHE (0.0ms) SELECT * FROM "products" WHERE ("products"."id" = 1) ORDER BY products.name
Variety Load (0.4ms) SELECT * FROM "varieties" WHERE ("varieties"."id" = 7) ORDER BY name
CACHE (0.0ms) SELECT * FROM "products" WHERE ("products"."id" = 43) ORDER BY products.name
CACHE (0.0ms) SELECT count(DISTINCT "products".id) AS count_products_id FROM "products" INNER JOIN "seasons" ON "products".id = "seasons".product_id WHERE (("seasons".user_id = 1))
CACHE (0.0ms) SELECT "seasons".* FROM "seasons" INNER JOIN "products" ON "products".id = "seasons".product_id WHERE ("seasons".user_id = 1) AND ("seasons".user_id = 1) ORDER BY products.name
CACHE (0.0ms) SELECT * FROM "products" WHERE ("products"."id" = 1) ORDER BY products.name
CACHE (0.0ms) SELECT * FROM "products" WHERE ("products"."id" = 1) ORDER BY products.name
CACHE (0.0ms) SELECT * FROM "varieties" WHERE ("varieties"."id" = 8) ORDER BY name
I'm having two problems:
(1) The :uniq option is not working for products. Three distinct versions of the same product are displaying on the page.
(2) The :uniq option is not working for varieties. I don't have validation set up on this yet, and if the user enters the same variety twice, it does appear on the page. In the previous working version, this was not the case.
The result I need is that only one product for any given ID displays, and all varieties associated with that ID display along with such unique product.
One thing that sticks out to me is the sql call in the most recent log output. It's adding 'count' to the distinct call. I'm not sure why it's doing that or whether it might be an indication of an issue. I found this unresolved lighthouse ticket that seems like it could potentially be related, but I'm not sure if it's the same issue: https://rails.lighthouseapp.com/projects/8994/tickets/2189-count-breaks-sqlite-has_many-through-association-collection-with-named-scope
Update
I think the problem is that the named_scope is being called once for each season. There needs to be something in the named_scope that narrows the returned products by season id.
What's happening right now is:
user = get me user
seasons = get me user's seasons (say, there are 3 seasons for the user)
products = get me the products
products += get me the products
products += get me the products
Give me each of the products
So what's happening is not that uniq is breaking, but rather than there's no delimeter on the named scope. (I think).
I tried the following, but it throws this exception: odd number list for Hash
named_scope :by_product_name, lambda { |seasons| { season_ids = seasons.map { |season| season.id }; :joins => :product, :conditions => { :seasons { :id => season_id } } :order => 'products.name' } }
Ideas?
Update #2
Ok, now I'm thinking maybe it's not the named scoped at all.
In #user/show, I just changed the loop to bypass the named scope:
<% #user.seasons.each do |season| %>
<%= link_to season.product.name, season.product %>
#Note: I couldn't get this loop to work at all, so I settled for the following:
#<% #user.varieties.find_all_by_product_id(product.id).each do |variety| %>
<%=h season.variety.name.capitalize %>
<%end%>
<%end%>
The above doesn't use the named scope, but I'm still getting the same result. In other words, I'm still seeing all instances of each product, instead of just one.
The code above that creates the first loop is the same as my original code that I listed at the top of this question. The difference is that this code is looping through seasons to hit the products, whereas my original code looped through products. This difference is where the problem is hiding, but I don't know how to fix it.
Also, I mentioned in my original question that I couldn't get the varieties loop working either. You can see the line commented in the code directly above. When looping through the seasons, instead of products, when Rails hits that varieties loop, it throws a name error:
undefined local variable or method `product'
Seems like that might be another symptom of the same problem?
Any other ideas?
I believe the issue is the formatting of the lambda. I obviously can't run the SQL, but the following lambda DOES create an apporpriate hash:
lambda { |seasons| season_ids = seasons.map { |season| season.id }; { :joins => :product, :conditions => { :seasons => { :id => season_ids } }, :order => 'products.name' } }
The output of that call with two seasons with ids 1 and 2 is:
{:joins=>:product, :conditions=>{:seasons=>{:id=>[1, 2]}}, :order=>"products.name"}

Can I :select multiple fields (*, foo) without the extra ones being added to my instances (Instance.foo=>bar)

I'm trying to write a named scope that will order my 'Products' class based on the average 'Review' value. The basic model looks like this
Product < ActiveRecord::Base
has_many :reviews
Review < ActiveRecord::Base
belongs_to :product
# integer value
I've defined the following named scope on Product:
named_scope :best_reviews,
:select => "*, AVG(reviews.value) score",
:joins => "INNER JOIN (SELECT * FROM reviews GROUP BY reviews.product_id) reviews ON reviews.product_id = products.id",
:group => "reviews.product_id",
:order => "score desc"
This seems to be working properly, except that it's adding the 'score' value in the select to my Product instances, which causes problems if I try to save them, and makes comparisons return false (#BestProduct != Product.best_reviews.first, becuase Product.best_reviews.first has score=whatever).
Is there a better way to structure the named_scope? Or a way to make Rails ignore the extra field in the select?
I'm not a Rails developer, but I know SQL allows you to sort by a field that is not in the select-list.
Can you do this:
:select => "*",
:joins => "INNER JOIN (SELECT * FROM reviews GROUP BY reviews.product_id) reviews ON reviews.product_id = products.id",
:group => "reviews.product_id",
:order => "AVG(reviews.value) desc"
Wow, so I should really wait before asking questions. Here's one solution (I'd love to hear if there are better approaches):
I moved the score field into the inner join. That makes it available for ordering but doesn't seem to add it to the instance:
named_scope :best_reviews,
:joins => "INNER JOIN (
SELECT *, AVG(value) score FROM reviews GROUP BY reviews.product_id
) reviews ON reviews.product_id = products.id",
:group => "reviews.product_id",
:order => "reviews.score desc"