Efficient model traversal (joins) in Rails - ruby-on-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}}})

Related

How to create has_many association with "includes" and "where" conditions

Here is the situation: a store has many products, and can join multiple alliances. But the store owner may not want to display certain products in some alliances. For example, a store selling computers and mobile phones might want to display only phones in a "Phone Marts".
By default, all products in the store will be displayed in all alliances one shop joins. So I think building a product-alliance blacklist (I know it's a bad name...any ideas?) might be a convenient way.
The question is: how to create a "has_many" to reflect such relations like the behaviours of function shown_alliances and shown_products?
I need this because Car.includes(:shop, :alliances) is needed elsewhere, using functions make that impossible.
Product:
class Product < ActiveRecord::Base
belongs_to :store
has_many :alliances, -> { uniq }, through: :store
has_many :blacklists
def shown_alliances
alliances.where.not(id: blacklists.pluck(:alliance_id))
end
end
Store:
class Store < ActiveRecord::Base
has_many :products
has_many :alliance_store_relationships
has_many :alliances, through: :alliance_store_relationships
has_many :allied_products, -> { uniq }, through: :alliances, source: :products
end
Alliance:
class Alliance < ActiveRecord::Base
has_many :alliance_store_relationships
has_many :store, through: :alliance_store_relationships
has_many :products, -> { uniq }, through: :companies
has_many :blacklists
def shown_produtcts
#produtcts ||= produtcts.where.not(id: blacklists.pluck(:produtct_id))
end
end
Blacklist:
class Blacklist < ActiveRecord::Base
belongs_to :produtct
belongs_to :alliance
validates :produtct_id, presence: true,
uniqueness: { scope: :alliance_id }
end
class Product < ActiveRecord::Base
# ...
has_many :shown_alliances, ->(product) { where.not(id: product.blacklists.pluck(:alliance_id) }, class_name: 'Alliance', through: :store, source: :alliances
# ...
end
class Alliance < ActiveRecord::Base
# ...
has_many :shown_products, ->(alliance) { where.not(id: alliance.blacklists.pluck(:product_id) }, class_name: 'Product', through: :companies, source: :products
# ...
end
You can specify a condition for associations. See docs here

Rails 4 has_many :through association with :source

I was following the answer laid out at the link below to set up a many_to_many relationships on my Rails 4 app. (New to rails, here.)
Implement "Add to favorites" in Rails 3 & 4
I have Users and Exercises, and I want users to be able to have Favorite Exercises. I created a join table called FavoriteExercise with user_id and exercise_id as columns. I've got it populating, and it seems to be working fine, but I'm not able to use it to call directly to my favorites. 
Meaning, I want to type:
user.favorite = #list of exercises that have been favorited
I get this error when I try to load that list in my browser:
SQLite3::SQLException: no such column: exercises.favorite_exercise_id:
SELECT "exercises".* FROM "exercises" INNER JOIN "favorite_exercises"
ON "exercises"."favorite_exercise_id" = "favorite_exercises"."id"
WHERE > "favorite_exercises"."user_id" = ?
My models:
class User < ActiveRecord::Base
has_many :workouts
has_many :exercises
has_many :favorite_exercises
has_many :favorites, through: :favorite_exercises, source: :exercises
class Exercise < ActiveRecord::Base
belongs_to :user
has_many :workouts, :through => :exercises_workouts
has_many :favorites
has_many :favorited_by, through: :favorite_exercises, source: :exercises
class FavoriteExercise < ActiveRecord::Base
has_many :exercises
has_many :users
I just tried switching FavoriteExercise to 'belongs_to' instead of 'has_many, because it seems maybe that's the way that should go? but then I get this error:
uninitialized constant User::Exercises
Just trying to figure out how to set up the tables and associations so I can call .favorites on a user and get all their favorites.
If you want the list of exercises of the user and at the same, the list of favorite exercise of the user, then I think your join table should just be users_exercises wherein it will list all the exercises by the users. To list the favorite exercises, just add a boolean field indicating if the exercise is a user favorite and add a :scope to get all the favorite exercises.
So in your migration file:
users_exercises should have user_id, exercise_id, is_favorite
Then in your model:
class User < ActiveRecord::Base
has_many :workouts
has_many :users_exercises
has_many :exercises, through: :users_exercises
scope :favorite_exercises, -> {
joins(:users_exercises).
where("users_exercises.is_favorite = ?", true)
}
class Exercise < ActiveRecord::Base
has_many :workouts, :through => :exercises_workouts
has_many :users_exercises
has_many :users, through: :users_exercises
class UsersExercise < ActiveRecord::Base
belongs_to :exercise
belongs_to :user
You just need to simplify your model logic as follow:
class User < ActiveRecord::Base
has_many :workouts
has_many :exercises
has_many :favorite_exercises
has_many :favorites, through: :favorite_exercises, class_name: "Exercise"
class Exercise < ActiveRecord::Base
belongs_to :user
has_many :workouts, :through => :exercises_workouts
has_many :favorite_exercises
has_many :favorited_by, through: :favorite_exercises, class_name: "User"
class FavoriteExercise < ActiveRecord::Base
belongs_to :favorited_by
belongs_to :favorite
Then you can call user.excercises or excercise.users in your User/Excercise instance.
user.excercises = #list of exercises that have been favorited
Is that the many-to-many relationship you want?

Polymorphic Association with same model Ruby on Rails 4

So I have an app in which users can create cars. They can also like cars and I want to create an association between both. Cars that they created belong to them and Cars that they have liked belong to them through the context of liking them. To do this I have set up my associations as follows:
User Association:
class User < ActiveRecord::Base
has_many :cars
has_many :cars, -> {distinct}, through: :likes
end
Car Association:
class Car < ActiveRecord::Base
belongs_to :users
has_many :likes
has_many :users, -> { distinct }, through: :likes
end
Like Association:
class Like < ActiveRecord::Base
belongs_to :user
belongs_to :car
end
The problem is that before I had my user has_many cars through like relationship declared. I used to be able to call #user.cars and it would present the user's cars. Now it returns the collection of the cars the user has liked. I need methods for each collection.
When I try: User.likes.cars
I get a
No Method error
and the console log looks through the likes records and still doesn't return the cars even though my likes records have a car_id field.
I have looked at a bunch of questions but have trouble understanding them. I've also tried to define methods in the model and nothing is seeming to work. Any help is appreciated.
How would I be able to change my associations so I can have a for query both User.cars (for cars the user has created) and User.likes.cars (for cars the user has liked)?
So the below answer from Oleg didn't work exactly but led me in the right direction. Thank you! I started by following the above example and doing:
class User < ActiveRecord::Base
has_many :cars
has_many :car_likes, -> {distinct}, class_name: 'Car', through: :likes
end
class Car < ActiveRecord::Base
belongs_to :users
has_many :likes
has_many :user_likes, -> { distinct }, class_name: 'User', through: :likes
end
This returned the following error in console:
ActiveRecord::HasManyThroughSourceAssociationNotFoundError: Could not find the source association(s) "car_likes" or :car_like in model Like. Try 'has_many :car_likes, :through => :likes, :source => '. Is it one of user or car?
So I changed it to:
class User < ActiveRecord::Base
has_many :cars
has_many :car_likes, -> {distinct}, through: :likes, source: :cars
end
Car Association:
class Car < ActiveRecord::Base
belongs_to :users
has_many :likes
has_many :user_likes, -> { distinct }, through: :likes, source: :users
end
It nows works for both models! Thanks and hopefully this is helpful to someone else with the same problem.
has_many :cars, -> {distinct}, through: :likes overrides has_many :cars because it redefines User.cars. Try the following:
class User < ActiveRecord::Base
has_many :cars
has_many :car_likes, -> {distinct}, class_name: 'Car', through: :likes
end
Car Association:
class Car < ActiveRecord::Base
belongs_to :users
has_many :likes
has_many :user_likes, -> { distinct }, class_name: 'User', through: :likes
end
#To get them, instead of user.likes.cars
#user.car_likes
#car.user_likes
Please let me know if the problem persists. There might be another error.
I don't see where you are defining any model as polymorphic.
In the past I have done something like this.. actually I did this for tags/taggings and made "like" a tag a user applied to another instance. This is an ad-hoc modification and I may have missed something but it's a pretty common use case for polymorphic associations.
class Like < ActiveRecord::Base
belongs_to :likeable, polymorphic: true
...
end
class Liking < ActiveRecord::Base
belongs_to :like
belongs_to :likeable, :polymorphic => true
end
class User < ActiveRecord::Base
has_many :likings, :as => :likeable
has_many :likes, -> { order(created_at: :desc) }, :through => :taggings
end

How do I properly alias a has_many through a join in Rails ActiveRecord?

Rails/ActiveRecord newbie here. Consider the following models for a Classroom, User and ClassroomEnrollments (join between the two)
class Classroom < ActiveRecord::Base
has_many :fulltime_enrollments, -> { where(duration: 'full-time') }, class_name: "ClassroomEnrollments"
has_many :fulltimers, :through => :fulltime_enrollments, class_name: "User"
has_many :parttime_enrollments, -> { where(duration: 'part-time') }, class_name: "ClassroomEnrollments"
has_many :parttimers, :through => :parttime_enrollments, class_name: "User"
end
class ClassroomEnrollment < ActiveRecord::Base
# columns: user_id, classroom_id, duration
belongs_to :user
belongs_to :classroom
end
class User < ActiveRecord::Base
has_many :classroom_enrollments
has_many :classrooms, :through => :classroom_enrollments
end
The following model for a classroom and classroom_enrollments does not work. Specifically the :fulltimers and :parttimers aliases throw undefined method 'to_sym' for nil:NilClass errors when I try to access them via my_classroom.fulltimers or my_classroom.parttimers.
If I remove the :parttimers alias and rename :fulltimers to :users it works fine (and displays only the full time students), so it seems to me that it has something to do with it figuring out that :fulltimers is of type User, even though I've specified the classname: "User" in the has_many condition.
What am I doing wrong?
Since the source association cannot be inferred automatically, you need specify it using the :source option:
class Classroom < ActiveRecord::Base
has_many(
:fulltime_enrollments,
-> { where(duration: 'full-time') },
class_name: "ClassroomEnrollments"
)
has_many :fulltimers, :through => :fulltime_enrollments, :source => :user
has_many(
:parttime_enrollments,
-> { where(duration: 'part-time') },
class_name: "ClassroomEnrollments"
)
has_many :parttimers, :through => :parttime_enrollments, :source => :user
end
http://guides.rubyonrails.org/association_basics.html#options-for-has-many-source
How about trying a cleaner, more readable approach? Something like this:
class Classroom < ActiveRecord::Base
has_many :classroom_enrollments
has_many :users, through: :classroom_enrollments
def full_timers
users_by_duration("full-time")
end
def part_timers
users_by_duration("part-time")
end
private
def users_by_duration(duration)
users.where(classroom_enrollments: { duration: duration })
end
end
Then:
my_classroom = Classroom.find(1)
my_classroom.full_timers
I stumbled on this while working on something similar. This is will generate the same sql and is a bit easier to look at.
class Classroom < ActiveRecord::Base
has_many :classroom_enrollments
has_many :users, through: :classroom_enrollments
def fulltimers
users.merge(ClassroomEnrollment.full_time)
end
def parttimers
users.merge(ClassroomEnrollment.part_time)
end
end
class ClassroomEnrollment < ActiveRecord::Base
belongs_to :user
belongs_to :classroom
scope :fulltime, ->{ where(duration: 'full-time') }
scope :parttime, ->{ where(duration: 'part-time') }
end
class User < ActiveRecord::Base
has_many :classroom_enrollments
has_many :classrooms, :through => :classroom_enrollments
end

Nested Attributes in Rails

Imagine 4 models in Rails 3.1
class Student < ActiveRecord::Base
has_many :memberships
has_many :courses, :through => :memberships
has_many :tests, :through => :courses
end
class Membership < ActiveRecord::Base
belongs_to :student
belongs_to :course
end
class Course < ActiveRecod::Base
has_many :tests
has_many :students, :through => :memberships
end
class Test < ActiveRecord::Base
belongs_to :course
end
How can I output a sorted list (ie by date) of a student's upcoming tests
(I'm guessing there is a fairly simple answer, but I've been trying in vain for a while)
My best guess is something like:
#upcomingTests = #currstudent.tests.sort_by &:testDateTime
but it seems to return an empty array
First of all, there is an slight error on your model "Course". It needs "belongs_to :student".
class Course < ActiveRecod::Base
has_many :tests
has_many :students, :through => :memberships
belongs_to :student
end
After you've created and populated a foreign key, you can create a simple named_scope on your test mode:
named_scope :ordered, :order => "created_at DESC"
Then it's just the matter of accessing it from wherever you want:
#ordered_tests = #student.tests.ordered

Resources