Multiple INNER JOINs in a Rails has_many relationship? - ruby-on-rails-3

I have the following models:
class Image < ActiveRecord::Base
belongs_to :gallery
has_many :bookmarks
has_many :gallery_tags, :foreign_key => :gallery_id
end
class Bookmark < ActiveRecord::Base
belongs_to :user
belongs_to :image
has_many :gallery_tags, :through => :image, :source => :gallery_tags
end
class GalleryTag < ActiveRecord::Base
belongs_to :gallery
belongs_to :tag
end
class Gallery < ActiveRecord::Base
belongs_to :provider
has_many :images
belongs_to :user
has_many :gallery_tags
has_many :tags, :through => :gallery_tags
end
class Tag < ActiveRecord::Base
end
class User < ActiveRecord::Base
has_many :bookmarks
has_many :galleries
end
I'd like to be able to do
User.find(1).bookmarked_tags
and retrieve all tags associated with all of the user's bookmarked images via the galleries associated with the images. Tags are associated with galleries.
The query would end up looking like this:
SELECT
tags.*
FROM
tags
INNER JOIN gallery_tags ON gallery_tags.tag_id = tags.id
INNER JOIN images ON gallery_tags.gallery_id = images.gallery_id
INNER JOIN bookmarks ON images.id = bookmarks.image_id
WHERE
bookmarks.user_id = 1
GROUP BY
tags.id;
I've tried adding
has_many :tags, :through => :gallery_tags, :foreign_key => :gallery_id
to Image, which causes Image.find(34).tags to result in
SELECT `tags`.* FROM `tags` INNER JOIN `gallery_tags` ON `tags`.id = `gallery_tags`.tag_id WHERE ((`gallery_tags`.gallery_id = 34))
(it's not using the image's gallery_id in the query), and then I've tried adding
has_many :bookmarked_tags, :through => :bookmarked_images, :source => :tags
to User, which causes User.find(1).bookmarked_tags to result in
ActiveRecord::HasManyThroughSourceAssociationMacroError: Invalid
source reflection macro :has_many :through for has_many
:bookmarked_tags, :through => :bookmarked_images. Use :source to
specify the source reflection.
So: how can I get Rails to run the query I posted with User.find(1).bookmarked_tags?

there are two solutions
First Solution:
Create a view inside your database that acts like a join table:
CREATE VIEW user_bookmarks_tags (
SELECT
bookmarks.user_id AS user_id
gallery_tags.tag_id AS tag_id
FROM
gallery_tags
INNER JOIN images ON gallery_tags.gallery_id = images.gallery_id
INNER JOIN bookmarks ON images.id = bookmarks.image_id
GROUP BY
user_id, tag_id
)
(you can do this inside a migration)
This view acts like a "table" with the columns user_id | tag_id
Now we can adapt our models using has_and_belongs_to_many:
user.rb
class User < ActiveRecord::Base
has_many :bookmarks
has_many :galleries
# we use our magic (view) join table here!
has_and_belongs_to_many :tags, :join_table => :user_bookmarks_tags
end
tag.rb
class Tag < ActiveRecord::Base
# we use our magic (view) join table here
has_and_belongs_to_many :users, :join_table => :user_bookmarks_tags
end
Now you can do: User.find(1).tags or Tag.find(1).users :)
Second Solution
Do the join manually without a view:
define the missing relations (needed for the automated joins foreign_key lookup):
tag.rb
class Tag < ActiveRecord::Base
has_many :gallery_tags
end
gallery_tag.rb
class GalleryTag < ActiveRecord::Base
belongs_to :gallery
belongs_to :tag
# new:
has_many :images, :through => :gallery
end
Now we can add a bookmarked_tags method to our user.rb
class User < ActiveRecordBase
has_many :bookmarks
has_many :galleries
def bookmarked_tags
Tag.joins(:gallery_tags => {:images => :bookmarks}).where('bookmarks.user_id = ?', self.id).group('tags.id')
end
end

Related

Excluding results in has_many :through relation in Rails 3

I am trying to select groups which the #current_user is NOT a member of
The relevant parts of my models are as follows:
class Group < ActiveRecord::Base
belongs_to :user
has_many :group_memberships
has_many :members, :class_name => "User", :through=>:group_memberships
...
class User < ActiveRecord::Base
has_many :group_memberships, :foreign_key => 'member_id'
has_many :groups, :through => :group_memberships
...
class GroupMembership < ActiveRecord::Base
belongs_to :member, :class_name=>"User"
belongs_to :group
end
Thanks!
You could try and do something like:
#groups = Group.where("id NOT IN (?)", current_user.groups)
More information can be found in the Active Record Query.
have a method like this:
def group_ids_not_a_member_of
# get all group_ids and subtract out the ids that he is a member of
Group.pluck('id') - current_user.groups.map(&:id)
end
then
user.group_ids_not_a_member_of

Rails joins through association

In Ruby on Rails, I want to find employers in the city.
Lets say the models are set up this way:
City
has_many :suburbs
has_many :households, :through => suburbs
has_many :people, :through => suburbs
Suburb
has_many :households
has_many people, :through => households
belongs_to :city
Household
has_many :people
belongs_to :suburb
People
belongs_to :household
belongs_to :employer
Employer
has_many :people
I feel like I want some sort of Employer joins some_city.people but I don't know how to do this. If people belonged directly to cities, I could join Employer to people where city_id is something, but I want to find the same data without that direct join and I am a little lost.
Thank you.
Use nested joins
Employer.joins({:people => {:household => {:suburb => :city}}})
should give you the join table you're looking. If you were traversing the other direction you would use plural names
City.joins( :suburbs => {:households => {:people => :employers }})
You can do the join like jvans has illustrated. Or you can setup your relationships like the following:
class Employer < ActiveRecord::Base
has_many :people
has_many :households, through: :people
has_many :suburbs, through: :households
has_many :cities, through: :suburbs
end
class Person < ActiveRecord::Base
belongs_to :household
belongs_to :employer
end
class Household < ActiveRecord::Base
belongs_to :suburb
has_many :people
end
class Suburb < ActiveRecord::Base
belongs_to :city
has_many :households
has_many :people, through: :households
end
class City < ActiveRecord::Base
has_many :suburbs
has_many :households, through: :suburbs
has_many :people, through: :households
has_many :employers, through: :people
end
Then you can join City from Employer, and vice-versa, directly.
For example:
Employer.joins(:cities).where("cities.name = ?", "Houston").first
SELECT "employers".* FROM "employers"
INNER JOIN "people" ON "people"."employer_id" = "employers"."id"
INNER JOIN "households" ON "households"."id" = "people"."household_id"
INNER JOIN "suburbs" ON "suburbs"."id" = "households"."suburb_id"
INNER JOIN "cities" ON "cities"."id" = "suburbs"."city_id" WHERE (cities.name = 'Houston')
LIMIT 1

Rails: Find with join model - How to?

I have the following models associations which I want to use in order to do some searches of players:
class Player < ActiveRecord::Base
belongs_to :user
has_many :abilities
has_many :sports, :through => :abilities
...
end
class User < ActiveRecord::Base
has_one :player
...
end
class Ability < ActiveRecord::Base
belongs_to :player
belongs_to :sport
has_one :level
...
end
class Sport < ActiveRecord::Base
has_and_belongs_to_many :category_sports
has_many :abilities
has_many :players, :through => :abilities
...
end
class CategorySport < ActiveRecord::Base
has_and_belongs_to_many :sports
end
I have a form where a user can fill two inputs: city and sport
The city field is in the User model as (#user.city) and the sport field can be either in CategorySport as (#category_sport.name) or in the Sport model as (#sport.name).
Right now I have the following to perform the search in the Player model:
def search
self.find(:all,:include => 'user',:conditions => ['users.city LIKE ?', "%#{city}%"])
end
I would like to know how would I add the join model (ability) and related (sport, categorysport) in this query in order to find by sport too. So, not just find for user city but also by sport.
Try this:
def search
self.includes(:user, :abilities => {:sport => :category_sports}).
where(['users.city LIKE ? OR category_sports.name=? OR sports.name=?',
"%#{city}%", sport, sport])
end

Rails ActiveRecord Query (Cross model)

I have an app that lets users input dates & interests that relate to those dates .I need to send them deals (a few days before the date - Via Email) that are based off of their interests and location. I have all the models setup and recording the data properly, just wondering how to query the models for the dates and then send the appropriate deal based off of the city and interests.
Notes:
*Each city and interest category has only 1 deal
*I have several different models for types of dates (Holidays, Occasions, Friends Birthdays ect).. all are pretty much identical in structure.
*All interests for each type of date are stored in person_interests.
Models:
Class User
belongs_to :province
belongs_to :city
has_many :friends_bdays
has_many :occasions
has_many :person_interests, :as => :person
has_many :interests, :through => :person_interests
has_many :user_holidays
has_many :holidays, :through => :user_holidays
has_many :anniversaries
end
class Deal < ActiveRecord::Base
belongs_to :interest
belongs_to :city
belongs_to :store
end
class Store < ActiveRecord::Base
has_many :deals
belongs_to :city
belongs_to :province
end
class PersonInterest < ActiveRecord::Base
belongs_to :interest
belongs_to :person, :polymorphic => true
end
class Interest < ActiveRecord::Base
has_many :person_interests
has_many :deals
end
class Occasion < ActiveRecord::Base
belongs_to :user
belongs_to :admin_user
has_many :person_interests, :as => :person
has_many :interests, :through => :person_interests
end
class Anniversary < ActiveRecord::Base
belongs_to :user
has_many :person_interests, :as => :person
has_many :interests, :through => :person_interests
end
class Friend_bday < ActiveRecord::Base
belongs_to :user
has_many :person_interests, :as => :person
has_many :interests, :through => :person_interests
end
You can achieve this using a variation of the solution below:
Install the squeel gem
class User
def deals(reload=false)
#deals = nil if
#deals ||= Deal.where{
( (:city => city_id) | ( :interest_id => interest_ids) ) &
:deal_date => (Time.now..3.days.from_now)
}
end
end
Now, user.deals returns the deals that will be active in next 3 days matching the user's city OR interests.
Edit 1:
Based on your comment it looks like you don't need the squeel gem. You can achieve what you want using regular AR syntax.
class User
def deals(reload=false)
#deals = nil if reload
#deals ||= Deal.where(
:city => city_id,
:interest_id => interest_ids,
:deal_date => (Time.now..3.days.from_now)
)
end
end

I need help with a Rails ActiveRecord Join - I know how to do it in SQL

I have 2 tables I need to join but the user_id column value is not the same in both tables. So I want to do something like this:
In my controller 4 will be substituted with current_user.id
select * from sites join pickups on sites.id = pickups.site_id where sites.user_id = '4'
But using an ActiveRecord Find.
Here are my associations:
class Site < ActiveRecord::Base
belongs_to :user
has_many :pickups
class Pickup < ActiveRecord::Base
belongs_to :site
belongs_to :user
class User < ActiveRecord::Base
has_one :profile
has_many :pickups
has_many :sites
Thanks in advance!
If you add this to your user model:
has_many :site_pickups, :through => :sites, :source => :pickups
You can do
current_user.site_pickups
try this:
sites = current_user.sites.find(:all, :include => [:pickup])