Create Rails scope comparing fields on two tables - ruby-on-rails

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') }

Related

In Rails 5 what is the best way to show duplicate records across a has_and_belongs_to_many and a belongs to relationship

Given the following model structures;
class Project < ApplicationRecord
has_many :leads
has_and_belonds_to_many :clients
end
class Lead < ApplicationRecord
belongs_to :project
end
class Client < ApplicationRecord
has_and_belongs_to_many :projects
end
How you would suggest reporting on duplicate leads across a Client?
Right now I am doing something very gnarly with flattens and counts, it feels like there should be a 'Ruby way'.
Ideally I would like the interface to be able to say either Client.first.duplicate_leads or Lead.first.duplicate?.
Current (terrible) solution
#duplicate_leads = Client.all.map(&:duplicate_leads).flatten
Class Client
def duplicate_leads
leads = projects.includes(:leads).map(&:leads).flatten
grouped_leads = leads.group_by(&:email)
grouped_leads.select { |_, v| v.size > 1 }.map { |a| a[1][0] }.flatten
end
end
Environment
Rails 5
Ruby 2.3.1
You could try this.
class Client < ApplicationRecord
has_and_belongs_to_many :projects
has_many :leads, through: :projects
def duplicate_leads
duplicate_ids = leads.group(:email).having("count(email) > 1").count.keys
Lead.where(id: duplicate_ids)
end
end
You could try creating a has_many association from Lead through Project back to Lead, in which you use a lambda to dynamically join based on a match on email between the two records, and on the id not matching. This would mark both records as a duplicate -- if you wanted to mark only one then you can require that the id of one is less than the id of the other.
Some hints: Rails has_many with dynamic conditions

Rails: Query to get collection associated with collection

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.

rails 4 scope through multiple has_one associations

I am trying to get an activerecord association through 2 layers of has_one associations and cannot quite figure it out.
I have 3 models:
class Dialog < ActiveRecord::Base
belongs_to :contact
end
class Contact < ActiveRecord::Base
has_many :dialogs
belongs_to :location
end
class Location < ActiveRecord::Base
has_many :contacts
end
I would like to create a scope in the Dialog model that allows me to pass in the id of a Location and get all Dialogs created by Contacts from the given Location... something like:
Dialog.from_location(Location.first.id)
I can get a non-activerecord array of the desired result using select:
Dialog.all.select{|s| s.contact.location_id == Location.first.id }
But I need this to return an activerecord array so that other scopes or class methods can be called on the result. I have tried using joins and includes, but am confused after reading the rails guides on how to use them.
Can anyone help me figure out how to construct a scope or class method that will accomplish this?
Thanks in advance
Just adding a note to the answer you accepted, quoting from the ruby on rails guides website:
Using a class method is the preferred way to accept arguments for scopes. These methods will still be accessible on the association objects ( link )
So in your condition, instead of doing a scope with an argument, define a method :
class Contact < ActiveRecord::Base
has_many :dialogs
belongs_to :location
def self.from_location(id)
where(location_id: id)
end
end
You can define your scopes as follows:
class Dialog < ActiveRecord::Base
belongs_to :contact
scope :from_location, -> (id) do
where(contact_id: Contact.from_location(id).select(:id))
end
end
class Contact < ActiveRecord::Base
has_many :dialogs
belongs_to :location
scope :from_location, ->(id) do
where(location_id: id)
end
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.

How to create an association that sets join table attributes automatically?

I am totally confused about how I should go about "the rails way" of effectively using my associations.
Here is an example model configuration from a Rails 4 app:
class Film < ActiveRecord::Base
# A movie, documentary, animated short, etc
has_many :roleships
has_many :participants, :through => :roleships
has_many :roles, :through => :roleships
# has_many :writers........ ?
end
class Participant < ActiveRecord::Base
# A human involved in making a movie
has_many :roleships
end
class Role < ActiveRecord::Base
# A person's role in a film. i.e. "Writer", "Actor", "Extra" etc
has_many :roleships
end
class Roleship < ActiveRecord::Base
# The join for connecting different people
# to the different roles they have had in
# different films
belongs_to :participant
belongs_to :film
belongs_to :role
end
Given the above model configuration, the code I wish I had would allow me to add writers directly to a film and in the end have the join setup correctly.
So for example, I'd love to be able to do something like this:
## The Code I WISH I Had
Film.create!(name: "Some film", writers: [Participant.first])
I'm not sure if I'm going about thinking about this totally wrong but it seems impossible. What is the right way to accomplish this? Nested resources? A custom setter + scope? Something else? Virtual attributes? thank you!
I created a sample app based on your question.
https://github.com/szines/hodor_filmdb
I think useful to setup in Participant and in Role model a through association as well, but without this will work. It depends how would you like to use later this database. Without through this query wouldn't work: Participant.find(1).films
class Participant < ActiveRecord::Base
has_many :roleships
has_many :films, through: :roleships
end
class Role < ActiveRecord::Base
has_many :roleships
has_many :films, through: :roleships
end
Don't forget to give permit for extra fields (strong_parameters) in your films_controller.rb
def film_params
params.require(:film).permit(:title, :participant_ids, :role_ids)
end
What is strange, that if you create a new film with a participant and a role, two records will be created in the join table.
Update:
You can create a kind of virtual attribute in your model. For example:
def writers=(participant)
#writer_role = Role.find(1)
self.roles << #writer_role
self.participants << participant
end
and you can use: Film.create(title: 'The Movie', writers: [Participant.first])
If you had a normal has_and_belongs_to_many relationship i.e. beween a film and a participant, then you can create a film together with your examples.
As your joining model is more complex, you have to build the roleships separately:
writer= Roleship.create(
participant: Participant.find_by_name('Spielberg'),
role: Role.find_by_name('Director')
)
main_actor= Roleship.create(
participant: Participant.find_by_name('Willis'),
role: Role.find_by_name('Actor')
)
Film.create!(name: "Some film", roleships: [writer, main_actor])
for that, all attributes you use to build roleships and films must be mass assignable, so in a Rails 3.2 you would have to write:
class Roleship < ActiveRecord::Base
attr_accessible :participant, :role
...
end
class Film < ActiveRecord::Base
attr_accessible :name, :roleships
...
end
If you want to user roleship_ids, you have to write
class Film < ActiveRecord::Base
attr_accessible :name, :roleship_ids
...
end
Addendum:
Of cause you could write a setter method
class Film ...
def writers=(part_ids)
writer_role=Role.find_by_name('Writer')
# skiped code to delete existing writers
part_ids.each do |part_id|
self.roleships << Roleship.new(role: writer_role, participant_id: part_id)
end
end
end
but that makes your code depending on the data in your DB (contents of table roles) which is a bad idea.

Resources