RoR table join with "where" needed on both tables - sql

I'm having trouble wrapping my head around joining tables. I have a one_to_many relationship between Locations and Listings. Locations have a longitude and latitude, as well as it's primary key: zipcode. Listings have a title and zipcode.
Listing.where("title LIKE ?", "%#{params[:search2]}%")
Location.where(:lat => #min_lat..#max_lat, :lon => #min_lon..#max_lon)
I'd basically Like to combine these two statements so that I can get all Listings within a given range of a zipcode, which is determnied by using a range of longitudes and latitudes. I can't figure out how to do this in Ruby on Rails in a fashion where I don't have to do separate SQL finds and loop through the data in my code to find the correct data.
Edit Updated code:
#results=Location.joins(:listings).where(:listings => ["title LIKE ?", "%#{params[:search2]}%"], :locations => {:zipcode => params[:searchZip]})
SQL output:SELECT "locations".* FROM "locations" INNER JOIN "listings" ON "listings"."location_id" = "locations"."zipcode" WHERE ("locations"."listings" IN ('title LIKE ?', '%fish%')) AND ("locations"."zipcode" = 44012)
I don't know why it is doing "locations"."listings" (which gives an error), or where the IN is coming from.

I suggest using ARel as opposed to writing your own SQL:
Listing.joins(:location).
where(:listings => {:lat => #min_lat..#max_lat, :lon => #min_lon..#max_lon},
:location => ["title LIKE ?", "%#{params[:search2]}%"])

If I am understanding correctly that Location has_many :listings, then this query will return all of the listings in the location range with matching titles:
Listing.joins(:location).where(
'listings.title LIKE ? AND
locations.lat BETWEEN ? AND ? AND
locations.lon BETWEEN ? AND ?',
"%#{params[:search2]}%", #min_lat, #max_lat, #min_lon, #max_lon)

Location.joins(:listings).where(:listings => {:lat => #min_lat..#max_lat, :lon => #min_lon..#max_lon} )

Related

SQL problems when migrating from MySQL to PostgreSQL

I have a Ruby on Rails 2.3.x application that I'm trying to migrate from my own VPS to Heroku, including porting from SQLite (development) and MySQL (production) to Postgres.
This is a typical Rails call I'm using:
spots = Spot.paginate(:all, :include => [:thing, :user, :store, {:thing => :tags}, {:thing => :brand}], :group => :thing_id, :order => order, :conditions => conditions, :page => page, :per_page => per_page)
Question 1: I get a lot of errors like PG::Error: ERROR: column "spots.id" must appear in the GROUP BY clause or be used in an aggregate function. SQLite/MySQL was evidently more forgiving here. Of course I can easily fix these by adding the specified fields to my :group parameter, but I feel I'm messing up my code. Is there a better way?
Question 2: If I throw in all the GROUP BY columns that Postgres is missing I end up with the following statement (only :group has changed):
spots = Spot.paginate(:all, :include => [:thing, :user, :store, {:thing => :tags}, {:thing => :brand}], :group => 'thing_id,things.id,users.id,spots.id', :order => order, :conditions => conditions, :page => page, :per_page => per_page)
This in turn produces the following SQL code:
SELECT * FROM (SELECT DISTINCT ON ("spots".id) "spots".id, spots.created_at AS alias_0 FROM "spots"
LEFT OUTER JOIN "things" ON "things".id = "spots".thing_id
WHERE (spots.recommended_to_user_id = 1 OR spots.user_id IN (1) OR things.is_featured = 't')
GROUP BY thing_id,things.id,users.id,spots.id) AS id_list
ORDER BY id_list.alias_0 DESC LIMIT 16 OFFSET 0;
...which produces the error PG::Error: ERROR: missing FROM-clause entry for table "users". How can I solve this?
Question 1:
...Is there a better way?
Yes. Since PostgreSQL 9.1 the primary key of a table logically covers all columns of a table in the GROUP BY clause. I quote the release notes for version 9.1:
Allow non-GROUP BY columns in the query target list when the primary
key is specified in the GROUP BY clause (Peter Eisentraut)
Question 2:
The following statement ... produces the error
PG::Error: ERROR: missing FROM-clause entry for table "users"
How can I solve this?
First (as always!), I formatted your query to make it easier to understand. The culprit has bold emphasis:
SELECT *
FROM (
SELECT DISTINCT ON (spots.id)
spots.id, spots.created_at AS alias_0
FROM spots
LEFT JOIN things ON things.id = spots.thing_id
WHERE (spots.recommended_to_user_id = 1 OR
spots.user_id IN (1) OR
things.is_featured = 't')
GROUP BY thing_id, things.id, users.id, spots.id
) id_list
ORDER BY id_list.alias_0 DESC
LIMIT 16
OFFSET 0;
It's all obvious now, right?
Well, not all of it. There is a lot more. DISTINCT ON and GROUP BY in the same query for one, which has its uses, but not here. Radically simplify to:
SELECT s.id, s.created_at AS alias_0
FROM spots s
WHERE s.recommended_to_user_id = 1 OR
s.user_id = 1 OR
EXISTS (
SELECT 1 FROM things t
WHERE t.id = s.thing_id
AND t.is_featured = 't')
ORDER BY s.created_at DESC
LIMIT 16;
The EXISTS semi-join avoids the later need to GROUP BY a priori. This should be much faster (besides being correct) - if my assumptions about the missing table definitions hold.
Going the "pure SQL" route opened up a can of worms for me, so I tried keeping the will_paginate gem and tweak the Spot.paginate parameters instead. The :joins parameter turned out to be very helpful.
This is currently working for me:
spots = Spot.paginate(:all, :include => [:thing, {:thing => :tags}, {:thing => :brand}], :joins => [:user, :store, :thing], :group => 'thing_id,things.id,users.id,spots.id', :order => order, :conditions => conditions, :page => page, :per_page => per_page)

Rails 3.0 One-One Association Using associated model in WHERE clause

When I do:
conditions = {:first_name => 'Chris'}
Patient.joins(:user).find(:all, :conditions => conditions)
It Produces (and fails because the first_name is not in the patients table)
SELECT "patients".* FROM "patients" INNER JOIN "users" ON "users"."id" = "patients"."user_id" WHERE "patients"."first_name" = 'Chris'
I need to be able to query the User model's fields also and get back Patient objects. Is this possible?
Try this:
conditions = ['users.first_name = ?', 'Chris']
Patient.joins(:user).find(:all, :conditions => conditions)
Try changing you conditions hash to:
conditions = {'users.first_name' => 'Chris'}
I've used this style in Rails 2.3, and it worked great for me. Cheers!

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"

rails group by multiple columns

i have budgets table with emptype_id and calendar_id actual_head, estimated_head
when i do Budgets.sum(:actual_head ,:group=>"emptype_id,calendar_id") i do not get the result grouped by the above two columns but only by the emptype_id
however when i check the log the sql query is right
SELECT sum(`budgets`.actual_head) AS sum_actual_head, emptype_id,calendar_id AS emptype_id_calendar_id FROM `budgets` GROUP BY emptype_id,calendar_id
has 103 rows
I wanted to iterate through each emptype_id and calendar_id to get a sum of actual_head
and do some calculations on it.
Grouping with multiple columns cannot be supported by rails. You have to use a regular find all:
budgets = Budgets.find(:all,
:select => "emptype_id, calendar_id, sum(budgets.actual_head) AS sum_actual_head",
:group => "emptype_id, calendar_id")
budgets.each { |budget| puts budget.sum_actual_head }
I cheat. Do :group => ["emptype_id,calendar_id"].
Not want you nor I want, but this works at least.
I'm not sure of this, buy try :group => [:emptype_id, :calendar_id]

Rails SQL Query with find

I want this SQL query to be written in rails controller using find:
select id,name from questions
where id not in (select question_id from levels_questions where level_id=15)
How will I do this? I am using Rails framework and MySQL.
Thanks in advance.
Question.find_all_by_sql('select id,name from questions where id not in (select question_id from levels_questions where level_id=15)')
This is admittedly non-ActiveRecord-ish, but I find that complicated queries such as this tend to be LESS clear/efficient when using the AR macros. If you already have the SQL constructed, you might as well use it.
Some suggestions: encapsulate this find call in a method INSIDE the Question class to hide the details from the controller/view, and consider other SQL constructions that may be more efficient (eg, an OUTER JOIN where levels_questions.question_id is null)
Simple way:
ids = LevelsQuestion.all(:select => "question_id",
:conditions => "level_id = 15").collect(&:question_id)
Question.all(:select => "id, name", :conditions => ["id not in (?)", ids])
One shot:
Question.all(:select => "id, name",
:conditions => ["id not in (select question_id from levels_questions where level_id=15)"])
And the rails 3 way:
ids = LevelsQuestion.select(:question_id).where(:level_id => 15).collect(&:question_id)
Question.select(:id, :name).where("id not in (?)", ids)