How can I set up conditional associations in Rails? - ruby-on-rails

So I have two associated models Users and Magazines. They are associated by a has_many :through relationship via Subscriptions. So this is what it looks like:
class User < ActiveRecord::Base
has_many :subscriptions
has_many :magazines, :through => :subscriptions
end
class Subscription < ActiveRecord::Base
belongs_to :user
belongs_to :magazine
end
class Magazine < ActiveRecord::Base
has_many :subscriptions
has_many :users, :through => :subscriptions
end
Users have a boolean attribute called paid. I only want users where paid == true to be able to subscribe to any magazines. How can I set up a conditional association like this? Thanks in advance for the help!

For the associations, you can pass a lambda before the hash options. In this lambda you can specify conditions, ordering, etc.
class Magazine < ActiveRecord::Base
has_many :subscriptions
has_many :users, ->{ where(users: {paid: true}) }, through: :subscriptions
end
For users, you could do something like this:
class User < ActiveRecord::Base
has_many :subscriptions
has_many :magazines, through: :subscriptions
def magazines
self.paid ? super : Magazine.none
end
end
While it's neat, the above might behave unpredictably when you're joining tables e.g. User.joins(:magazines) might ignore the paid condition or crash.
A better alternative:
class User < ActiveRecord::Base
has_many :subscriptions
has_many :magazines,
->{ where("subscriptions.user_id IN (
SELECT id FROM users WHERE users.paid = 't')") },
through: :subscriptions
end
That where in the lambda might be replaceable by a joins like so:
joins(subscriptions: :user).where(users: {paid: true})
But I think that'll join subscriptions twice - once for the lambda and once for the association. I'm not certain. If so, and if that bothers you, then:
joins("INNER JOIN users ON (users.id = subscriptions.user_id)").
where(users: {paid: true})
or:
joins("INNER JOIN users ON (
(users.id = subscriptions.user_id) AND (users.paid = 't')
)")
Also, I think you mislabeled your Subscription model as Registration

Related

Extracting data using rails query from a join table

I have users table, books table and books_users join table. In the users_controller.rb I am trying extract the users who have filtered_books. Please help me to resolve that problem.
user.rb
has_many :books_users, dependent: :destroy
has_and_belongs_to_many :books, join_table: :books_users
book.rb
has_and_belongs_to_many :users
books_user.rb
belongs_to :user
belongs_to :book
users_controller.rb
def filter_users
#filtered_books = Fiction.find(params[:ID]).books
#users = **I want only those users who have filtered_books**
end
has_and_belongs_to_many does not actually use a join model. What you are looking for is has_many through:
class User < ApplicationRecord
has_many :book_users
has_many :books, through: :book_users
end
class Book < ApplicationRecord
has_many :book_users
has_many :users, through: :book_users
end
class BookUser < ApplicationRecord
belongs_to :book
belongs_to :user
end
If you want to add categories to books you would do it by adding a Category model and another join table. Not by creating a Fiction model which will just create a crazy amount of code duplication if you want multiple categories.
class Book < ApplicationRecord
has_many :book_users
has_many :users, through: :book_users
has_many :book_categories
has_many :categories, through: :book_categories
end
class BookCategory < ApplicationRecord
belongs_to :book
belongs_to :category
end
class Category < ApplicationRecord
has_many :book_categories
has_many :books, through: :book_categories
end
If you want to query for users that follow a certain book you can do it by using an inner join with a condition on books:
User.joins(:books)
.where(books: { title: 'Lord Of The Rings' })
If you want to get books that have a certain category:
Book.joins(:categories)
.where(categories: { name: 'Fiction' })
Then for the grand finale - to query users with a relation to at least one book that's categorized with "Fiction" you would do:
User.joins(books: :categories)
.where(categories: { name: 'Fiction' })
# or if you have an id
User.joins(books: :categories)
.where(categories: { id: params[:category_id] })
You can also add an indirect association that lets you go straight from categories to users:
class Category < ApplicationRecord
# ...
has_many :users, though: :books
end
category = Category.includes(:users)
.find(params[:id])
users = category.users
See:
The has_many :through Association
Joining nested assocations.
Specifying Conditions on Joined Tables
From looking at the code i am assuming that Book model has fiction_id as well because of the has_many association shown in this line Fiction.find(params[:ID]).books. There could be two approaches achieve this. First one could be that you use #filtered_books variable and extract users from it like #filtered_books.collect {|b| b.users}.flatten to extract all the users. Second approach could be through associations using fiction_id which could be something like User.joins(:books).where(books: {id: #filtererd_books.pluck(:id)})

Efficient model traversal (joins) in Rails

I have a rails app with a set of relations. Users read and rank books, and organizations are collections of users, who collectively have a list of books they've read/rated:
class Organization
has_many :users, through: memberships
end
class Membership
belongs_to :organization
belongs_to :user
end
class User
has_many :books, through: :readings
has_many :organizations, through: :memberships
end
class Readings
belongs_to :user
belongs_to :book
end
class Book
has_many :readings
end
I would like to, in one query, find all the books that an organization has read and rated. Something like:
organization.members.books
I would ideally like to use this with will_paginate and sort by the ratings on the Readings class. Any idea how to do this without custom SQL?
Try the following relations:
class Organization < ActiveRecord::Base
has_many :memberships
has_many :users, through: :memberships
has_many :books, -> { uniq }, through: :users
end
class Membership < ActiveRecord::Base
belongs_to :organization
belongs_to :user
end
class User < ActiveRecord::Base
has_many :memberships
has_many :readings
has_many :organizations, through: :memberships
has_many :books, through: :readings
end
class Reading < ActiveRecord::Base
belongs_to :user
belongs_to :book
end
class Book < ActiveRecord::Base
has_many :readings
has_many :users, through: :readings
has_many :organizations, -> { uniq }, through: :users
end
Now you can call #organization.books to get all books for a specific organization.
I don't know exactly how you handle ratings, but you could add a scope called rated to your Book model and then call #organization.books.rated to get all rated books for a specific organization. Here is an example of what that scope might look like:
class Book < ActiveRecord::Base
has_many :readings
has_many :users, through: :readings
has_many :organizations, through: :users
scope :rated, -> { where.not(rating: nil) }
scope :rated_above, ->(rating) { where('rating >= ?', rating) }
scope :rated_below, ->(rating) { where('rating <= ?', rating) }
end
That is just an example assuming you use some integer based rating system where a nil rating means it is unrated. I also threw in the rated_above and rated_below scopes, which you may or may not find useful. You could use them like #organization.books.rated_above(6) to only get the books with a rating greater than or equal to 6. Again, these are just examples, you might need to change them to work with your rating implementation.
Update
In the case where your ratings are stored on the Reading model, you can change your Book model to the following:
class Book < ActiveRecord::Base
has_many :readings
has_many :users, through: :readings
has_many :organizations, -> { uniq }, through: :users
scope :rated, -> { with_ratings.having('COUNT(readings.rating) > 0') }
scope :rated_above, ->(rating) { with_ratings.having('average_rating >= ?', rating) }
scope :rated_below, ->(rating) { with_ratings.having('average_rating <= ?', rating) }
private
def self.with_readings
includes(:readings).group('books.id')
end
def self.with_ratings
with_readings.select('*, AVG(readings.rating) AS average_rating')
end
end
I am not sure if there is a simpler approach, but it gets the job done. Now the scopes should work as expected. Additionally, you can sort by rating like this: #organization.books.rated.order('average_rating DESC')
This should give you all the books that an organization has read in one query. Not sure how your ratings are implemented.
Book.joins(:readings => {:user => :memberships})
.where(:readings => {:users => {:memberships => {:organization_id => #organization.id}}})

Rails find by has_many :through

I am looking for a way to query a model based on children in a has_many through association.
I have 3 models:
class Conversation < ActiveRecord::Base
has_many :conversations_participants
has_many :participants, through: :conversations_participants
end
class ConversationsParticipant < ActiveRecord::Base
belongs_to :conversation
belongs_to :participant, class_name: 'User'
end
class User < ActiveRecord::Base
has_many :conversations_participants
has_many :conversations, through: :conversations_participants
end
I need to find conversations where participants matches an array of ids.
This is what i have at the moment (not working):
Conversation.includes(:participants).where(participants: params[:participants])
Sounds like you just want the conversations, if so you can joins.
Conversation.joins(:participants).where(:users => { :id => params[:participants] } )
Otherwise, if you want to eager load the participants, use includes
Conversation.includes(:participants).where(:users => { :id => params[:participants] } )
You can pass an array like this:
Conversation.includes(:participants).where(:id => params[:participants])
Assuming that params[:participants] is an array.
Conversation.includes(:participants).where( 'conversation_participants.participant_id' => params[:participants])
assuming that participant_id is the foreign key of participants in the conversation_participants table

Adding a Rating to each User per category on Ruby on Rails

Right now I'm building a social media app, where i want an user to have a rating per category, how would the association go? The way it needs to be setup it's Each user will have a different rating in each category.
I'm think that
belongs_to :user
belongs_to :category
in the UserCategoryRating model.
and
has_many :user_category_ratings, through => :category
on the User model, Is this the correct approach?
The UserCategoryRating table has the User_id column, Category_id column, and the rating column, that updates each time an user gets votes (The rating it's just the AVG between votes and the score based on 1-5)
UPDATE: If I'm understanding you correctly, here is a diagram of the simple design you'd like:
And this would be the basic skeleton of your classes:
class User < ActiveRecord::Base
has_many :ratings
# has_many :categories, :through => :ratings
end
class Category < ActiveRecord::Base
has_many :ratings
# has_many :users, :through => :ratings
end
class Rating < ActiveRecord::Base
belongs_to :user
belongs_to :category
validates_uniqueness_of :user_id, :scope => [:category_id]
end
Will allow for these query:
#category_ratings_by_user = Rating.where("ratings.user_id = ? AND ratings.category_id = ?", user_id, category_id)
#specific_rating = user.ratings.where("ratings.category_id = ?", category_id)
# make nice model methods, you know the deal
# ... if you added the has_many :through,
#john = User.find_by_name("john")
# Two ways to collect all categories that john's ratings belong to:
#johns_categories_1 = #john.ratings.collect { |rating| rating.category }
#johns_categories_2 = #john.categories
#categories_john_likes = #john.categories.where("categories.rating >= ?", 7)
I'm just unsure as to why you want this has_many, :through (this doesn't seem like a many to many -- a rating only belongs to one user, correct?).
I will use the following data model:
class User
has_many :user_categories
has_many :categories, :through => :user_categories
end
class UserCategory
belongs_to :user
belongs_to :category
# this model stores the average score also.
end
class Category
has_many :user_categories
has_many :users, :through => :user_categories
end
Now when you want to update the score of a user for a category
uc = u.user_categories.find_by_category_id(id)
uc.score = score
uc.save

Linking Two Models Together in Ruby on Rails

I've been stuck on this all day. I have a setup like the one below. I'm trying to define friends using the group_memberships association.
class User < ActiveRecord::Base
has_many :group_memberships
has_many :groups, :through => :group_memberships
has_many :friends # what goes here? <<
end
class GroupMembership < ActiveRecord::Base
belongs_to :user
belongs_to :role
belongs_to :group
end
class Role < ActiveRecord::Base
has_many :group_memberships
end
class Group < ActiveRecord::Base
has_many :group_memberships
has_many :users, :through > :group_memberships
end
I'd like to do this without creating a join table for friends, unless it's completely crazy to do it without.
The group_membership table contains user_id and group_id linking one user to one group.
I'd trying to get
#user.friends
to return users with common group_memberships using the group_id.
has_many :friends, :through => :group_memberships, :source => :group
Nothing I've tried works, but I'll chalk that up to my complete misunderstanding of the above code.
Unfortunately Rails doesn't let you nest has_many's more than 2 deep.. Forgetting about naming it friends for a moment (let's call it users instead), this would theoretically be what you'd want:
has_many :group_memberships
has_many :groups, :through => :group_memberships
has_many :users, :through => groups
Except that this doesn't work. If you try it you'll see this not-so-helpful error message which comes from this bit of code, specifically source_reflection.options[:through].nil?. That is, the through isn't allowed to have a through itself.
Instead, you may want to do something like this:
Solution 1
class User < ActiveRecord::Base
has_many :group_memberships
has_many :groups, :through => :group_memberships
def friends
groups.with_users.map(&:users).flatten.uniq.reject{|u| u == self}
end
end
class Group < ActiveRecord::Base
has_many :group_memberships
has_many :users, :through => :group_memberships
named_scope :with_users, :include => :users
end
Solution 2
Use the nested_has_many_through plugin that Radar mentioned. It looks like at least one fork of it on github has been updated to work on the latest Rails.
Solution 3 (just for kicks)
or, just for kicks, you could do it with one big SQL query:
class User < ActiveRecord::Base
has_many :group_memberships
has_many :groups, :through => :group_memberships
def friends
sql = <<-SQL
SELECT users.* FROM users, (
SELECT DISTINCT gm2.user_id AS user_id
FROM group_memberships gm, groups g, group_memberships gm2
WHERE gm.user_id = ? AND g.id = gm.group_id AND gm2.group_id = g.id AND gm2.user_id != ?
) AS user_ids
WHERE users.id = user_ids.user_id
SQL
User.find_by_sql([sql, id, id])
end
end
Use the nested_has_many_through plugin.
delegate :users, :to => 'group'

Resources