I have the following models:
class User < ActiveRecord::Base
has_many :subscriptions, as: :subscribable
has_many :user_to_high_school_subscriptions
has_many :high_school_subscriptions, through: :user_to_high_school_subscriptions
def all_subscriptions
self.subscriptions + self.high_school_subscriptions.subscriptions
end
end
class UserToHighSchoolSubscription < ActiveRecord::Base
belongs_to :user
belongs_to :high_school_subscription
end
class HighSchoolSubscription < ActiveRecord::Base
has_many :user_to_high_school_subscriptions
has_many :users, through: :user_to_high_school_subscriptions
has_many :subscriptions, as: :subscribable
end
class Subscription < ActiveRecord::Base
belongs_to :subscribable, polymorphic: true
end
Is there a clever way for me to get ALL Subscriptions that a User has.
I tried
u = User.first
subs = u.all_subscriptions
but that is erroring out (undefined method subscriptions' for #<ActiveRecord::Relation:). I think it's choking when I try to use the has_many :subscriptions on the HighSchoolSubscription because a user has_many :high_school_subscriptions. (This line: self.high_school_subscriptions.subscriptions).
Is there a way to aggregate has_many on a has_many in Rails?
Running rails 3.2.1
self.subscriptions does not return an Array but ActiveRecord::Relation. That's the reason why the + methods and does not work and your get the mentioned error. The simplest fix is to do it this way:
def all_subscriptions
self.subscriptions.all + self.high_school_subscriptions.all.collect { |hss| hss.subscriptions.all }.flatten
end
The all method will trigger the database queries and return an array. Because a user might have many high school subscriptions and these also may has many subscriptions, you have to iterate over all high school subscriptions and collect their subscriptions. As you can see, this is a complete overkill.
Redesign your data model or just do it a different way.
Perhaps, scoping the Subscription model might be the way. Add it an attribute that would specify what kind of subscription it is and then you can completely remove the HighSchoolSubscription model.
Related
I am working on an app where users have many quizzes and quizzes can have many users. I have set the relationships:
class User < ApplicationRecord
has_many :studies
has_many :quizzes, through: :studies
end
class Quiz < ApplicationRecord
has_many :studies
has_many :users, through: :studies
end
class Study < ApplicationRecord
belongs_to :user
belongs_to :quiz
end
I have a field in the Study table to store the score that the user made on the quiz, but I am unable to access the field. I have tried #quiz.studies.score and #quiz.study.score but Rails give me an undefined method. How to I access the field in a join model of a has_many though relationship?
#quiz.studies return the collection of studies. So you have to use first, last, each to get the score of the specific studies.
Try this:
#quiz.studies.first.score
Although similar questions have already been asked:
counter_cache with has_many :through
dependent => destroy on a "has_many through" association
has_many :through with counter_cache
none of them actually addresses my issue.
I have three models, with a has_many :through association :
class User < ActiveRecord::Base
has_many :administrations
has_many :calendars, through: :administrations
end
class Calendar < ActiveRecord::Base
has_many :administrations
has_many :users, through: :administrations
end
class Administration < ActiveRecord::Base
belongs_to :user
belongs_to :calendar
end
The join Administration model has the following attributes:
id
user_id
calendar_id
role
I would like to count how many calendars each user has and how many users each calendar has.
I was going to go with counter_cache as follows:
class Administration < ActiveRecord::Base
belongs_to :user, counter_cache: :count_of_calendars
belongs_to :calendar, counter_cache: :count_of_users
end
(and, of course, the corresponding migrations to add :count_of_calendars to the users table and :count_of_users to the calendars table.)
But then, I stumbled upon this warning in Rails Guides:
4.1.2.4 :dependent
If you set the :dependent option to:
:destroy, when the object is destroyed, destroy will be called on its associated objects.
:delete, when the object is destroyed, all its associated objects will be deleted directly from the database without calling their
destroy method.
You should not specify this option on a belongs_to association that is
connected with a has_many association on the other class. Doing so can
lead to orphaned records in your database.
Therefore, what would be a good practice to count how many calendars each user has and how many users each calendar has?
Well, dependent: :destroy will destroy the associated records, but it won't update the counter_cache, so you may have wrong count in counter_cache. Instead you can implement a callback that will destroy the associated records, and update your counter_cache.
class Calendar < ActiveRecord::Base
has_many :administrations
has_many :users, through: :administrations
before_destroy :delete_dependents
private
def delete_dependents
user_ids = self.user_ids
User.delete_all(:calendar_id => self.id)
user_ids.each do |u_id|
Calendar.reset_counters u_id, :users
end
end
end
And similarly, implement this for User model too
I have two AR models and a third has_many :through join model like this:
class User < ActiveRecord::Base
has_many :ratings
has_many :movies, through: :ratings
end
class Movie < ActiveRecord::Base
has_many :ratings
has_many :users, through: :ratings
end
class Rating < ActiveRecord::Base
belongs_to :user
belongs_to :movie
after_destroy do
puts 'destroyed'
end
end
Occasionally, a user will want to drop a movie directly (without directly destroying the rating). However, when I do:
# puts user.movie_ids
# => [1,2,3]
user.movie_ids = [1, 2]
the rating's after_destroy callback isn't called, although the join record is deleted appropriately. If I modify my user model like this:
class User < ActiveRecord::Base
has_many :ratings
has_many :movies,
through: :ratings,
before_remove: proc { |u, m| Rating.where(movie: m, user: u).destroy_all }
end
Everything works fine, but this is really ugly, and Rails then tries to delete the join model a second time.
How can I use a dependent: :destroy strategy for this association, rather than dependent: :delete?
Answering my own question, since this was difficult to Google, and the answer is super counter-intuitive (although I don't know what the ideal interface would be).
First, the situation is described thoroughly here: https://github.com/rails/rails/issues/7618. However, the specific answer is buried about halfway down the page, and the issue was closed (even though it is still an issue in current Rails versions).
You can specify dependent: :destroy for these types of join model destructions, by adding the option to the has_many :through command, like this:
class User < ActiveRecord::Base
has_many :ratings
has_many :movies,
through: :ratings,
dependent: :destroy
end
This is counter-intuitive because in normal cases, dependent: :destroy will destroy that specific association's object(s).
For example, if we had has_many :ratings, dependent: :destroy here, all of a user's ratings would be destroyed when that user was destroyed.
We certainly don't want to destroy the specific movie objects here, because they may be in use by other users/ratings. However, Rails magically knows that we want to destroy the join record, not the association record, in this case.
I have two models Activities and Users
class Activity < ActiveRecord::Base
has_and_belongs_to_many :users
end
class User < ActiveRecord::Base
has_and_belongs_to_many :activities
end
I can load all the activities which are associated with users by calling
User.all.includes(:activities)
But this actually pulls up all my activity objects from the database, which can cause performance problems as the table is quite large. My requirement is only to get the activity_ids associated
I can directly get that by executing the plain sql on the join table
select activity_id from activities_users where user_id in (...)
My question is:
Can I do get the above functionality in a rails friendly way
From has_and_belongs_to_many you could do things like #User.find(1).activity_ids
In terms of including them for a collection to avoid an n+1 query I don't think you can do it any way other than the way you've currently got it.
If you make it a has_many_through it'll have a model:
class Activity < ActiveRecord::Base
has_many :user_activities
has_many :users, through: :user_activities
end
class User < ActiveRecord::Base
has_many :user_activities
has_many :activities, through: :user_activities
end
class UserActivity < ActiveRecord::Base
belongs_to :user
belongs_to :activity
end
Then you can do #users = User.includes(:user_activities).all so when you loop over them and do something like it should avoid the n+1 query because you've already loaded them in.
#users.each do |user|
user.activity_ids
end
Just ran into an issue with a has_many :through association and after/before-destroy callbacks not being triggered.
Say, I have users, groups, and an intermediate relation called membership.
I have a form that allows users to be enrolled into groups by creating a new membership record when they check off associated checkboxes. Basically an array of group_ids.
Looks something like this:
Which group would you like to join? (check all that apply)
[] Group A
[] Group B
[] Group C
And I wish to record actions such as joining a group or leaving a group to activity log table and do some other less important thigns.
I have the following defined:
class Group < AR::Base
has_many :memberships
has_many :users, :through => :memberships
end
class Membership < AR::Base
belongs_to :user
belongs_to :group
after_create :log_event_to_audit_table
after_destroy :log_event_to_audit_table
end
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, :through => :memberships
attr_accessible :group_ids # enables mass-assignment
end
When a new membership record is created the after_create is run as expected. However, the after_destroy does not get triggered!
After google-ing and read up the docs I discovered the reason why:
"Automatic deletion of join models is
direct, no destroy callbacks are
triggered" - from Ruby Guides.
Hmmmmmm...
So the join model's (in this case Membership's) destroy callbacks are not being triggered. Well that's a downer. Any reason as to why?
So my question what is the best way to work around this issue?
Should I define my own membership_ids= method in User model that calls membership.destroy directly?
Open to any suggestions about the best practices in such a scenario.
Thanks!
After carefully examining the API docs, it turns out has_many and has_and_belongs_to_many ("HABTM") have a few options just for this case:
before_add
after_add
before_remove
after_remove
class User < ActiveRecord::Base
has_many :groups, :through => :memberships, :after_remove => :your_custom_method
end
Judging by how many responses I got, this must not be a very well documented/used feature.
Just noting it here for myself and others who may stumble like I did.
I've struggled with the same problem recently and solved it by extending association and overriding its delete method:
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, :through => :memberships do
def delete(*args)
groups = args.flatten
# destroy memberships in order to trigger their callbacks:
proxy_association.owner.memberships.where(group_id: groups).destroy_all
super
end
end
...
end
adding dependent: :destroy to the has many relationship actually calls the before_destroy and after_destroy methods in the Membership class.
class User < ActiveRecord::Base
has_many :groups, through: :memberships, dependent: :destroy
end