I am trying to figure out the right way to chain my associations effeciently. I basically have the following hierarchy:
School > Course > Lecture > Attachment
Users can have many of any of these, which indicates not that they own them but rather that they are enrolled in or have access to them. We then use cancan for authorization.
My challenge is that users can be enrolled in a school and then separately enroll in each course, OR they can be enrolled in a "school-wide" package that gives them access to all courses. Before the addition of the latter, our user model includes the following:
belongs_to :school
has_many :active_sales, -> { where active: true }, class_name: "Sale"
has_many :courses, through: :active_sales, source: :sale
has_many :lectures, through: :courses
has_many :attachments, through: :lectures
In trying to add the school-wide sales (sales is polymorphic as "enrollable" with both courses and schools), I can't figure out how to chain the associations together properly. Basically, it's like the chain forks and then rejoins itself. It would be nice if I could do something like
has_many :courses, through: :active_sales, source: :sale, -> {where (id: active_sale.enrollable_id && active_sale.enrollable_type: 'Course') || (school_id: active_sale.enrollable_id && active_sale.enrollable_type: 'School') }
...but I'm pretty sure the where method doesn't take associations and not clear if it takes strung together conditions either. I tried messing with association extensions, but was having trouble there too.
Is there a best way to do this?
Vanilla AtiveRecord can be hard to use in case of complex queries unless you write the SQL statement yourself. The only way to do an OR statement would be to write the whole SQL statement as a string and use it as the where clause.
But you could also use Arel to shape your query instead.
https://github.com/rails/arel
In your case, I think it would make your code cleaner if you created a scope for the active_sales_courses or a class method, instead of trying to put everything in an has_many statement.
def self.active_sales_courses
courses = Arel::Table.new(:courses)
active_sales = Arel::Table.new(:active_sales)
query = courses.joins(active_sales).on(courses[:id].eq(active_sales[:enrollable_id])).
where((courses[:id].eq(active_sales[:enrollable_id]).
and(active_sales[:enrollable_type].eq('Course'))).
or(courses[:school_id].eq(active_sales[:enrollable_id]).
and(active_sales[:enrollable_type].eq('School'))))
query.to_sql
end
I can't guarantee this code works as I have no way of testing it but it is provided as an indication.
Related
I'm trying to refactor a model that has a lot of has_many :through associations that involve conditions on the join table
class Assignment
has_many :assignment_reviewers
has_many :preferred_assignment_reviewers, -> { preferred }
# more of these for different types of reviewers
has_many :reviewers, through: :assignment_reviewers
has_many :preferred_reviewers, through: :preferred_assignment_reviewers
# more of these
I added the following scope on Reviewer
class Reviewer
scope :preferred, -> do
joins(:assignment_reviewers).merge(AssignmentReviewer.preferred)
so that I could do
assignment.reviewers.preferred
instead of using the has_many
assignment.preferred_reviewers
However, the former results in a duplicate INNER JOIN
INNER JOIN `assignment_reviewers` `assignment_reviewers_reviewers`
ON `assignment_reviewers_reviewers`.`reviewer_id` = `reviewers`.`id`
INNER JOIN `assignment_reviewers`
ON `reviewers`.`id` = `assignment_reviewers`.`reviewer_id`
It seems I have three options:
Keep defining has_manys and has_many :throughs for each (downside: lots of specific associations on a model that is already a god class)
Use the scope (downside: duplicate join)
Use the merge directly
assignment.reviewers.merge(AssignmentReviewer.preferred)
(downside: not as eloquent)
I'm inclined to choose option 2 because cleaner code and I'm guessing the extra join won't have a substantial impact on performance.
Any advice / is there a better alternative that I'm missing?
I figured out a solution that I like better than the three I listed:
I added an extension block to my has_many :through declaration
has_many :reviewers, through: :assignment_reviewers do
def preferred
merge(AssignmentReviewer.preferred)
end
end
While I will still have to declare a lot of extensions, this is a much more concise than declaring a has_many and has_many through for each has_many through that I need
I may be going about this the wrong way but after reading various SO articles and the Rails docs on associations and scopes, I'm not much wiser.
I have a many-to-may relationship expressed like so:
class User < ActiveRecord::Base
has_many :user_program_records
has_many :programs, through: :user_program_records
end
class Program < ActiveRecord::Base
has_many :user_program_records
has_many :users, through: :user_program_records
end
class UserProgramRecord < ActiveRecord::Base
belongs_to :user
belongs_to :program
# has a field "role"
end
The idea is that there are many users in the system and many programs. Programs have many users in them and users may belong to multiple programs. However - within a given program, a user can only have one role.
What I'd really like to be able to write is:
Program.first.users.first.role
and have that return me the role (which is just a String).
What's the cleanest way to do this? Basically, once I've scoped a user to a given program, how do I cleanly access fields on the relevant join table?
You are thinking about it slightly wrong:
user.role
Would be very ambiguous as a user can have different roles in different programs. Instead you need to think of the join entity as a thing of its own.
The easiest way is to select the join model directly:
program = Program.includes(:user_program_records, :users).first
role = program.user_program_records
.find_by(user: program.users.first)
.role
You can use stuff like association extensions and helper methods to make this a bit sexier.
I have a chain model that has many facilities
I have a company model has many company_mappings. Through company_mappings a comapny can have many facilities either through chains or directly from a facility, both of them through a polymorphic association in the companies_mapping model.
The Company_Mapping Model
belongs_to :company
belongs_to :company_associations, polymorphic: true
The Chain Model
has_many :company_mappings, as: :company_associations
has_many :facilities
The Facility Model
has_many :company_mappings, as: :company_associations
belongs_to :chain
Now i have the company model has two different queries to get its assoiciated facilities. I would like to have one query that fetches all facilities associated from a company
The company model
has_many :company_mappings
has_many :chains, through: :company_mappings, source: :company_associations, source_type: "Chain"
has_many :facilities, through: :company_mappings, source: :company_associations, source_type: "Facility"
has_many :facilities_from_chains, through: :chains,source: :facilities, class_name: 'Facility'
I would like to combine facilities and _facilities_from_chains_ into a single query or able to somehow merge them. I tried using .merge but that gives an error.
One way to do this would be to provide an instance method which queried both relations separately, fetched the results and combined them. Try adding this to your company model:
def all_facilities
facilities + facilities_from_chains
end
Combining the results together into a single array like this will cause your results to be converted from a relation object (which you could call further .where clauses on for example) to an array which you cannot chain further AR statements to. It's worth keeping that in mind.
There may be another way to do this with a single SQL query, but this seems like the easiest implementation at the potential expense of DB performance.
I found a way just in case somebody what like to know how.
scope :company_facilities, -> (chain_ids,company_id){
includes(:company_mappings)
.where("chain_id IN (?) OR (company_mappings.company_associations_type=? and company_mappings.company_id=?)", chain_ids , "Facility", company_id)
.references(:company_mappings)
}
The idea is to include Company_Mapping which would do a left outer join. Then for each facility record find if it is part of any of the chain the Company is associated to else if its company_mappings.company_id equals to calling Company's id.
I'm having a bit of difficulty figuring out how to do this in the "Rails" way, if it is even possible at all.
Background: I have a model Client, which has a has_many relationship called :users_and_managers, which is defined like so:
has_many :users_and_managers, -> do
Spree::User.joins(:roles).where( {spree_roles: {name: ["manager", "client"]}})
end, class_name: "Spree::User"
The model Users have a has_many relationship called credit_cards which is merely a simple has_many - belongs_to relationship (it is defined in the framework).
So in short, clients ---has many---> users ---has many---> credit_cards
The Goal: I would like to get all the credit cards created by users (as defined in the above relationship) that belong to this client.
The Problem: I thought I could achieve this using a has_many ... :through, which I defined like this:
has_many :credit_cards, through: :users_and_managers
Unfortunately, this generated an error in relation to the join with the roles table:
SQLite3::SQLException: no such column: spree_roles.name:
SELECT "spree_credit_cards".*
FROM "spree_credit_cards"
INNER JOIN "spree_users" ON "spree_credit_cards"."user_id" = "spree_users"."id"
WHERE "spree_users"."client_id" = 9 AND "spree_roles"."name" IN ('manager', 'client')
(Emphasis and formatting mine)
As you can see in the generated query, Rails seems to be ignoring the join(:roles) portion of the query I defined in the block of :users_and_managers, while still maintaining the where clause portion.
Current Solution: I can, of course, solve the problem by defining a plain 'ol method like so:
def credit_cards
Spree::CreditCard.where(user_id: self.users_and_managers.joins(:credit_cards))
end
But I feel there must be a more concise way of doing this, and I am rather confused about the source of the error message.
The Question: Does anyone know why the AR / Rails seems to be "selective" about which AR methods it will include in the query, and how can I get a collection of credit cards for all users and managers of this client using a has_many relationship, assuming it is possible at all?
The joins(:roles) is being ignored because that can't be appended to the ActiveRecord::Relation. You need to use direct AR methods in the block. Also, let's clean things up a bit:
class Spree::Role < ActiveRecord::Base
scope :clients_and_managers, -> { where(name: %w{client manager}) }
# a better scope name would be nice :)
end
class Client < ActiveRecord::Base
has_many :users,
class_name: "Spree::User",
foreign_key: :client_id
has_many :clients_and_managers_roles,
-> { merge(Spree::Role.clients_and_managers) },
through: :users,
source: :roles
has_many :clients_and_managers_credit_cards,
-> { joins(:clients_and_managers_roles) },
through: :users,
source: :credit_cards
end
With that setup, you should be able to do the following:
client = # find client according to your criteria
credit_card_ids = Client.
clients_and_managers_credit_cards.
where(clients: {id: client.id}).
pluck("DISTINCT spree_credit_cards.id")
credit_cards = Spree::CreditCard.where(id: credit_card_ids)
As you can see, that'll query the database twice. For querying it once, check out the following:
class Spree::CreditCard < ActiveRecord::Base
belongs_to :user # with Spree::User conditions, if necessary
end
credit_cards = Spree::CreditCard.
where(spree_users: {id: client.id}).
joins(user: :roles).
merge(Spree::Role.clients_and_managers)
CONTEXT:
In my setup Users have many Communities through CommunityUser, and Communities have many Posts through CommunityPost. If follows then, that Users have many Posts through Communities.
User.rb
has_many :community_users
has_many :communities, through: :community_users
has_many :posts, through: :communities
Given the above User.rb, Calling "current_user.posts" returns posts with one or more communities in common with current_user.
QUESTION:
I'm looking to refine that association so that calling "current_user.posts" returns only those posts whose communities are a complete subset of the current_user's communities.
So, given a current_user with community_ids of [1,2,3], calling "current_user.posts" would yield only those posts whose "community_ids" array is either 1, [2], [3], [1,2], [1,3], [2,3], or [1,2,3].
I've been researching scopes here, but can't seem to pinpoint how to accomplish this successfully...
Nice question...
My immediate thoughts:
--
ActiveRecord Association Extension
These basically allow you to create a series of methods for associations, allowing you to determine specific criteria, like this:
#app/models/user.rb
has_many :communities, through: :community_users
has_many :posts, through: :communities do
def in_community
where("community_ids IN (?)", user.community_ids)
end
end
--
Conditional Association
You could use conditions in your association, like so:
#app/models/user.rb
has_many :posts, -> { where("id IN (?)", user.community_ids) }, through: :communities #-> I believe the model object (I.E user) is available in the association
--
Source
I originally thought this would be your best bet, but looking at it more deeply, I think it's only if you want to use a different association name
Specifies the source association name used by has_many :through
queries. Only use it if the name cannot be inferred from the
association. has_many :subscribers, through: :subscriptions will look
for either :subscribers or :subscriber on Subscription, unless a
:source is given.
Being honest, this is one of those questions which needs some thought
Firstly, how are you storing / calling the community_ids array? Is it stored in the db directly, or is it accessed through the ActiveRecord method Model.association_ids?
Looking forward to helping you further!
You don't show the model and relationship definitions for for Community or CommunityPost so make sure you have a has_many :community_posts and a has_many :posts, through: :community_posts on your Community model and a belongs_to :community and a belongs_to :post on CommunityPost. If you don't need to track anything on ComunityPost you could just use a has_and_belongs_to_many :posts on Community and a join table communities_posts containing just the foreign keys community_id and post_id.
Assuming you have the relationships setup as I describe you should be able to just use current_user.posts to get a relation that can be further chained and which returns all posts for all communities the user is associated with when you call .all on it. Any class methods (such as scope methods) defined your Post model will also be callable from that relation so that is where you should put your refinements unless those refinements pertain to the Community or CommunityPost in which case you would put them on the Community or CommunityPost models respectively. Its actually rare to need an AR relationship extension for scopes since usually you also want to be able to refine the model independently of whatever related model you may use to get to it.