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.
Related
I have 3 model User Project Bug. I want to create many to many relation with through. I create the relation in model i don't know it is correct or not, user have user type column which is enum type user type contain developer, manager , QA
user.user_type.manager belong to many project it has one to many relation
user.user_type.developer has many project and many project belong to developer. it has many to many realtion
project has many bugs and bugs belong to project
developer has many bugs and many bugs belong to developer
bug model
class Bug < ApplicationRecord
belongs_to :project
has_many :developers, -> { where user_type: :Developer }, class_name: 'User', through: :project, source: :bugs
end
project model
class Project < ApplicationRecord
has_many :bugs, dependent: :delete_all
has_many :developers, -> { where user_type: :Developer }, class_name: 'User', through: :users, source: :project
has_many :users //it belong to manager_id
end
user model
class User < ApplicationRecord
enum user_type: %i[Manager Developer QA]
has_many :projects
has_many :bugs
end
developer_bug model
class DevelopersBug < ApplicationRecord
has_many :bugs
has_many :users
end
project_developer model
class ProjectsDeveloper < ApplicationRecord
has_many :projects
has_many :users
end
This
has_many :developers, -> { where user_type: :Developer },
class_name: 'User',
through: :users,
source: :project
is not what you think it is. It means something on the line of:
I already have an association 'users'. The users have an association 'project'.
Please configure an association that makes both JOINs and gives me the list of projects associated to the associated users.
This association will be named "developers" and be of objects of class "User".
You can see how these instructions are inconsistent. This
has_many :projects, through: :users, source: :project
will define a list of associated projects, by jumping over users.
On the other side, this:
has_many :developers, -> { where user_type: :Developer }, class_name: 'User'
will define a direct has-many association with a subset of all the users.
Given your description, your data model seems wrong, maybe this will be better:
class User < ApplicationRecord
has_many :managed_projects, inverse_of: :manager, class_name: 'Project'
has_and_belongs_to_many :projects
has_many :bugs
end
class Project < ApplicationRecord
belongs_to :manager, class_name: 'User', inverse_of: :managed_projects
has_and_belongs_to_many :users
has_many :bugs
end
class Bug < ApplicationRecord
belongs_to :user
belongs_to :project
end
Your schema should include the three tables, and an additional many-to-many join table projects_users that holds foreign keys to both users and projects.
As rewritten has already pointed in his excellent answer out your data model is flawed. What you want instead is a join table which joins the users and projects:
class User < ApplicationRecord
has_many :project_roles
has_many :projects, through: :project_roles
end
class ProjectRole < ApplicationRecord
belongs_to :user
belongs_to :project
end
class Project < ApplicationRecord
has_many :users
has_many :projects, through: :project_roles
end
If you then want to give the user specific roles in a project you would add the enum to the join table and this is where it starts to get hairy so bear with me here:
class ProjectRole < ApplicationRecord
enum roles: [:manager, :developer, :qa]
belongs_to :user
belongs_to :project
end
class User < ApplicationRecord
has_many :project_roles
has_many :projects, through: :project_roles
has_many :project_roles_as_manager,
-> { manager }, # short for `where(role: :manager)`
class_name: 'ProjectRole'
has_many :projects_as_manager,
class_name: 'Project',
through: :project_roles_as_manager,
source: :project
has_many :project_roles_as_developer,
-> { developer },
class_name: 'ProjectRole'
has_many :projects_as_developer,
class_name: 'Project',
through: :project_roles_as_developer,
source: :project
# ...
end
This defines associations with a default scope and then joins through that association. You would then do the same thing on the other end of the assocation:
class Project < ApplicationRecord
has_many :users
has_many :projects, through: :project_roles
has_many :manager_project_roles,
-> { manager },
class_name: 'ProjectRole'
has_many :managers,
through: :manager_project_roles,
source: :user
# ...
end
Of course this is a lot of duplication which you can cut by looping over ProjectRoles.roles.keys and defining the assocations dynamically.
This is a very flexible way of modeling it which makes as few assumptions about the domain as possible. For example it allows multiple managers for a project and it allows users to have different roles in different projects.
If you want to model "bugs" as you would typically would with issues in a tracker you would create one table for the bug and a join table for the assignment:
class Bug < ApplicationRecord
belongs_to :project
has_many :bug_assignments
has_many :users, through: :bug_assignments
end
class BugAssignment < ApplicationRecord
has_one :project, through: :bug
belongs_to :bug
belongs_to :user
end
class User < ApplicationRecord
# ...
has_many :bug_assignments
has_many :bugs
end
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)})
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 !!
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
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}}})