Find records where association didnt already exists - ruby-on-rails

how to get only records that isn't associated in Ebook model? Simply, i want to offer only ebooks that user didnt already have. I tried much solutions, one of best is:
Magazine.left_joins(:ebooks).where(ebooks: { id: nil })
but i dont know where to specify user_id
model Magazine
has_many :ebooks
has_many :users, :through => :ebook
model Ebook
belongs_to :user
belongs_to :magazine
model User
has_many :ebooks
Im new in rails, sorry for stupid question maybe.
#Hass
thank you, but this generate sql like this:
SELECT "magazines".* FROM "magazines"
INNER JOIN "ebooks" ON "magazines"."id" = "ebooks"."magazine_id"
LEFT OUTER JOIN "ebooks" "ebooks_magazines" ON "ebooks_magazines"."magazine_id" = "magazines"."id"
WHERE "ebooks"."user_id" = 1 AND "ebooks"."id" IS NULL
(WHERE "ebooks"."user_id" = 1 => this returns 0 records, because where filter doesnt find any row in table ebooks)
but i need something like this:
SELECT "magazines".*, ebooks.user_id FROM "magazines"
LEFT JOIN "ebooks" ON "magazines"."id" = "ebooks"."magazine_id" AND ebooks.user_id = 1
LEFT OUTER JOIN "ebooks" "ebooks_magazines" ON "ebooks_magazines"."magazine_id" = "magazines"."id"
WHERE ebooks.user_id IS NULL
this returns rows that im looking for but i dont know how to do this with rais and associations

You want to use a left_outer_join instead.
Magazine.left_outer_joins(:ebooks).where(ebooks: { id: nil })

Related

ActiveRecord - find records that its association count is 0

In my Ruby on Rails app I have the following model:
class SlideGroup < ApplicationRecord
has_many :survey_group_lists, foreign_key: 'group_id'
has_many :surveys, through: :survey_group_lists
end
I want to find all orphaned slide groups. Orphaned slide group is slide group which is not connected to any survey. I've been trying following query but it does not return anything and I'm sure that I have orphaned records in my test database:
SlideGroup.joins(:surveys).group("slide_groups.id, surveys.id").having("count(surveys.id) = ?",0)
this generates following sql query:
SlideGroup Load (9.3ms) SELECT "slide_groups".* FROM "slide_groups" INNER JOIN "survey_group_lists" ON "survey_group_lists"."group_id" = "slide_groups"."id" INNER JOIN "surveys" ON "surveys"."id" = "survey_group_lists"."survey_id" GROUP BY slide_groups.id, surveys.id HAVING (count(surveys.id) = 0)
You're using joins, which is INNER JOIN, whereas what you need is an OUTER JOIN -
includes:
SlideGroup.includes(:surveys).group("slide_groups.id, surveys.id").having("count(surveys.id) = ?",0)
A bit cleaner query:
SlideGroup.includes(:surveys).where(surveys: { id: nil })
Finding orphan records has been explained by others.
I see problems with this approach:
There should not be any orphan in the first place
The presence of a survey.id does not guarantee the presence of a Survey
What about SurveyGroupList that are orphan?
So the proper solution would be to ensure that no orphans are left in the DB. By implementing the proper logic AND adding foreign keys with on delete cascade to the DB. You can also add dependent: :destroy option to your associations but this only works if you use #destroy on your models (not delete) and of course does not work if you delete directly via SQL.

Respect negative conditions for advanced collection associations

I was trying to use this functionality introduced in #645 with conditional 2nd degree has_many ... through relationships with little success.
In my case:
a Course has_many :user_assigned_content_skills, -> { where(source: 'user') }, class_name: "ContentSkill"
and a ContentSkill belongs_to :skill and belongs_to :course
Then Course.ransack({user_assigned_content_skills_skill_name_not_cont: 'ruby'}).result.to_sql returns the following:
"SELECT courses.* FROM courses LEFT OUTER JOIN content_skills ON content_skills.course_id = courses.id AND content_skills.source = 'user' LEFT OUTER JOIN skills ON skills.id = content_skills.skill_id WHERE (skills.name NOT ILIKE '%ruby%')"
This means false positives again if a course has multiple content_skills. Any ideas how to retrieve all courses not being associated with a given skill name?
Many thanks for any insights!
You can get ids of courses associated with a given skill name, and then get a list of courses with ids that don't match the previous found. You can even make it as one composite SQL query.
Course.where.not(id: Course.ransack({user_assigned_content_skills_skill_name_cont: 'ruby'}).result)
This will generate an SQL like this:
SELECT courses.*
FROM courses
WHERE courses.id NOT IN (
SELECT courses.id FROM courses
LEFT OUTER JOIN content_skills ON content_skills.course_id = courses.id AND content_skills.source = 'user'
LEFT OUTER JOIN skills ON skills.id = content_skills.skill_id
WHERE (skills.name ILIKE '%ruby%')
)

Rails ActiveRecord query using multiple joins involving polymorphic association

I'm trying to figure out how I can replicate the following SQL query using AR given the model definitions below. The cast is necessary to perform the average. The result set should group foo by bar (which comes from the polymorphic association). Any help is appreciated.
SQL:
SELECT AVG(CAST(r.foo AS decimal)) "Average", s.bar
FROM rotation r INNER JOIN cogs c ON r.cog_id = c.id
INNER JOIN sprockets s ON s.id = c.crankable_id
INNER JOIN machinists m ON r.machinist_id = m.id
WHERE c.crankable_type = 'Sprocket' AND
r.machine_id = 123 AND
m.shop_id = 1
GROUP BY s.bar
ActiveRecord Models:
class Rotation < ActiveRecord::Base
belongs_to :cog
belongs_to :machinist
belongs_to :machine
end
class Cog < ActiveRecord::Base
belongs_to :crankable, :polymorphic => true
has_many :rotation
end
class Sprocket < ActiveRecord::Base
has_many :cogs, :as => :crankable
end
class Machinist < ActiveRecord::Base
belongs_to :shop
end
UPDATE
I've figured out a way to make it work, but it feels like cheating. Is there are a better way than this?
Sprocket.joins('INNER JOIN cogs c ON c.crankable_id = sprockets.id',
'INNER JOIN rotations r ON r.cog_id = c.id',
'INNER JOIN machinists m ON r.machinist_id = m.id')
.select('sprockets.bar', 'r.foo')
.where(:r => {:machine_id => 123}, :m => {:shop_id => 1})
.group('sprockets.bar')
.average('CAST(r.foo AS decimal)')
SOLUTION
Albin's answer didn't work as-is, but did lead me to a working solution. First, I had a typo in Cog and had to change the relation from:
has_many :rotation
to the plural form:
has_many :rotations
With that in place, I am able to use the following query
Sprocket.joins(cogs: {rotations: :machinist})
.where({ machinists: { shop_id: 1 }, rotations: { machine_id: 123}})
.group(:bar)
.average('CAST(rotations.foo AS decimal)')
The only real difference is that I had to separate the where clause since a machine does not belong to a machinist. Thanks Albin!
I think this code is a little simpler and taking more help from AR
Sprocket
.joins(cogs: {rotations: :machinist})
.where({ machinists: { machine_id: 123, shop_id: 1 } } )
.group(:bar)
.average('CAST(rotations.foo AS decimal)')
The select clause was unnecessary, you don't have to select values since you only need them internally in the query, AR helps you decide what you need afterwards.
I tested this out using a similar structure in one of my own projects but it is not the exact same models so there might be a typo or something in there if it does not run straight up. I ran:
Activity
.joins(locations: {participants: :stuff})
.where({ stuffs: { my_field: 1 } })
.group(:title)
.average('CAST(participants.date_of_birth as decimal)')
producing this query
SELECT AVG(CAST(participants.date_of_birth as decimal)) AS average_cast_participants_date_of_birth_as_decimal, title AS title
FROM `activities`
INNER JOIN `locations` ON `locations`.`activity_id` = `activities`.`id`
INNER JOIN `participants` ON `participants`.`location_id` = `locations`.`id`
INNER JOIN `stuffs` ON `stuffs`.`id` = `participants`.`stuff_id`
WHERE `stuffs`.`my_field` = 1
GROUP BY title
which AR makes in to a hash looking like this:
{"dummy title"=>#<BigDecimal:7fe9fe44d3c0,'0.19652273E4',18(18)>, "stats test"=>nil}

Conditional has_many in rails 2

My app has applicants with many question_sections with many questions with many answers.
Here I am returning these for an applicant
#question_sections = QuestionSection.find(
:all, :include => {:questions => :answers},
:conditions => ['answers.application_form_id is NULL OR answers.application_form_id = ?', #application_form.id],
:order => 'question_sections.list_index ASC, questions.list_index ASC'
)
What I'd like to do is return a row even if the answer row is null (ie, a left join on answer) so we can identify questions that have not been answered rather than omitting them entirely (which is what happens currently.)
I think the issue might be that the answer belongs to both the question and the applicant;
class Answer < ActiveRecord::Base
belongs_to :question
belongs_to :application_form, :touch => true
So, is pseudo code I'd like 'belongs_to :application_form IF :application_form is not null' - to retain any potential associations.
While I can write all this with SQL fairly easily, I'd like to let rails handle that and fix the model.
In SQL I want to go from this
FROM `question_sections`
LEFT OUTER JOIN `questions` ON questions.question_section_id = question_sections.id
LEFT OUTER JOIN `answers` ON answers.question_id = questions.id
WHERE ((answers.application_form_id IS NULL
OR answers.application_form_id = 656))
to this
FROM `question_sections`
LEFT OUTER JOIN `questions` ON questions.question_section_id = question_sections.id
LEFT JOIN `answers` ON answers.question_id = questions.id AND answers.application_form_id = 656
// No WHERE
Thanks.
EDIT
What I need, I think, is a lambda on the has_many association. Something like;
has_many :answers_and_null_answers, :whatever => lambda ( a = Answer.find(n); if a.nil? a = Answer.new; )
Obviously, thats just messy pseudo - but is this possible?
EDIT #2
Aha! first_or_create does what I want, but seems you can'd do it on :includes. I'm assuming there is something I can do to the model to allow this?
The solution I went with was to add raw SQL to find() call. Wrote my joins manually and then edited the view to suit the different output.
Not what I would have preferred to do, but it works.

ActiveRecord find categories which contain at least one item

Support I have two models for items and categories, in a many-to-many relation
class Item < ActiveRecord::Base
has_and_belongs_to_many :categories
class Category < ActiveRecord::Base
has_and_belongs_to_many :items
Now I want to filter out categories which contain at least one items, what will be the best way to do this?
I would like to echo #Delba's answer and expand on it because it's correct - what #huan son is suggesting with the count column is completely unnecessary, if you have your indexes set up correctly.
I would add that you probably want to use .uniq, as it's a many-to-many you only want DISTINCT categories to come back:
Category.joins(:items).uniq
Using the joins query will let you more easily work conditions into your count of items too, giving much more flexibility. For example you might not want to count items where enabled = false:
Category.joins(:items).where(:items => { :enabled => true }).uniq
This would generate the following SQL, using inner joins which are EXTREMELY fast:
SELECT `categories`.* FROM `categories` INNER JOIN `categories_items` ON `categories_items`.`category_id` = `categories`.`id` INNER JOIN `items` ON `items`.`id` = `categories_items`.`item_id` WHERE `items`.`enabled` = 1
Good luck,
Stu
Category.joins(:items)
More details here: http://guides.rubyonrails.org/active_record_querying.html#joining-tables
please notice, what the other guys answererd is NOT performant!
the most performant solution:
better to work with a counter_cache and save the items_count in the model!
scope :with_items, where("items_count > 0")
has_and_belongs_to_many :categories, :after_add=>:update_count, :after_remove=>:update_count
def update_count(category)
category.items_count = category.items.count
category.save
end
for normal "belongs_to" relation you just write
belongs_to :parent, :counter_cache=>true
and in the parent_model you have an field items_count (items is the pluralized has_many class name)
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
in a has_and_belongs_to_many relation you have to write it as your own as above
scope :has_item, where("#{table_name}.id IN (SELECT categories_items.category_id FROM categories_items")
This will return all categories which have an entry in the join table because, ostensibly, a category shouldn't have an entry there if it does not have an item. You could add a AND categories_items.item_id IS NOT NULL to the subselect condition just to be sure.
In case you're not aware, table_name is a method which returns the table name of ActiveRecord class calling it. In this case it would be "categories".

Resources