Accessing associated objects through a specific table - ruby-on-rails

I have the following:
class User < ActiveRecord::Base
has_many :saved_courses, dependent: :destroy
has_many :courses, through: :saved_courses
has_many :course_completions
has_many :courses, through: :course_completions
end
And its course counterpart:
class Course < ActiveRecord::Base
has_many :course_completions
has_many :users, through: :course_completions
has_many :saved_courses, dependent: :destroy
has_many :users, through: :saved_courses
end
Based on these relationships, how do I get an array of all of the courses that a #user has saved?
As in, when I do #user.courses, I want it grab its saved courses, but not completed courses. I tried to do #user.saved_courses.courses but that doesn't seem like a valid operation. And #user.saved_courses.to_a simply returns an array of saved_courses, whereas I need to go a step further from there and grab the courses that those saved_course items represent.
Bonus points: I'm trying to do this in the shortest, most elegant way possible. For example, I don't want to manually iterate over #user.saved_courses.to_a, find the courses and push them into an array.

Having two associations named the same thing isn't going to work. You're going to need to do something the effect of
class User < ActiveRecord::Base
has_many :saved_courses, dependent: :destroy
has_many :saved_course_courses, through: :saved_courses, source: :course
has_many :course_completions, dependent: :destroy
has_many :course_completion_courses, through: :course_completions, source: :course
end
Note the use of :source, which tells ActiveRecord which class the has_many is referring to. Then when you're going through and want all of a user's courses, you would do: user.saved_course_courses + user.course_completion_courses

Related

Destroying entities through many to many relationship in rails

I have 3 models.
Story, Tag and Tagging.
A Story has many tags through taggings. Here are my models:
**Story.rb**
class Story < ApplicationRecor
has_many :taggings, dependent: :delete_all
has_many :tags, through: :taggings
end
**Tagging.rb**
class Tagging < ApplicationRecord
belongs_to :tag
belongs_to :story
end
**Tag.rb**
class Tag < ApplicationRecord
has_many :taggings
has_many :stories, through: :taggings
end
So when I delete a story, I have dependent: :delete_all on taggings which calls a single SQL delete statement on all taggings associated with a Story. I'd like to also delete all Tags if there are no longer any taggings associated to it. For example, a story has 1 tagging and one tag through the tagging. When I delete that story, that story and tagging are deleted but that tag still remains. I'd like that tag to be removed as well since there are no taggings associated to that tag anymore.
I've tried this:
**Story.rb**
has_many :taggings, dependent: :destroy
has_many :tags, through: :taggings
and this:
**Story.rb**
has_many :taggings, dependent: :destroy
has_many :tags, through: :taggings, dependent: :destroy
Both doesnt work.. Any advice on how to handle this?
You can check the tags using an after_destroy
class Tagging < ApplicationRecord
after_destroy :destroy_unused_tag
private
def destroy_unused_tag
tag.destroy if tag.taggings.empty?
end
end
Your Tagging model might be missing dependant option for tags.
**Story.rb**
has_many :taggings, dependent: :destroy
has_many :tags, through: :taggings
**Tagging.rb**
belongs_to :story
belongs_to :tags, dependent: :destroy

Multiple polymorphic associations on the same object

I have a Flag model which is joined to multiple other objects with FlagInstance and a polymorphic flaggable on that table:
table 'flag_instances'
flag_id
flaggable_id
flaggable_type
.....
With has many_through I'm able to fetch any flaggable object like user.flags which is great.
However I'm trying to flag objects with errors and the notify other objects so I've added
table 'flag_instances'
flag_id
flaggable_id
flaggable_type
notifiable_id
notifiable_type
.....
The problem is, a User can have a flag and can be notified of a flag. So user.flags isn't specific enough to show me which is a flag and which is a notification of a flag.
I think I need to change the relationships:
user.rb
has_many :flag_instances, as: :flaggable, dependent: :destroy
has_many :flags, through: :flag_instances
has_many :flag_instances, as: :notifiable, dependent: :destroy
has_many :flags, through: :flag_instances
But I'm not sure what to change them to. Can someone please suggest a solution?
Note: both flags and notifications of flags can belong to multiple objects, so they both need to remain polymorphic.
Thanks
Association for notifiable needs to be changed. In this case user.rb:
has_many :flag_instances, as: :flaggable, dependent: :destroy
has_many :flags, through: :flag_instances
has_many :notifiables, as: :notifiable, dependent: :destroy, class_name: 'FlagInstance'
has_many :notifications, through: :notifiables, class_name: 'Flag'
Note: You might also need to provide foreign_key in case Rails association is not able to pick up the key itself.
Each association must have a unique name - otherwise the later definition will just overwrite the former.
Here the third line overwrites the first line:
has_many :flag_instances, as: :flaggable, dependent: :destroy
has_many :flags, through: :flag_instances
has_many :flag_instances, as: :notifiable, dependent: :destroy
To reference the correct associations we would need to setup the user model as so:
class User < ApplicationRecord
has_many :flag_instances_as_flaggable,
as: :flaggable
class_name: 'FlagInstance'
has_many :flags_as_flaggable,
through: :flag_instances_as_flaggable,
source: :flag
has_many :flag_instances_as_notifiable,
as: :notifiable
class_name: 'FlagInstance'
has_many :flags_as_notifiable,
through: :flag_instances_as_notifiable,
source: :flag
end
In your case you might want to use concerns to keep it dry:
module Flaggable
extend ActiveSupport::Concern
included do
has_many :flag_instances_as_flaggable,
as: :flaggable,
class_name: 'FlagInstance'
has_many :flags_as_flaggable,
through: :flag_instances_as_flaggable,
source: :flag
end
end
module Notifiable
extend ActiveSupport::Concern
included do
has_many :flag_instances_as_notifiable,
as: :notifiable,
class_name: 'FlagInstance'
has_many :flags_as_notifiable,
through: :flag_instances_as_notifiable,
source: :flag
end
end
class User < ApplicationRecord
include Flaggable
include Notifiable
end

Is the first has_many redundant? - Ruby on Rails Active records associations

Rails 4.2 newbie:
2 Questions;
1) Is the first has_many redundant? Since its name is a plural of Save Class?
can I have only:
has_many :savers, through: :saves, source: :saver
Or even better;
has_many :savers, through: :saves
If the answer is yes, where can I set "dependent: :destroy"?
class Post < ActiveRecord::Base
belongs_to :user
has_many :saves, class_name: "Save", foreign_key: "saver_id", dependent: :destroy
has_many :savers, through: :saves, source: :saver
end
class Save < ActiveRecord::Base
belongs_to :saver, class_name: "User"
validates :saver_id, presence: true
end
class User < ActiveRecord::Base
has_many :posts, dependent: :destroy
...
end
2) This is the typical blog model, where user can 'save' posts posted by another user to their timeline. Does this model make use best practices? Specially in db performance, doing a Join to get posts saved by a User. The 'Save' table that will have 100MM rows?
Lets first alter your example a bit to make the naming less confusing:
class User
has_many :bookmarks
has_many :posts, through: :bookmarks
end
class Post
has_many :bookmarks
has_many :users, through: :bookmarks
end
class Bookmark
belongs_to :user
belongs_to :post
end
Lets have a look at the query generated when we do #user.posts
irb(main):009:0> #user.posts
Post Load (0.2ms) SELECT "posts".* FROM "posts" INNER JOIN "bookmarks" ON "posts"."id" = "bookmarks"."post_id" WHERE "bookmarks"."user_id" = ? [["user_id", 1]]
=> #<ActiveRecord::Associations::CollectionProxy []>
Now lets comment out has_many :bookmarks and reload:
class User
# has_many :bookmarks
has_many :posts, through: :bookmarks
end
irb(main):005:0> #user.posts
ActiveRecord::HasManyThroughAssociationNotFoundError: Could not find the association :bookmarks in model User
So no, the first has_many is not redundant - in fact its the very core of how has_many through: works. You setup a shortcut of sorts through another relation.
Note in has_many :posts, through: :bookmarks :bookmarks is the name relation we are joining through. Not the table which contains the joins.
To fix your original code you would need to do:
class Post < ActiveRecord::Base
has_many :saves, dependent: :destroy
has_many :savers, through: :saves
end
class Save < ActiveRecord::Base
belongs_to :saver, class_name: "User"
belongs_to :post # A join table with only one relation is pretty worthless.
validates :saver_id, presence: true
end
class User < ActiveRecord::Base
has_many :posts
has_many :saves, dependent: :destroy
has_many :posts, through: :saves
end
Note that you don't need half the junk - if you have has_many :savers, through: :saves ActiveRecord will look for the relation saver by itself. Also you only want to use dependent: destroy on the join model - not on the post relation as that would remove all the posts a user has "saved" - even those written by others!
Teaching Rails myself, I want to learn the professional way to use the framework and following Rails guidelines best practices. That's not easy, because I usually find answers that 'just works'
I'll try to answer myself and maybe it could be useful for Rails Newbies:
Using has_many through, association, Rails firstly infers the association by looking at the foreign key of the form <class>_id where <class> is the lowercase of the class name, in this example; 'save_id'.
So, if we have the column name 'save_id', we will have the following simplified model:
class Post < ActiveRecord::Base
belongs_to :user
has_many :saves, through: :saves
end
class Save < ActiveRecord::Base
belongs_to :savers, class_name: "User"
validates :save_id, presence: true
end
class User < ActiveRecord::Base
has_many :posts, dependent: :destroy
...
end

Can I use has_many through: without destroying all associated records on delete?

I'm trying to make a list of commissions to add to applicant_commissions.
ApplicantCommission.rb
belongs_to :applicant
belongs_to :commission
Applicant.rb
has_many :applicant_commissions
Commission.rb
has_many :applicant_commissions
The problem is, if I use has_many through: when I delete an Applicant, the Commission is removed too. (and visa versa)
Any suggestions how I can create and destroy this without losing the associated record?
You should have following relations in your file
ApplicantCommission.rb
belongs_to :applicant
belongs_to :commission
Applicant.rb
has_many :applicant_commissions, dependent: :destroy
has_many :commissions, through: :applicant_commissions
Commission.rb
has_many :applicant_commissions, dependent: :destroy
has_many :applications, through: :applicant_commissions
This way your assosiation will not be deleted.
Secondly i would recommend you to have a look at this 'mark_for_destruction' for deletion.
This will help you deleting things easily.
http://api.rubyonrails.org/classes/ActiveRecord/AutosaveAssociation.html

Rails: ignoring duplicates in an nested association

I have models User, Team, Document. There's a many-to-many relationship between Users and Teams, and a many-to-many relationship between Teams and Documents, using join tables called TeamMembership and TeamDocument respectively.
The relationships in my models look like this:
class Document < ActiveRecord::Base
has_many :team_documents, dependent: :destroy
has_many :teams, through: :team_documents
end
class User < ActiveRecord::Base
has_many :team_memberships, dependent: :destroy, foreign_key: :member_id
has_many :teams, through: :team_memberships
has_many :documents, through: :teams
end
class TeamDocument < ActiveRecord::Base
belongs_to :team
belongs_to :document
end
class TeamMembership < ActiveRecord::Base
belongs_to :team
belongs_to :member, class_name: "User"
end
class Team < ActiveRecord::Base
has_many :team_documents, dependent: :destroy
has_many :documents, through: :team_documents
has_many :team_memberships, dependent: :destroy
has_many :members, through: :team_memberships
end
The idea is that users can belong to many teams, a document can be associated with many teams, and users will only have access to documents that "belong" to at least one team that the user is a member of.
Here's the question: I can use User#documents to retrieve a list of all the documents that this user is allowed to view. But this will return duplicates if a document is viewable by more than one team which the user is a member of. How can I avoid this?
I know I can remove the duplicates after the fact with #user.documents.uniq, but as I will never want to include the duplicates in any case, is there a way I can just make #documents not include duplicates every time?
I don't have nested has_many :through like yours to test it, but I suspect using uniq option on your user association would help :
class User < ActiveRecord::Base
has_many :documents, through: :teams, uniq: true
end
You can add a default_scope on Document model:
class Document < ActiveRecord::Base
default_scope group: { documents: :id }

Resources