Create scope with .where on both collection and grand-parent - ruby-on-rails

I have three models with grand-parent, parent, child relation: Organization, Category, Post.
I'm trying to create a scope in my Post model, using where first on the passed collection and then on the grand-parent:
scope :ready, -> {
where("next_setup_at < ?", DateTime.current)
.joins(category: :organization)
.where("organizations.active = ?", true)
}
But Postgres is throwing me an error:
ActiveRecord::StatementInvalid: PG::AmbiguousColumn: ERROR: column reference "next_setup_at" is ambiguous
LINE 1: ...zations"."id" = "categories"."organization_id" WHERE (next_setup...
^
: SELECT "posts".* FROM "posts" INNER JOIN "categories" ON "categories"."id" = "posts"."category_id" INNER JOIN "organizations" ON "organizations"."id" = "categories"."organization_id" WHERE (next_setup_at < '2016-03-22 15:57:19.971887') AND (organizations.active = 't')

Take a look at your .where clauses. The second one does a good job at defining what column to query.
where("organizations.active = ?", true)
The first one doesn't.
where("next_setup_at < ?", DateTime.current)
You have to define what table the next_setup_at column references to. Leading to
where("posts.next_setup_at < ?", DateTime.current)
Further imporvement
You can easily specify what table to reference in pure ActiveRecord like so:
where(posts: {next_setup_at: DateTime.current}, categories: {active: true})

Related

How do I count the number of records that have one or more associated object?

I have a Property model that has_many :photos. I want to count the number of properties that have one or more photo.
How do I do that?
I have tried the simple:
> Property.where('properties.photos.count > ?', 0).count
(3.1ms) SELECT COUNT(*) FROM "properties" WHERE (properties.photos.count > 1)
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: missing FROM-clause entry for table "photos"
LINE 1: SELECT COUNT(*) FROM "properties" WHERE (properties.photos....
^
: SELECT COUNT(*) FROM "properties" WHERE (properties.photos.count > 0)
from /ruby-2.3.0#myproject/gems/activerecord-3.2.22.5/lib/active_record/connection_adapters/postgresql_adapter.rb:1163:in `async_exec'
Caused by PG::UndefinedTable: ERROR: missing FROM-clause entry for table "photos"
LINE 1: SELECT COUNT(*) FROM "properties" WHERE (properties.photos....
to:
> Property.joins(:photos).where('photos.count > ?', 0).count
(3.7ms) SELECT COUNT(*) FROM "properties" INNER JOIN "photos" ON "photos"."property_id" = "properties"."id" WHERE (photos.count > 0)
ActiveRecord::StatementInvalid: PG::GroupingError: ERROR: aggregate functions are not allowed in WHERE
LINE 1: ..."photos"."property_id" = "properties"."id" WHERE (photos.cou...
^
: SELECT COUNT(*) FROM "properties" INNER JOIN "photos" ON "photos"."property_id" = "properties"."id" WHERE (photos.count > 0)
from ruby-2.3.0#myproject/gems/activerecord-3.2.22.5/lib/active_record/connection_adapters/postgresql_adapter.rb:1163:in `async_exec'
Caused by PG::GroupingError: ERROR: aggregate functions are not allowed in WHERE
LINE 1: ..."photos"."property_id" = "properties"."id" WHERE (photos.cou...
to the more advanced:
>Property.includes(:photos).group(['property.id', 'photos.id']).order('COUNT(photos.id) DESC').count
(0.6ms) SELECT COUNT(DISTINCT "properties"."id") AS count_id, property.id AS property_id, photos.id AS photos_id FROM "properties" LEFT OUTER JOIN "photos" ON "photos"."property_id" = "properties"."id" GROUP BY property.id, photos.id ORDER BY COUNT(photos.id) DESC
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: missing FROM-clause entry for table "property"
LINE 1: ...CT COUNT(DISTINCT "properties"."id") AS count_id, property.i...
^
: SELECT COUNT(DISTINCT "properties"."id") AS count_id, property.id AS property_id, photos.id AS photos_id FROM "properties" LEFT OUTER JOIN "photos" ON "photos"."property_id" = "properties"."id" GROUP BY property.id, photos.id ORDER BY COUNT(photos.id) DESC
from ruby-2.3.0#myproject/gems/activerecord-3.2.22.5/lib/active_record/connection_adapters/postgresql_adapter.rb:1163:in `async_exec'
Caused by PG::UndefinedTable: ERROR: missing FROM-clause entry for table "property"
LINE 1: ...CT COUNT(DISTINCT "properties"."id") AS count_id, property.i...
and a few other variations, and they all produce similar errors.
What am I doing wrong?
Note: All I want is the count of properties that have photos.count > 0. I don't want a hash of all the properties and the count of photos. In other words, if there are 5000 properties in my db, I want to build a scope that returns just the properties that actually have photos.
Since all you want is the Propertys with Photos then an INNER JOIN is all you need.
Property.joins(:photos)
That is it. If you want a scope then
class Property < ActiveRecord::Base
scope :with_photos, -> {joins(:photos)}
end
To get the count using rails 3.2
Property.with_photos.count(distinct: true)
You could also use: in rails 3.2
Property.count(joins: :photos, distinct: true)
ActiveRecord::Calculations#count Doc
This will execute
SELECT
COUNT(DISTINCT properties.id)
FROM
properties
INNER JOIN photos ON photos.property_id = properties.id
EDIT:
Property.joins(:photos).group('photos.property_id').having('count(photos.property_id) > 1').count
#=> {1234=>2} # 1234 is property id 2 is count of photos
You will get the property_ids with the number of associated photos with it.
Old Answer:
You can get the properties with atleast one photos associated with it
Property.includes(:photos).where.not(photos: { property_id: nil })
As you are using rails 3.2 .not will not work you have to use
Property.includes(:photos).where("property_id IS NOT null")
Property.includes(:photos).where("SELECT count(photos.id) > 0 FROM photos WHERE property_id = properties.id")
As a scope:
scope :with_photos, -> { where("SELECT count(photos.id) > 0 FROM photos WHERE property_id = properties.id") }
You can try like this, I have done in my projects,
Photo.group(:property_id).count
You will get property id with photos count
results = { 3314=>3, 2033=>3, 3532=>2, 3565=>6, 3510=>1, 3022=>7, 648=>2, 570=>3, 4678=>3, 3540=>1, 3489=>4, 536=>1, 1715=>4 }
Give this a go:
class Property < ApplicationRecord
has_many :photos
def self.with_photos
self.all.reject { |p| p.photos.empty? }
end
end
Property.with_photos.count
Source
More Efficient (Rails 4+):
Property.joins(:photos).uniq.count
Source
More Efficient (Rails 5.1+):
Property.joins(:photos).distinct.count
Source
According to your requirement you can try this
1) A simple count of the number of properties that have 1 or more
photo
To just get the number of properties which have one or more photo you can do this
Property.joins(:photos).distinct.count
As we are not using group the distinct or uniq is necessary. distinct will return ActiveRecord_Relation and uniq will return Array.
2) I would like that set of properties returned so I can
create a scope of just those properties. Also, I do have lots of
properties with more than 1 photo.
To get all the property objects which have one or more than one photo you can use the same query:
Property.joins(:photos).distinct
or you can use the group_by clause:
Property.joins(:photos).group('properties.id')
The difference will be that when you will use size method on the group query it will return a hash with the property_id as key and number of photos on the property as value.
Performance Update:
If you always require the count of associated object and you want to fetch it efficiently you may use counter_cache like this:
class Photo < ApplicationRecord
belongs_to :property, counter_cache: true
end
class Property < ApplicationRecord
has_many :photos
end
And then you will have to add a column in the properties table named photos_count and Rails will automatically update the column each time a new photo record is added or a exisisting photo record is removed. And then you can also query directly:
Property.where('photos_count > ?', 1)
This is optional and you can do this if you are facing performance issues with the data fetch.

Joins Great Grandparent Model - Ruby On Rails

I'm sure this is very simple but I cannot get it to work. I have the following associations.
model Category
has_many :category_brands
end
model CategoryBrand
has_many :category_models
belongs_to :category
end
model CategoryModel
has_many :products
belongs_to :category_brand
end
model Product
belongs_to :category_model
end
In theory, I want to query all D records that have an A record with the name equal to "x". So like this:
#products = Product.joins(category_model: {category_brand: :category}).where("category.name like ?", "%Incline Motors%")
But I cannot get this to work. Any help would be appreciated.
Current Error:
G::UndefinedTable: ERROR: missing FROM-clause entry for table "category" LINE 1: ...es"."id" = "category_brands"."category_id" WHERE (category.n... ^ : SELECT COUNT(*) FROM "products" INNER JOIN "category_models" ON "category_models"."id" = "products"."category_model_id" INNER JOIN "category_brands" ON "category_brands"."id" = "category_models"."category_brand_id" INNER JOIN "categories" ON "categories"."id" = "category_brands"."category_id" WHERE (category.name like '%Incline Motors%')
The table name should be pluralised -- note the SQL statement text INNER JOIN "categories"
#products = Product.joins(category_model: {category_brand: :category}).where("categories.name like ?", "%Incline Motors%")

rails get a list of records by checking if association is approved

I've got a organization model and an organization_profile model. The organization_profile model has an approved column. I would like to be able to find all approved users by calling something like: Organization.approved. I found that the best way to handle this is probably through scope. However I can't seem to get it to work. This is what i am trying
scope :approved, -> {joins(:organization_profile).where('organization_profile.approved = ?', true) }
But then Organization.approved gives me all kinds of errors:
Organization Load (8.0ms) SELECT "organizations".* FROM "organizations" INNER JOIN "organization_profiles" ON "organization_profiles"."user_id" = "organizations"."id" WHERE (organization_profile.approved = 't')
PG::UndefinedTable: ERROR: missing FROM-clause entry for table "organization_profile"
LINE 1: ...profiles"."user_id" = "organizations"."id" WHERE (organizati...
^
: SELECT "organizations".* FROM "organizations" INNER JOIN "organization_profiles" ON "organization_profiles"."user_id" = "organizations"."id" WHERE (organization_profile.approved = 't')
Can anyone tell me the correct code?
Your query is using organization_profile (singular) but your table name is organization_profiles (plural).
A slightly better way to do this (which also avoids using strings), is to turn the where clause into an Arel predicate (might not be the right word):
scope :approved, -> { joins(:organization_profile).where(OrganizationProfile.arel_table['approved'].eq(true)) }
The condition is SQL code that should reflect correctly the name of your tables which are always plural :
where('organization_profiles.approved = ?', true)

retrieve reverse multiple records of rails association

I have two models product and category.
I am able to make successful queries like Category.products etc.
Product.rb
belongs_to :category
Category.rb
has_many :products
Now I want to retrieve only those categories that has at least one existing product.
I tried like this :
#categories = Category.where(Category.products.present?)
# returned error undefined method `products' also changing to product didn't work.
Getting your comment that you need Categories with products and that the product property with_operator to be true, you can do that query in "rails style" using joins and merge:
#categories = Category.joins(:products).merge(Product.where(with_operator: true)).uniq
Which will generate the following SQL:
SELECT DISTINCT "categories".* FROM "categories" INNER JOIN "products" ON "products"."category_id" = "categories"."id" WHERE "products"."with_operator" = 't'
You could also use the rails 4 syntax, as pointed by #yukke:
Category.joins(:products).where(products: { with_operator: true }).uniq
All you need is inner join. It will skip those categories, that has no products. And to add a condition on joined table you can use rails 4 where's syntax:
#categories = Category.joins(:products).where(products: { with_operator: true }).uniq
It will produce next sql query:
SELECT DISTINCT "categories".*
FROM "categories" INNER JOIN "products" ON "products"."category_id" = "categories"."id"
WHERE "products"."with_operator" = 't'

PG::Error: ERROR: column reference "status" is ambiguous in active_admin

Using rails 3.2 with active_admin and seeing PG::Error: ERROR: column reference "status" is ambiguous when using a custom filter on active_admin in Rents.rb:
filter :travel_car_brand, as: :string
filter :travel_car_model, as: :string
The error points to:
: SELECT COUNT(DISTINCT "rents"."id") FROM "rents" LEFT OUTER JOIN "travels" ON "travels"."id" = "rents"."travel_id" LEFT OUTER JOIN "cars" ON "cars"."travel_id" = "travels"."id" WHERE ("cars"."brand" ILIKE '%mazda%') AND ("startDate" > '2014-08-04 10:15:14 +0200' and status = 'paid'):
it's interesting that the above has status = 'paid' since I'm not sure why its using that as a filter.
models
Rent.rb
belongs_to :travel
Travel.rb
has_one :car
and both rents table and travels table have a status attribute.
I've seen Lucas' answer but if this is a rails app, the SQL should be generated by the application, not hardcoded. Therefore changing the SQL directly is not the solution.
Instead, I would suggest you find the code that is adding the "paid" filter and modify it to declare the relevant model name.
Somewhere you probably have a scope:
scope :paid, where(status: 'paid')
change that to (for example):
scope :paid, where("model.status = 'paid'")
You need to chose wich table you want your attribute to select, or use both
e.g.
SELECT
COUNT(DISTINCT "rents"."id")
FROM "rents"
LEFT OUTER JOIN "travels" ON "travels"."id" = "rents"."travel_id"
LEFT OUTER JOIN "cars" ON "cars"."travel_id" = "travels"."id"
WHERE ("cars"."brand" ILIKE '%mazda%')
AND ("startDate" > '2014-08-04 10:15:14 +0200')
AND rents.status = 'paid'
or if you require both:
SELECT
COUNT(DISTINCT "rents"."id")
FROM "rents"
LEFT OUTER JOIN "travels" ON "travels"."id" = "rents"."travel_id"
LEFT OUTER JOIN "cars" ON "cars"."travel_id" = "travels"."id"
WHERE ("cars"."brand" ILIKE '%mazda%')
AND ("startDate" > '2014-08-04 10:15:14 +0200')
AND rents.status = 'paid'
AND travels.status = 'paid'
Your "status" column is ambiguous. Because SQL can't understand which one table's column you want. Rent.status or Travels.status that SQL can not understand.

Resources