简体   繁体   中英

Active record, SQL: select and count association with joins condition

I'm having an issue optimizing a SQL query, and utilizing active record to produce the query. it involves selecting the id and the count of an association as well as a conditional join with a variable input. Further, i havent been able to use a where statement because i want results that do not have any associations yet. My models are as follows

class Vote < ActiveRecord::Base
  belongs_to :user
  belongs_to :venue
  belongs_to :catering_event
  validates :catering_event_id, uniqueness: { scope: :user_id }
end

class Venue < ActiveRecord::Base
  has_many :votes
  has_many :catering_events, through: :votes
  has_many :users, through: :votes
end

class CateringEvent < ActiveRecord::Base
  has_many :votes
  has_many :venues, through: :votes
  has_many :users, through: :votes  
end

What I have are venues, catering_events (events for short), and users. users can cast 1 vote per event, where a vote is a user, choosing a venue, for an event (user_id, venue_id, and catering_event_id).

On the show event page, i want to display all venues and their vote counts, where venues without vote counts display a vote count of 0, which i'm trying to accomplish with a single query. I havnt been able to utilize a where clause, because my attempts have filtered out venues which have not yet been voted on.

i currently have two functional solutions, both of which i'm unsatisfied with. the first is a direct find_by_sql query; however i have to semi-hack the return values because find_by_sql is only returning attributes that the model currently has.

For my current Data:

#<Vote:0x007fb9f681ac38 id: 1, user_id: 2, venue_id: 3, catering_event_id: 1>,
#<Vote:0x007fb9f681aa58 id: 2, user_id: 1, venue_id: 2, catering_event_id: 1>,
#<Vote:0x007fb9f681a8f0 id: 3, user_id: 1, venue_id: 1,  catering_event_id: 2>

My functional find_by_sql query:

sql =     
"SELECT COUNT(votes.id) AS id, venues.id AS venue_id 
FROM venues 
LEFT OUTER JOIN votes ON votes.venue_id = venues.id 
AND votes.catering_event_id = ? 
LEFT OUTER JOIN catering_events ON catering_events.id = votes.catering_event_id 
GROUP BY venues.id"

=> "SELECT COUNT(votes.id) AS id, venues.id AS venue_id FROM venues LEFT OUTER JOIN votes ON votes.venue_id = venues.id AND votes.catering_event_id = ? LEFT OUTER JOIN catering_events ON catering_events.id = votes.catering_event_id GROUP BY venues.id"

pry(main)> Vote.find_by_sql [sql, '1']
Vote Load (9.0ms)  SELECT COUNT(votes.id) AS id, venues.id AS venue_id FROM venues LEFT OUTER JOIN votes ON votes.venue_id = venues.id AND votes.catering_event_id = '1' LEFT OUTER JOIN catering_events ON catering_events.id = votes.catering_event_id GROUP BY venues.id

=> [
  #<Vote:0x007fb9f33ca8c0 id: 0, venue_id: 1>, 
  #<Vote:0x007fb9f33ca668 id: 1, venue_id: 2>, 
  #<Vote:0x007fb9f33ca258 id: 1, venue_id: 3>
]

this returns all venues and their votecounts for catering event 1. however, i have to name the vote_count as some attribute on the model from which i call find_by_sql, and because a vote has 3 conditions, theres no 1 model to which i can add a votecount attribute. when i try to namespace the count to a custom field, i dont get an @attributes has as the docs say, it just omits the value:

Vote Load (0.7ms)  SELECT COUNT(votes.id) AS vote_count, venues.id AS venue_id 
FROM venues 
LEFT OUTER JOIN votes ON votes.venue_id = venues.id 
AND votes.catering_event_id = '1' 
LEFT OUTER JOIN catering_events ON catering_events.id = votes.catering_event_id 
GROUP BY venues.id
=> [
  #<Vote:0x007fb9f31d29a0 id: nil, venue_id: 1>, 
  #<Vote:0x007fb9f31d2248 id: nil, venue_id: 2>, 
  #<Vote:0x007fb9f31d1c58 id: nil, venue_id: 3>
]

The second solution i've worked up involves using active record to make the query, however i havent figured out how to properly sanitize the joins statement with the variable catering_event_id: (i dont actually run this on new lines, but for readability i split it up)

Venue.select('venues.id as venue_id')
  .joins('LEFT OUTER JOIN votes ON votes.venue_id = venues.id AND votes.catering_event_id = 2')
  .joins('LEFT OUTER JOIN catering_events ON catering_events.id = votes.catering_event_id').group('venues.id')
  .count('votes.id')
(0.6ms)  SELECT COUNT(votes.id) AS count_votes_id, venues.id AS venues_id FROM `venues` LEFT OUTER JOIN votes ON votes.venue_id = venues.id AND votes.catering_event_id = 2 LEFT OUTER JOIN catering_events ON catering_events.id = votes.catering_event_id GROUP BY venues.id
=> {1=>1, 2=>0, 3=>0}

notice the first joins statement, I had to hard code the 2 in:

AND votes.catering_event_id = 2

as i havnt figured out how to pass a variable to the active record joins statement.

in summary, I'm trying to perform a single query using either direct sql with find_by_sql or active record (my preference is with active record), to select all venues, and their vote count for a variable catering_event. I'd prefer to tweak one of my current solutions to either allow find_by_sql to return custom name-spaced attributes or figure out how to properly pass a joins statement an interpolated/sanitized variable.

i'm on Rails 4.2.1, and ruby 2.2.0

The answer ended up being in the find_by_sql command.

Although it doesn't show it in the return values, a virtual attribute is created using the AS keyword in the query. I can now do SELECT COUNT(votes.id) AS vote_count and although the return objects don't show the attribute vote_count , they respond to it.

eg

query = Vote.find_by_sql([sql, '1'])
query.map(&:vote_count)
=> [0,1,1]

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM