How to retrieve data from multiple tables by multiple foreign keys in one query in ActiveRecord?

Viewed 233

There is a model named Track:

class Track < ApplicationRecord
  has_many :profile_tracks
  has_many :favorite_tracks
  has_many :bookmark_tracks
  has_many :listened_tracks
end

And there is a model named Profile:

class Profile < ApplicationRecord
  has_many :profile_tracks
  has_many :favorite_tracks
  has_many :bookmark_tracks
  has_many :listened_tracks
end

I need to retrieve entries from those tables that share the same track_id and profile_id values. E.g.:

ProfileTrack.find_by(track_id: 1, profile_id: 1)
+
FavoriteTrack.find_by(track_id: 1, profile_id: 1)
+
BookmarkTrack.find_by(track_id: 1, profile_id: 1)
+
ListenedTrack.find_by(track_id: 1, profile_id: 1)

or

track = Track.find_by(id: 1)

track.profile_tracks.find_by(profile_id: 1)
+
track.favorite_tracks.find_by(profile_id: 1)
+
track.bookmark_tracks.find_by(profile_id: 1)
+
track.listened_tracks.find_by(profile_id: 1)

or

profile = Profile.find_by(id: 1)

profile.profile_tracks.find_by(track_id: 1)
+
profile.favorite_tracks.find_by(track_id: 1)
+
profile.bookmark_tracks.find_by(track_id: 1)
+
profile.listened_tracks.find_by(track_id: 1)

and return something like this:

{
  profile_track_id: some_id,
  favorite_track_id: some_id,
  bookmark_track_id: some_id,
  listened_track_id: some_id
}

or ActiveRecord model with these ids.

How can I do it in one query?

2 Answers

If I understand correctly you are trying to create a some kind of music playlist app? You might want to change your has_many in Track to belongs_to, but it depends on your other models. Anyway, you should be able to access them directly by using joins.

track = Track.joins(:profile_tracks, :favorite_tracks, :bookmark_tracks, 
                    :listened_tracks).find_by(id: 1)

You can use find(1) and this will raise an error if not found, find_by(id:1) will return nil. Then in your hash

{
  profile_track_id: track.profile_track_id,
  favorite_track_id: track.favorite_track_id,
  bookmark_track_id: track.bookmark_track_id,
  listened_track_id: track.listened_track_id
}

Update

To align with the question, as the above was wrong answer but still might be useful.

track = Track.joins(:profile_tracks, :favorite_tracks, :bookmark_tracks, 
                    :listened_tracks, :profiles).where(profiles: {id: 1})

This will produce below query

SELECT "tracks".* FROM "tracks" 
INNER JOIN "profile_tracks" ON "profile_tracks"."track_id" = "tracks"."id"
... 
INNER JOIN "profiles" ON "profiles"."track_id" = "tracks"."id"
WHERE "profiles"."id" = $1 

Update 2

Thanks for pointing out that the relation was non-existing between tracks and profiles, how about rearranging the structure?

class Profile < ApplicationRecord
  has_many :profile_tracks
  has_many :tracks, through: :profile_tracks
end

class ProfileTrack < ApplicationRecord
  belongs_to :profile
  has_many :tracks
end

class Track < ApplicationRecord
  belongs_to :profile_track
end

that would allow you to access both track_ids and profile_track_ids from Profile

Did it with raw SQL and UNION:

track = Track.find_by(id: 1)

a =
  track
  .profile_tracks
  .where(profile_id: 1)
  .select("id as value, 'profile_track_id' as key")
  .to_sql

b =
  track
  .favorite_tracks
  .where(profile_id: 1)
  .select("id as value, 'favorite_track_id' as key")
  .to_sql

c =
  track
  .bookmark_tracks
  .where(profile_id: 1)
  .select("id as value, 'bookmark_track_id' as key")
  .to_sql

d =
  track
  .listened_tracks
  .where(profile_id: 1)
  .select("id as value, 'listened_track_id' as key")
  .to_sql

ActiveRecord::Base
  .connection
  .execute("#{a} UNION #{b} UNION #{c} UNION #{d}")
  .to_a
  .map {|a| { a['key'] => a['value']}}
  .inject(:merge)
Related