ActiveRecord: Reference attribute from associated model in where statement - ruby-on-rails

I've got two models with a has_many / has_many relationship. I have a variable exp_ids which is an array of integers representing the id's of some ExperienceLevel records. I need to write a query that will select all JobDescriptions that have an ExperienceLevel with one of those ids.
The query must work on an existing ActiveRelation object called job_descriptions, which is being passed through some flow controls in my controller to filter the results based on my params.
I've tried these queries below and some other variations, but with little success. As far as I can tell, ActiveRecord thinks that experience_levels is an attribute, which is causing it to fail.
job_descriptions.where(experience_levels: exp_ids)
job_descriptions.joins(:experience_levels).where(experience_levels.id: exp_ids)
job_descriptions.joins(:experience_levels).where(experience_levels: exp_ids)
job_descriptions.joins(:experience_levels).where("experience_levels.id IN exp_ids")
job_descriptions.includes(:experience_levels).where("experience_levels.id = ?", exp_ids).references(:experience_levels)
Here are my models:
class JobDescription < ActiveRecord::Base
has_many :job_description_experience_levels
has_many :experience_levels, through: :job_description_experience_levels
end
class JobDescriptionExperienceLevel < ActiveRecord::Base
belongs_to :job_description
belongs_to :experience_level
end
class ExperienceLevel < ActiveRecord::Base
has_many :job_description_experience_levels
has_many :job_descriptions, through: :job_description_experience_levels
end
I'm not sure if what I want to do is even possible. I've used a similar approach for another job_description filter where I selected the company_id, but in the case, company_id was an attribute of JobDescription.

Try this:
job_descriptions.joins(:job_description_experience_levels).where(job_description_experience_levels: { experience_level_id: exp_ids })

job_descriptions.joins(:experience_levels).where(experience_levels: {id: exp_ids})

Try this one. Note the lack of plural on the experience level.id
job_descriptions.includes(:experience_levels).where("experience_level.id = ?", exp_ids).references(:experience_levels)

Related

Rails has_many association with individual references

How can you create a has_many association but also be able to refer to the various instances by a term that isn't the name? Sort of like an attribute of the class? (dog1 in this case.)
class User < ActiveRecord::Base
has_many :dogs
dogs: dog1, dog2, dog3, dog4 # I know this won't work but just to give the idea
end
So you can do:
barry = User.create(name: 'Barry')
barry.dog1.create(name: 'rover', weight: 12)
barry.dog1.name #=> rover
barry.dog1.weight #=> 12
There is no clean way to do this in Rails, a workaround is to use has_one:
class User
has_many :dogs
has_one :favorite_dog, class_name: 'Dog', -> { where(favorite: true).order(updated_at: :desc) }
But, the thing is, Rails' has_one is just a has_many with a SQL LIMIT 1. So in fact, the relation some_user.favorite_dog will return only one record even though there could be several "favorited dogs". The has_one :favorite_dog I used as an example should be latest_favorited_dog (because of the order on updated_at).
has_one documentation: https://apidock.com/rails/ActiveRecord/Associations/ClassMethods/has_one

Search where HABTM is in another model's HABTM

I have two models, Subcontractor:
class Subcontractor < ActiveRecord::Base
has_and_belongs_to_many :trades
end
And Bid:
class Bid < ActiveRecord::Base
has_and_belongs_to_many :trades
end
I'm trying to find all subcontractors who have trades that match the trades of a particular bid. I've tried something along the lines of Subcontractor.where(trade_ids: bid.trade_ids) but that doesn't seem to work.
If you pass an array to an ActiveRecord query, ActiveRecord is smart enough to know that you mean all items IN the array.
Have you tried
Subcontractor.where(trade_ids: bid.trades.pluck(:id))
Here is how I finally did the query:
Subcontractor.joins(:trades).where('trades.id' => bid.trade_ids).uniq
You can do it in this way also
Subcontractor.where("trade_id in (?)", bid.trades.pluck(:id))

How should I design/structure my database and ActiveRecord associations? has_many :through?

Using Rails 4 (and Postgres), I'm trying to work out the best way in which to structure my ActiveRecord associations and corresponding database tables/relationships for a diet tracking app.
I want my app to be structured as follows:
I have Document and FoodEntry models. Each Document has a number of FoodEntries. I want to be able to iterate over them like document.food_entries.each ... (which is easy with your typical has_many association).
However all the FoodEntries for each Document need to be able (potentially but not necessarily) to be subdivided by day, as this is a natural division for which logic and calculations must be able to be performed, in addition to doing them for the whole document. For instance I'd be using something like document.day(1).food_entries.each ....
Furthermore, each day should be able to be subdivided (again, optionally) into meals in a similar manner, e.g. document.day(1).meal(1).food_entries.each ...
Lastly, there must be a way to record the user-specified order that the FoodEntries, meals, and days are in for each document. Presumably using number sequence(s)?
I was thinking there are a few ways I could do this:
Use a simple has_many relationship. Have day, meal and sort columns in the food_entries table, where the value for day and meal is left blank or given a default value if a day/meal isn't provided. Use a logic-based approach to get and sort the entries for a day or meal.
Outline:
class Document
has_many :food_entries
class FoodEntry
belongs_to :document
Potential issues:
This might leave things a bit messy in general in the table?
All the logic for subdividing things would have to be hand-coded.
Storing/using the user-defined (i.e. arbitrary) sort order might get a bit complicated? The order for entries AND days AND meals would have to stored in and inferred from one sequence (unless more columns were added).
Use has_many :through to set up associations through days and meals (naming?) tables. Entries where a day/meal isn't specified get given a default. Both these tables have their own individual sort column, along with the food_entries table.
Outline:
class Document
has_many :days
has_many :meals, through: :days
has_many :food_entries, through: :days (AND :meals???)
class Day
belongs_to :document
has_many :meals
has_many :food_entries, through: :meals
class Meal
belongs_to :day
has_many :food_entries
class FoodEntry
belongs_to :meal
Potential issues:
Adds unnecessary relational complexity? (consider that days or at the very least meals are meant to be optional)
Can I even use has_many :food_entries through: ... in my Document model if it would have to go through both tables?
A compromise between the two approaches above: have a days table but keep meal in a column in the food_entries table.
Something else? Polymorphic association(s)?
This is getting a bit complicated to wrap my head around, and so I'm really having a hard time working out what I should use. What is the correct way to go about things?
A couple of final questions which are related but completely optional:
Ideally the day value could be either a datetime value or an arbitrary string, depending on what the user sets. Is this possible?
Could anyone point me to a resource that can inform me about sorting/ordering strategies? Like I said I assume the simplest way is to use a sequence of numbers, but I'm not exactly sure how I would work with such a sequence.
has_many :through
You'd only use a has_many :through relationship if you wanted to attribute multiple FoodEntries to Document, like this:
#app/models/document.rb
Class Document < ActiveRecord::Base
has_many :food_entries_types
has_many :food_entries, through: :food_entries_types
end
#app/models/food_entry_type.rb
Class FoodEntryType < ActiveRecord::Base
belongs_to :document
belongs_to :food_entry
end
#app/models/food_entry.rb
Class FoodEntry < ActiveRecord::Base
has_many :food_entries_types
has_many :documents, through: :food_entries_types
end
This would only allow you to associate many food_entries with a similar number of documents. Although you could add specific days & meals attributes to the join model, allowing you to call them as required
Scopes
I believe a much better option for you is to use ActiveRecord scopes:
#app/models/document.rb
Class Document < ActiveRecord::Base
has_many :food_entries
#uses [wday][3]
#needs to return dates for specific day
scope :day, ->(day = 1) { where(created_at: Date::DAYNAMES[day]) }
scope :meal, ->(meal = 1) { where(meal: meal) }
end
Because scopes can be chained, I believe you'd be able to do this:
food = Document.day(1).meal(2).food_entries
Class Method
You could also create a class_method to achieve something similar:
#app/models/document.rb
Class Document < ActiveRecord::Base
has_many :food_entries
def self.sorted(day = 1, meal = 1)
document = self.where("created_at = ? AND meal = ?", Date::DAYNAMES[day], meal)
end
end
#app/controllers/documents_controller.rb
def show
#document = Document.sorted
end
I ended up implementing my #2 option. This has given me the most flexibility and worked out well. I think it has been the most elegant approach for my use case.

Specifying the default ordering for a has_many association using a join (Rails)?

I have a simple Customer model with a has many relationship with a Purchase model.
class Customer < ActiveRecord::Base
has_many :purchases
end
I am repeatedly finding that I need to order Customer.purchases in my views in the following way:
#customer.purchases.joins(:shop).order("shops.position").order(:position) #yes, two orders chained
In the interest of keeping things DRY, I'd like to put this somewhere centralized so I don't have to repeatedly do it. Ideally, I'd like to make it the default ordering for Customer.purchases. For example:
class Customer < ActiveRecord::Base
has_many :purchases, :order => joins(:shop).order("shops.position").order(:position)
end
Obviously the above doesn't work. How should I do this?
In your customer model you specified joins(:shop) is the value for the key :order. I think here is the problem, So you can use the joins as a key instead of order like below,
class Customer < ActiveRecord::Base
has_many :purchases, :joins => [:shop], :order => "shops.position"
end
I think it may work.
In your purchases model, you can create a class method:
Purchase.rb:
def self.order_by_position
joins(:shop).order("shops.position").order(:position)
end
Then you can say things like:
#customer.purchases.order_by_position
Purchase.order_by_position
You could create a method on Customer that returns ordered purchases:
class Customer < ActiveRecord::Base
has_many :purchases
def ordered_purchases
purchases.joins(:shop).order("shops.position").order(:position)
end
end
and call #customer.ordered_purchases from your views.

has_and_belongs_to_many or polymorphic has_many :through?

I'm having trouble figuring out the right way to set this association up.
I have 3 models: Musicians, Bands and Managers. Each of those can have a certain "level" associated with them (stress, happiness, etc).
But I'm not sure how to correctly associated my Levels model with the other 3.
Do I need some sort of has_many :through that's polymorphic? And how on earth do I set that up? Is there some other type of associated I need?
If this is the case where you're defining attributes that can be assigned to a particular class of model, then you probably want to use a more conventional approach. Instead of kind use something like record_type and build a scope on that instead.
That way what you'd do to fetch them is create an accessor method, not a relationship, between any of your entities and this column. Something like this:
def levels
Level.for_record_type(self.class)
end
The for_record_type is a scope:
scope :for_record_type, lambda { |record_type|
where(:record_type => record_type.to_s)
}
There's no convention for associating models with classes instead of instances of other models.
Yep, this would be a polymorphic association:
class Level < ActiveRecord::Base
belongs_to :leveled, :polymorphic => true # maybe there's a better word than "leveled"
end
class Musician < ActiveRecord::Base
has_many :levels, :as => :leveled
end
You'll need to add a leveled_type and leveled_id to your Levels table.

Resources