Rails belongs_to STI table with scope - ruby-on-rails

I've a STI table "parties" as follows:
# app/models/party.rb
class Party < ApplicationRecord
has_many :party_contacts
scope :vendors, -> { where(type: 'Party::Vendor') }
scope :customers, -> { where(type: 'Party::Customer') }
end
# app/models/party/vendor.rb
class Party::Vendor < Party
end
# app/models/party/customer.rb
class Party::Customer < Party
end
And "party_contacts" table as follow:
class PartyContact < ApplicationRecord
belongs_to :party
scope :of_vendors, -> {# fetch all contacts belongs to all vendors logic }
scope :of_customers, -> {# fetch all contacts belongs to all customers logic }
end
I want to make query on "party_contacts" to get list of all vendors/customers contacts. How can I write the scope for "party_contacts" (or should it be in parent model)?
I'm trying following scopes:
scope :of_vendors, -> { joins(:party).includes(:party).where( party: { type: "Party::Vendor" } ) }
scope :of_customers, -> { joins(:party).includes(:party).where( party: { type: "Party::Customer" } ) }
But get error:
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: missing FROM-clause entry for table "party"
LINE 1: ...parties"."id" = "party_contacts"."party_id" WHERE "party"."t...

Thank you #Swards, I figured it out just a while before. The scope should be as follows:
scope :of_vendors, -> { joins(:party).includes(:parties).where( parties: { type: "Party::Vendor" } ) }
scope :of_customers, -> { joins(:party).includes(:parties).where( parties: { type: "Party::Customer" } ) }
It constructs right query:
2.4.0 :042 > PartyContact.of_vendors
PartyContact Load (8.8ms) SELECT "party_contacts".* FROM "party_contacts" INNER JOIN "parties" ON "parties"."id" = "party_contacts"."party_id" WHERE "parties"."type" = $1 [["type", "Party::Vendor"]]
=> #<ActiveRecord::Relation []>
2.4.0 :043 > PartyContact.of_customers
PartyContact Load (0.5ms) SELECT "party_contacts".* FROM "party_contacts" INNER JOIN "parties" ON "parties"."id" = "party_contacts"."party_id" WHERE "parties"."type" = $1 [["type", "Party::Customer"]]
=> #<ActiveRecord::Relation []>
Here party is singular in joins(:party) as it is belongs_to relation. And parties is plural in includes(:parties) and where(parties:{...}) as it is name of table.

Related

In Rails 6, how do I add a condition to a left-outer-joins finder?

I’m using Rails 6.1.4.4. I have this model with a has_many
class MyObject < ApplicationRecord
has_many :products, inverse_of: :application, as: :item
How do I write a scope that does a left outer join and also adds a condition in the LEFT-OUTER-JOIN-ON clause? I have fallen back on raw sql …
scope :awaiting_funding, ->(resubmissions: false) {
joins("LEFT OUTER JOIN products on products.item_id = my_objects.id and products.product_type = 11 and products.item_type = ‘MyObject’”).where('products.id is null')
}
But I would like to convert this to a more Rails-like finder method.
Define a new has_many
class MyObject < ApplicationRecord
has_many :products, inverse_of: :application, as: :item
has_many :my_object_products, -> { where(product_type: 11, item_type: 'MyObject') }, class_name: 'Product'
Now you can define your scope
scope :awaiting_funding, ->(resubmissions: false) {
where.missing(:my_object_products)
}
This will create the query where product_type and item_type are part of the ON in the LEFT OUTER JOIN
PS: use a better name for my_object_products but you get the idea.
Does this work?
scope :awaiting_funding, ->(resubmissions: false) {
left_outer_joins(:products).where(product_type: 11, item_type: 'MyObject', products: { id: nil })
}
I will give you a much generic example of Left Outer Join
Source.
select('a.*', 'count(b.*)').
left_outer_joins(:b).
joins(:c).
where('c.body_parser = ?', true).
group('a.id').
having('count(b.id) = 0').
all
Else, You can also use includes. This will generate a LEFT OUTER JOIN query
MyObject.includes(:products).where(product_type: 11, item_type: 'MyObject', products: { id: nil })

Scoping in Rails with a has_one relationship and an OR

I'm having trouble setting up a scope that will check a value on a has_one relationship. I have a Document model. Each Document has_one Document::Response, and Document::Response has a status field (I generated the Document::Response model with rails g model documents/response, which made things a bit more confusing than I'd anticipated. The strong parameters method references it as :document_responses for example. Not sure if it's relevant though. )
scope :rejected, -> { joins(:response).where(status: 'rejected') }
I'll also need to check for something else once I get that working:
scope :rejected, -> { joins(:response).where(status: 'rejected').or(status: 'owed' }
But I can't seem to get the syntax right on this.
PG::UndefinedColumn: ERROR: column documents.status does not exist
LINE 1: ...ts"."id" WHERE "documents"."mortgage_id" = $1 AND "documents...
^
: SELECT COUNT(*) FROM "documents" INNER JOIN "document_responses" ON
"document_responses"."document_id" = "documents"."id" WHERE
"documents"."mortgage_id" = $1 AND "documents"."status" = $2
Routes:
resources :mortgages, shallow: true do
resources :documents do
collection do
post :create_templates
end
end
member do
post :archive
end
end
resources :responses, controller: 'document/responses' do
member do
post :accept
post :reject
end
end
It is required to explicitly tell AR to use status column from responses table instead of documents:
scope :rejected, -> {
joins(:response).where(responses: { status: 'rejected' })
}
scope :processed, -> {
joins(:response).where(responses: { status: ['rejected', 'owed'] })
}

Scope with 2 different conditions

I would like to filter stories on my index based on 2 different conditions where one is for the Current Country and the other one is for All Countries. Is it possible to create a scope where it could fetch stories for both this condition ?
All Countries is boolean field where in my Story table. The logic is if the Story is created for all countries the field, all_countries = 1
Featured Item model, is where the stories could be featured on the index page if the writer would like to do so.
This is how my model looks like for now with the scopes
class Country < ActiveRecord::Base
has_many :stories
end
class Story < ActiveRecord::Base
belongs_to :countries
has_many :featured_items, dependent: :destroy
scope :by_country, lambda { |id| where(:country_id => id)}
scope :for_all_countries, where(:all_countries => true)
end
class FeaturedItem < ActiveRecord::Base
belongs_to :story
scope :by_country, -> (country) {joins(:story).where('`stories`.country_id = ?', Country.find(country) )}
scope :for_all_countries, -> { joins(:story).where('`stories`.all_countries = ?',true) }
end
p/s the scope for all countries on the featured Items also returns an error.
You can do this sort of thing:
scope :by_country, -> (country) { country == :all ? where(:all_countries => true) : where(:country_id => country) }
You may need to add a little more logic to handle bad params.
And for the join table, you can join and filter on the stories.
class FeaturedItem < ActiveRecord::Base
scope :by_country, -> (country) { (country == :all ? where( :stories => { :all_countries => true } ) : where( :stories => { :country_id => country } ) ).joins(:story) }
end
Your scope syntax is currently wrong, as is your pluralization of the belongs_to association.
You'll need to use the following (#swards answer is right, this is just an addition):
#app/models/story.rb
class Story < ActiveRecord::Base
belongs_to :country
scope :countries, ->(ids = :all) { ids == :all ? where(all_countries: true) : find(ids) }
end
This will allow you to call Story.countries to return all countries, and Story.countries(1,2,4,5) to return individual ones.
filter stories on my index based on 2 different conditions where one is for the Current Country and the other one is for All Countries.
Have you considered using the following in your Country model:
#stories = #country ? #country.stories : Country.stories
#app/models/country.rb
class Country < ActiveRecord::Base
has_many :stories
scope :stories, -> { joins(:stories).where(story: {all_countries: true}) }
end

Querying through a join table Rails 4 Postgres

I am trying to get an array of records through a join table. I want to find All of one User's Favorites that are Blue, but that are "current". User_Favorites can expire. This is what I'm trying:
User.rb
has_many :user_favorites, dependent: :destroy
has_many :favorites, through: :user_favorites
Favorite.rb
has_many :user_favorites, dependent: :destroy
has_many :users, through: :user_favorites
UserFavorite.rb
belongs_to :user
belongs_to :favorite
scope :current_as_of, -> (date) do
where('start_date <= ?',date).
where('(end_date >= ? or end_date IS NULL)', date)
end
scope :blue, -> { where('self.favorite.color = ?','blue') }
class UsersController < ApplicationController
def profile
#user = User.find(params[:id])
#blue_favorites = #user.user_favorites.current_as_of(Date.today).blue.all
end
This is the error I get:
There is an Error: PG::UndefinedTable: ERROR: missing FROM-clause entry for table "favorite"
LINE 1: ...d_date >= '2015-10-06' or end_date IS NULL)) AND (Favorite.co...
^
: SELECT "user_favorites".* FROM "user_favorites" WHERE "user_favorites"."user_id" = $1 AND (start_date <= '2015-10-06') AND ((end_date >= '2015-10-06' or end_date IS NULL)) AND (Favorite.color = 'blue')
in regards to this:
scope :blue, -> { where('self.favorite.color = ?','blue') }
It looks like you're mixing up database and ruby syntax. The other problem too is that the query, at this point, has no idea what favorite is because it hasn't been joined yet. Try something like this instead:
scope :blue, -> { joins(:favorite).where(favorites: {color: 'blue'}) }
If I understand it correctly your :blue scope should look something like this:
scope :blue, -> { joins(:favorite).where(favorites: { color: 'blue' }) }
In the joins you have to use the association name while in the where clause you have to use the table name.

Trouble translating SQL query to Arel

I have a search page that narrows down the list of a specific class, and I want an OR condition that can grab two different conditions and add the together, for example, I have classes
model/party.rb
class Party < ActiveRecord::Base
has_many :invitations
end
mode/invitation.rb
class Invitation < ActiveRecord::Base
belongs_to :party
end
invitation has a status attribute, which will be "decline", "accept", or "unanswered"
What I want to do is grab all the parties that do not have any invitations, or any that have all of the invitations "unanswered".
I currently do
scope :not_confirmed, lambda { find_by_sql( "SELECT * FROM `parties` INNER JOIN `invitations` ON `invitations`.`party_id` = `parties`.`id` WHERE (invitations.status = 'unanswered') OR (parties.id NOT IN (SELECT DISTINCT(party_id) FROM invitations))" ) }
which works, but since it does not lazy load I can't add them in a facet like query.
I did something like
no_invitations.or(no_one_has_answered)
but it did not work.
I especially do not get the concept of using OR on AREL, could someone please help out?
edited:
For a very ugly yet functional work around until I get this down, here is what I have done
party.rb
scope :not_confirmed, lambda { joins(:invitations).where( "invitations.status NOT IN (?)", ["accepted", "declined" ] ) }
scope :with_no_invitations, lambda { includes(:invitaions).where( :invitations => { :party_id => nil } ) }
search_controller.rb
#parties = Party.all_the_shared_queries
#parties = ( #parties.not_confirmed + #parties.with_no_invitations).uniq
The query:
scope :not_confirmed, lambda { find_by_sql( "SELECT * FROM `parties` INNER JOIN `invitations` ON `invitations`.`party_id` = `parties`.`id` WHERE (invitations.status = 'unanswered') OR (parties.id NOT IN (SELECT DISTINCT(party_id) FROM invitations))" ) }
can be converted to arel with some transformation using boolean algebra too. But since it is only theoretical conversion, you have to verify it manually. So:
class Invitation < ActiveRecord::Base
belongs_to :party
scope :non_answered, -> { where(arel_table[:status].not_eq('unanswered')) }
end
class Party < ActiveRecord::Base
has_many :invitations
scope :not_confirmed, -> { not.join(:invitaions).merge(Invitation.non_answered)) }
end
Please test it and comment here.
Firstly, from the question tags, I have assumed that you are using Rails3 (had it been Rails4, there were more easy ways of doing things :))
For your requirement above (ie grab all the parties that do not have any invitations, or any that have all of the invitations "unanswered"), here is a way of doing it (using scope :unattended):
Party Model:
class Party < ActiveRecord::Base
has_many :invitations
scope :invitations_answered, -> { joins(:invitations).merge(Invitation.answered) }
scope :unattended, -> { where(arel_table[:id].not_in invitations_answered.pluck(:id)) }
end
Invitation Model:
class Invitation < ActiveRecord::Base
belongs_to :party
scope :answered, -> { where(status: ["decline", "accept"])}
end
In Rails 4, you can use where.not and simplify it further like this:
Party Model:
class Party < ActiveRecord::Base
has_many :invitations
scope :invitations_answered, -> { joins(:invitations).merge(Invitation.answered) }
scope :unattended, -> { where.not(id: invitations_answered.pluck(:id)) }
end
Invitation Model:
class Invitation < ActiveRecord::Base
belongs_to :party
scope :answered, -> { where.not(status: 'unanswered') }
end

Resources