Rails: Query to get collection associated with collection - ruby-on-rails

So I have these four classes:
class User < ActiveRecord::Base
has_many :water_rights
end
class WaterRight < ActiveRecord::Base
belongs_to :user
has_many :place_of_use_area_water_rights
has_many :place_of_use_areas, through: :place_of_use_area_water_rights
end
class PlaceOfUseAreaWaterRight < ActiveRecord::Base
belongs_to :place_of_use_area
belongs_to :water_right
end
class PlaceOfUseArea < ActiveRecord::Base
has_many :place_of_use_area_water_rights
has_many :water_rights, through: :place_of_use_area_water_rights
end
and I call User.first.water_rights and get a collection of WaterRights. My question is how do I get a collection of PlaceOfUseAreas associated with those WaterRights without doing something like this:
areas = []
water_rights.each do |wr|
areas << wr.place_of_use_areas
end
areas.flatten.uniq{ |a| a.id }
This works but it makes a new query for every single WaterRight. I'm looking for a way to make one query to get the collection of associated PlaceOfUseAreas.

You just want to get all associated PlaceOfUseAreas objects in single query, right?
If so, Rails have pretty single line solution for it:
PlaceOfUseArea.joins(:water_wights).uniq
Read more about joins method if you want more information.

Related

Create Rails scope comparing fields on two tables

I have a number of associated tables in an application
class Listing < ActiveRecord::Base
belongs_to :house
belongs_to :multiple_listing_service
end
class House < ActiveRecord::Base
has_one :zip_code
has_one :primary_mls, through: :zip_code
end
I wanted to create a scope that produces all the Listings that are related to the Primary MLS for the associated House. Put another way, the scope should produce all the Listings where the multiple_listing_service_id = primary_mls.id for the associated house.
I've tried dozens of nested joins scopes, and none seem to work. At best they just return all the Listings, and normally they fail out.
Any ideas?
If I understand correctly, I'm not sure a pure scope would be the way to go. Assuming you have:
class MultipleListingService < ActiveRecord::Base
has_many :listings
has_many :zip_codes
end
I would go for something like:
class House < ActiveRecord::Base
...
def associated_listings
primary_mls.listings
end
end
Update 1
If your goal is to just get the primary listing then I would add an is_primary field to the Listing. This would be the most efficient. The alternative is a 3 table join which can work but is hard to optimize well:
class Listing < ActiveRecord::Base
...
scope :primary, -> { joins(:houses => [:zip_codes])
.where('zip_codes.multiple_listing_service_id = listings.multiple_listing_service_id') }

Rails has_many chain

So I have a table structure below:
Plane
plane_id
plane_info
Seating
seating_id
seating_info
PlaneSeating
plane_seating_id
plane_id
seating_id
PlaneSeatingNote
plane_seating_note_id
plane_seating_id
note_id
Note
note_id
note_info
This gives me a third-normal DB, but I need to set the model relations up.
I currently have:
class Plane < ActiveRecord::Base
has_many :plane_seatings, dependent: :destroy
has_many :seatings, through: :plane_seatings
end
class Seatings < ActiveRecord::Base
has_many :plane_seatings, dependent: :destroy
has_many :planes, through: :plane_seatings
end
class PlaneSeating < ActiveRecord::Base
belongs_to :plane
belongs_to :seating
has_many :plane_seating_notes, dependent: :destroy
has_many :notes, through: :plane_seating_notes
end
class PlaneSeatingNote < ActiveRecord::Base
belongs_to :plane_seating
has_one :note
end
class Note < ActiveRecord::Base
end
Now, this will give me the ability to say Plane.all.first.plan_seatings.first.notes and get the notes I believe. However, I'd like to be able to say Plane.all.first.seatings.notes and get the notes associated with that plane given that seating.
My thought is there should be a way to say, in Plane:
has_many :seatings, through: plane_seating, has_many :notes, through: plane_seating
or some other chaining magic to get a seating with some notes that only apply to that plane and seating combo. a :with, if you will. But I can't think of any syntax that would give me that. Anyone know?
The best is to pivot it the other way, it you want to grab the notes for a certain Plane:
Note.joins(plane_seating_note: [:plane_seating]).where(plane_seating_note: {plane_seating: {plane_id: 1})
You could make that a scope if you're using it in multiple places and if you want it on the Plane model:
class Plane < ActiveRecord::Base
has_many :plane_seatings, dependent: :destroy
has_many :seatings, through: :plane_seatings
def notes
#notes ||= Note.for_plane_id id
end
end
class Note < ActiveRecord::Base
has_many :plane_seating_notes
scope :for_plane_id ->(plane_id) { joins(plane_seating_notes: [:plane_seating]).where(plane_seating_notes: {plane_seating: {plane_id: plane_id}) }
end
For a specific seat on a specific plane, you'd typically see something like this in a controller:
#seat = PlaneSeat.find params[:id]
#plane = #seat.plane
#notes = Note.joins(:plane_seating_notes).where(plane_seating_notes: {plane_seating_id: #seat.id})
But since you have a HMT you could just do
#seat = PlaneSeat.find params[:id]
#plane = #seat.plane
#notes = #seat.notes
A couple "Rails-way" notes:
Unless you are using Note elsewhere, you should just skip the plane_seat_notes.
Consider using has_and_belongs_to_many if you aren't appending any extra meta-data in the intermediate table; this makes relationships easier and gives you shallower query helpers
Consider using polymorphic relationships rather than unnecessary join tables
I used a helper method in the Plane model to get what I wanted. This method may be inefficient if you are dealing with large datasets, but for my datasets, it works fine. It packages up each seating subset for each plane with the notes associated with it into a hash.
#Get an easy to read hash of the seatings with their notes
def seatings_with_notes
#seatings_with_notes = []
self.plane_seatings.each do |item|
seating = Seating.where(id: item.product_application_id).first
notes = item.notes
combo = {seating:seating, notes:notes}
#seatings_with_notes.append(combo)
end
return #seatings_with_notes
end

How to build a has_many-alike association containing a list of various Models?

I have an Order which has many line_items. Only this is not a LineItem module, but a list of "things that act Orderable". E.g. Addon or Site.
class Order
attr_accessor :line_items
before_save :persist_line_items
private
def persist_line_items
#line_items.each {|li| li.save }
end
end
class Addon
belongs_to: order
end
class Site
belongs_to: order
end
Which can be used as:
order = Order.new
order.line_items << Addon.new(order: order)
order.line_items << Site.new(order: order)
But, now I want to load an Order and join the "associated" line_items. I
could load them in an after_initialize hook, and do an
Addon.find_by(order_id: self.id) but that quickly leads to a lot of
queries; where a JOIN would be more appropriate. In addition, I
currently miss the validations trickling up: when a normal has_many
related item is invalid the containing model will not be valid either:
order = Order.new(line_items: [an_invalid_line_item])
order.valid? #=> false
I am wondering if there is a way
to leverage ActiveRecords' has_many-relation to be used with a list of
different models.
I think that a polymorphic association should do the trick.
Would look like this:
class Order < ActiveRecord::Base
has_many :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :orderable, polymorphic: true
end
class Site < ActiveRecord::Base
has_many :line_items, as: :orderable
end
class Addon < ActiveRecord::Base
has_many :line_items, as: :orderable
end
It would use a join table, but i think this is actually a good thing. Otherwise you could use STI for your Addon and Site models, but that would not make a lot of sense in my regard.

Eager load in model?

I wonder if we could eager load in model level:
# country.rb
class Country < ActiveRecord::Base
has_many :country_days
def country_highlights
country_days.map { |country_day| country_day.shops }.flatten.uniq.map { |shop| shop.name }.join(", ")
end
end
# country_day.rb
class CountryDay < ActiveRecord::Base
belongs_to :country
has_many :country_day_shops
has_many :shops, :through => :country_day_shops
end
# shop.rb
class Shop < ActiveRecord::Base
end
Most of the times it's difficult to use .includes in controller because of some polymorphic association. Is there anyway for me to eager load the method country_highlights at the model level, so that I don't have to add .includes in the controller?
You can't "eager load" country_days from a model instance, but you can certainly skip loading them all together by using a has_many through:. You can also skip the extra map, too.
# country.rb
class Country < ActiveRecord::Base
has_many :country_days
has_many :country_day_shops, through: :country_days #EDIT: You may have to add this relationship
has_many :shops, through: :country_day_shops #And change this one to use the new relationship above.
def country_highlights
shops.distinct_names.join(", ")
end
end
# country_day.rb
class CountryDay < ActiveRecord::Base
belongs_to :country
has_many :country_day_shops
has_many :shops, :through => :country_day_shops
end
# shop.rb
class Shop < ActiveRecord::Base
def self.distinct_names
pluck("DISTINCT shops.name") #Edit 2: You may need this instead of 'DISTINCT name' if you get an ambiguous column name error.
end
end
The has_many through: will use a JOIN to load the associate shop records, in effect eager loading them, rather than loading all country_day records and then for each country_day record, loading the associated shop.
pluck("DISTINCT name") will return an array of all of the unique names of shops in the DB, using the DB to perform a SELECT DISTINCT, so it will not return duplicate records, and the pluck will avoid loading ActiveRecord instances when all you need is the string name.
Edit: Read the comments first
You could cache the end result (the joined string or text record in your case), so you'll not have to load several levels of records just to build this result.
1) Add a country_highlights text column (result might be beyond string column limits)
2) Cache the country_highlights in your model with a callback, e.g. before every save.
class Country < ActiveRecord::Base
has_many :country_days
before_save :cache_country_highlights
private
def cache_country_highlights
self.country_highlights = country_days.flat_map(&:shops).uniq.map(&:name).join(", ")
end
end
Caching you calculation will invoke a little overhead when saving a record, but having to load only one instead of three model records for displaying should speed up your controller actions so much that it's worth it.

Rails active record query: Find records on one side of a has_many :through that haven't been used with a particular record on the other side

I have a has_many :through relationship set up like so
class Situation < ActiveRecord::Base
has_many :notifications
has_many :notiftypes, through: :notifications
end
class Notification < ActiveRecord::Base
belongs_to :situation
belongs_to :notiftype
end
class Notiftype < ActiveRecord::Base
has_many :notifications
has_many :situations, through: :notifications
end
So, a Situation has many Notifications, which can be of many types (Notiftype).
My problem is trying to query for the notiftypes that have not been set for a particular situation.
Want to find records with no associated records in Rails 3
The answers in that question get me close, but only to the point of finding Notiftypes that have not been set AT ALL.
If this were the standard :situation has_many :notiftypes I could just do a Left Outer Join like so
myquery = Notiftype.joins('LEFT OUTER JOIN situations ON situations.notiftype_id = notiftype.id').where('notiftype_id IS NULL')
but I'm really not sure how to do this with the intermediate table between them.
I have been trying consecutive joins but it's not working. I'm not sure how to join the two separated tables.
Can anyone explain the right way to query the db? I am using SQLite, Rails 3.1, Ruby 1.9.2 right now, but likely Postgresql in the future.
Try this:
class Situation < ActiveRecord::Base
# ...
has_many :notiftypes, through: :notifications do
def missing(reload=false)
#missing_notiftypes = nil if reload
#missing_notiftypes ||= proxy_owner.notiftype_ids.empty? ?
Notiftype.all :
Notiftype.where("id NOT IN (?)", proxy_owner.notiftype_ids)
end
end
end
Now to get the missing Notiftype
situation.notiftypes.missing
If you want to further optimize this to use one SQL rather than two you can do the following:
class Situation < ActiveRecord::Base
# ...
has_many :notiftypes, through: :notifications do
def missing(reload=false)
#missing_notiftypes = nil if reload
#missing_notiftypes ||= Notiftype.joins("
LEFT OUTER JOIN (#{proxy_owner.notiftypes.to_sql}) A
ON A.id = notiftypes.id").
where("A.id IS NULL")
end
end
end
You can access the missing Notifytypes as:
situation.notiftypes.missing

Resources