I would like to scope a join table and create a new association.
In my join table (PrestaMission), I store two values (:assigned and :proposed). So I would like to create a new association to create two has_many in my model.
I give you the code, it will be clearest.
The model Mission :
class Mission < ApplicationRecord
has_many :presta_missions, dependent: :destroy
end
The join table (PrestaMission)
class PrestaMission < ApplicationRecord
belongs_to :client, polymorphic: true
belongs_to :mission
scope :assigned, -> { where(assigned: true) }
scope :proposed, -> { where(proposed: true) }
end
And the model Keyper (which is one of the client of PrestaMission)
class Keyper < ApplicationRecord
has_many :presta_missions, as: :client, dependent: :destroy
has_many :missions, through: :presta_missions
has_many :assigned_prestas, -> { where(assigned: true) }, :class_name => 'PrestaMission'
has_many :assigned_missions, through: :assigned_prestas
has_many :proposed_prestas, -> { where(proposed: true) }, :class_name => 'PrestaMission'
has_many :proposed_missions, through: :proposed_prestas
end
But this script to link association is currently not working. Any suggestion ?
I found an elegant solution to solve that problem :
has_many :presta_missions, as: :client, dependent: :destroy
has_many :missions, :through => :presta_missions do
def assigneds
where("presta_missions.assigned = ?", true)
end
def proposeds
where("presta_missions.proposed = ?", true)
end
end
And to call this association you can now use the following command
#keyper.missions.proposeds
OR
#keyper.missions.assigneds
Make good use and have fun !!
Related
I have three models, User, Registrant, Program.
Registrant is a join table between User and Program and contains user_id, program_id and status, which is an enum.
class User < ApplicationRecord
has_many :registrants, dependent: :destroy
has_many :programs, through: :registrants
end
class Registrant < ApplicationRecord
belongs_to :user
belongs_to :program
enum status: {
enrolled: 1,
unenrolled: 2,
instructor: 3,
passed: 4,
failed: 5
}
scope :active, -> { where(status: [:enrolled, :instructor]) }
end
class Program < ApplicationRecord
has_many :registrants, dependent: :destroy
has_many :users, through: :registrants
end
I would like to be able to retrieve only records from the join table that have an enum status of :enrolled or :instructor, so that I can do something like:
program.registrants.active.users
How do I accomplish this?
Long chains like program.registrants.active.users should be avoided due to the Law of Demeter so that not a good goal.
You can add conditions to joined table in .where by passing a hash:
program.users.where(
registrants: { status: [:enrolled, :instructor] }
)
Note that the key should be the table name (or its alias in the query) and not the name of the association.
You can also pass a scope:
program.users.where(Registrant.active)
And you can clean this up additionally be creating a "scoped" association:
class Program < ApplicationRecord
has_many :registrants, dependent: :destroy
has_many :users, through: :registrants
has_many :active_users,
through: :registrants,
source: :user,
-> { where(Registrant.active) }
end
Which will let you call:
program.active_users # Demeter is happy.
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/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
I have a many-to-many relation between User and "Link".
The join model is called LinkAddress and besides for saving the IDs of the other two models, it has an attribute called address - information it collects at creation.
How can I access the address attribute for a certain link in a request scenario like the following: User.first.links.first.address ?
Models:
class User < ActiveRecord::Base
has_many :link_addresses, dependent: :destroy
has_many :links, through: :link_addresses
accepts_nested_attributes_for :link_addresses, allow_destroy: true
end
class LinkAddress < ActiveRecord::Base
belongs_to :user
belongs_to :link
end
class Link < ActiveRecord::Base
has_many :link_addresses, dependent: :destroy
has_many :users, through: :link_addresses
end
You could access it through User since it's a has_many ... :through relation:
User.first.link_addresses.first.address
Or, if you'd like to go through links then:
User.first.links.first.link_addresses.first.address
SQL Aliases
I had this exact question: Rails Scoping For has_many :through To Access Extra Data
Here's the answer I got:
#Images
has_many :image_messages, :class_name => 'ImageMessage'
has_many :images, -> { select("#{Image.table_name}.*, #{ImageMessage.table_name}.caption AS caption") }, :class_name => 'Image', :through => :image_messages, dependent: :destroy
This uses SQL Aliases which I found at this RailsCast (at around 6:40 in). It allows us to call #user.image.caption (even though .caption is in the join model)
Your Code
For your query, I'd use this:
class User < ActiveRecord::Base
has_many :link_addresses, dependent: :destroy
has_many :links, -> { select("#{Link.table_name}.*, #{LinkAddress.table_name}.address AS address") }, through: :link_addresses
accepts_nested_attributes_for :link_addresses, allow_destroy: true
end
This will allow you to write #user.links.first.address, and gracefully handles an absence of the address record
How can I achieve the following? I have two models (blogs and readers) and a JOIN table that will allow me to have an N:M relationship between them:
class Blog < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :readers, :through => :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :blogs, :through => :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
What I want to do now, is add readers to different blogs. The condition, though, is that I can only add a reader to a blog ONCE. So there mustn't be any duplicates (same readerID, same blogID) in the BlogsReaders table. How can I achieve this?
The second question is, how do I get a list of blog that the readers isn't subscribed to already (e.g. to fill a drop-down select list, which can then be used to add the reader to another blog)?
Simpler solution that's built into Rails:
class Blog < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :readers, :through => :blogs_readers, :uniq => true
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :blogs, :through => :blogs_readers, :uniq => true
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
Note adding the :uniq => true option to the has_many call.
Also you might want to consider has_and_belongs_to_many between Blog and Reader, unless you have some other attributes you'd like to have on the join model (which you don't, currently). That method also has a :uniq opiton.
Note that this doesn't prevent you from creating the entries in the table, but it does ensure that when you query the collection you get only one of each object.
Update
In Rails 4 the way to do it is via a scope block. The Above changes to.
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { uniq }, through: :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :blogs, -> { uniq }, through: :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
Update for Rails 5
The use of uniq in the scope block will cause an error NoMethodError: undefined method 'extensions' for []:Array. Use distinct instead :
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { distinct }, through: :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :blogs, -> { distinct }, through: :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
This should take care of your first question:
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
validates_uniqueness_of :reader_id, :scope => :blog_id
end
The Rails 5.1 way
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { distinct }, through: :blogs_readers
end
class Reader < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :blogs, -> { distinct }, through: :blogs_readers
end
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
end
What about:
Blog.find(:all,
:conditions => ['id NOT IN (?)', the_reader.blog_ids])
Rails takes care of the collection of ids for us with association methods! :)
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
The answer at this link shows how to override the "<<" method to achieve what you are looking for without raising exceptions or creating a separate method: Rails idiom to avoid duplicates in has_many :through
The top answer currently says to use uniq in the proc:
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { uniq }, through: :blogs_readers
end
This however kicks the relation into an array and can break things that are expecting to perform operations on a relation, not an array.
If you use distinct it keeps it as a relation:
class Blog < ActiveRecord::Base
has_many :blogs_readers, dependent: :destroy
has_many :readers, -> { distinct }, through: :blogs_readers
end
I'm thinking someone will come along with a better answer than this.
the_reader = Reader.find(:first, :include => :blogs)
Blog.find(:all,
:conditions => ['id NOT IN (?)', the_reader.blogs.map(&:id)])
[edit]
Please see Josh's answer below. It's the way to go. (I knew there was a better way out there ;)
I do the following for Rails 6
class BlogsReaders < ActiveRecord::Base
belongs_to :blog
belongs_to :reader
validates :blog_id, uniqueness: { scope: :reader_id }
end
Don't forget to create database constraint to prevent violations of a uniqueness.
Easiest way is to serialize the relationship into an array:
class Blog < ActiveRecord::Base
has_many :blogs_readers, :dependent => :destroy
has_many :readers, :through => :blogs_readers
serialize :reader_ids, Array
end
Then when assigning values to readers, you apply them as
blog.reader_ids = [1,2,3,4]
When assigning relationships this way, duplicates are automatically removed.