Ordering with multi level nested attributes in rails - ruby-on-rails

So I've been trying to order this:
gig.rb
Class Gigenrollment < ActiveRecord::Base
has_many :gigenrollments, dependent: :destroy
gigenrollment.rb
Class Gigenrollment < ActiveRecord::Base
belongs_to :gig
belongs_to :profile
def self.order_by_instrument
includes(profile: :instruments).order(profiles: {instruments: {primary: :desc}})
end
profile.rb
Class Profile < ActiveRecord::Base
has_many: :gigenrollments, dependent: :destroy
has_many :instruments, -> {order(primary: :desc)}, dependent: :destroy
instrument.rb
Class Instrument < ActiveRecord::Base
belongs_to :profile
I have one way of getting the right order, but it's kind of ugly. It looks like this in the controller:
# #gig is an instance of the Gig class, eg. Gig.last
#orchestra = #gig.gigenrollments
#orchestra = #orchestra.sort_by{ |ge| ge.profile.instruments.find_by(primary: true).nil? ? 'z' : ge.profile.instruments.find_by(primary: true).name}
So I'm trying to figure out a neater way to order gig enrollments from one gig depending on one profiles primary instruments name (one profile should have none or more). So far I think I've got the includes correct but I can't find an example of an order that has nested attributes like I have.
So far I have done this in my controller in hope to get what I want:
#orchestra = #gig.gigenrollments
#orchestra = #orchestra.order_by_instrument # check out the order_by_instrument function in the code snippet in gigenrollment.rb
But the error I get is
Direction should be :asc or :desc
Which leaves me to think that I just can't get the syntax right. I have also tried this without success
# gigenrollment.rb
def self.order_by_instrument
includes(profile: :instruments).references(profile: :instruments).order('"profile"."instruments"."primary" ASC, "profile"."instruments"."name" ASC')
end
I've been reading this about includes and this about order and several posts about this topic on Stackoverflow, but they all seem to have either nested attributes in only one level (I have two, gigenrollments -> profile -> instruments), they are using scopes, have forms or then they have a where clause instead of order, which doesn't really get me to where I would want (wrong syntax). I also found some issues on Github that made me think that it should be doable.
But I kind of get the picture that this isn't doable with an order as one profile can have many instruments and only the instruments with the attribute 'primary' as true should affect the ordering of the gigenrollments, while I'm writing this question.
Apparently I need reputation to post links so I have them here instead...
The stackoverflow pages:
stackoverflow.com/questions/23121975/how-can-i-order-nested-includes-records-with-rails-4
stackoverflow.com/questions/19865208/rails-query-join-order-by-group-by-issue
stackoverflow.com/questions/13619560/rails-y-way-to-query-a-model-with-a-belongs-to-association
Github issues:
github.com/rails/rails/issues/8663
github.com/rails/rails/issues/726

Related

Rails multiple joins into one method

I have a user in my application that can have multiple assessments, plans, and materials. There is already a relationship between these in my database. I would like to show all these in a single tab without querying the database too many times.
I tried to do a method that joins them all in a single table but was unsuccessful. The return was the following error: undefined method 'joins' for #<User:0x007fcec9e91368>
def library
self.joins(:materials, :assessments, :plans)
end
My end goal is to just itterate over all objects returned from the join so they can be displayed rather than having three different variables that need to be queried slowing down my load times. Any idea how this is possible?
class User < ApplicationRecord
has_many :materials, dependent: :destroy
has_many :plans, dependent: :destroy
has_many :assessments, dependent: :destroy
end
class Material < ApplicationRecord
belongs_to :user
end
class Assessment < ApplicationRecord
belongs_to :user
end
class Plan < ApplicationRecord
belongs_to :user
end
If all you want to do is preload associations, use includes:
class User < ApplicationRecord
# ...
scope :with_library, -> { includes(:materials, :assessments, :plans) }
end
Use it like this:
User.with_library.find(1)
User.where(:name => "Trenton").with_library
User.all.with_library
# etc.
Once the associations are preloaded, you could use this for your library method to populate a single array with all the materials, assessments and plans of a particular user:
class User < ApplicationRecord
# ...
def library
[materials, assessments, plans].map(&:to_a).flatten(1)
end
end
Example use case:
users = User.all.with_library
users.first.library
# => [ ... ]
More info: https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
Prefer includes over joins unless you have a specific reason to do otherwise. includes will eliminate N+1 queries, while still constructing usable records in the associations: you can then loop through everything just as you would otherwise.
However, in this case, it sounds like you're working from a single User instance: in that case, includes (or joins) can't really help -- there are no N+1 queries to eliminate.
While it's important to avoid running queries per row you're displaying (N+1), the difference between one query and three is negligible. (It'd cost more in overhead to try to squish everything together.) For this usage, it's just unnecessary.

Rails Sort by join table row

I have a many to many relastionship through join table :
team.rb
has_many :partners_teams
has_many :partners, through: :partners_teams
partner.rb
has_many :partners_teams
has_many :teams, through: :partners_teams
partners_team.rb
belongs_to :team
belongs_to :partner
self.primary_key = :partner_id
include RankedModel
ranks :row_order, with_same: :team_id
default_scope { order(row_order: :asc) }
I set an additional row "row_order" integer in the join table teams_partners
Partner is a nested resource of Team
How can I address the row_order column when I request the partners from a designed card, which happens in partners#index and cards#show
Currently, I do (it works correctly) :
Partners#index :
def index
#card = Card.find(params[:id])
#partners = #team.partners.all
end
Teams#show
def show
#partners = #team.partners.all
end
I tried several things with joins and include but with no success. It's still a bit complicated to my level.
Moreover, I use Harvest ranked-model gem to sort my partners. It seems to work well except the initial order (problem described above). Thing is ranked-model use the .rank() method to order things. Ex: Partners.rank(:row_order). I'm not sure if that's a thing to take into account, I mean I can use an Partners.order(row_order: :desc), but maybe that'll have an impact for the following sorting.
Any help appreciated, really.
Thank you a lot.

Ruby On Rails ActiveRecord 3 Way Join

I have 3 models:
class ProductLine < ActiveRecord::Base
has_many :specifications
has_many :specification_categories, :through => :specifications,
end
class Specification < ActiveRecord::Base
belongs_to :product_line
belongs_to :specification_category
end
class SpecificationCategory < ActiveRecord::Base
has_many :specifications
has_many :product_lines, :through => :specifications
end
Basically, we are showing the specifications as a subset of data on the product line page and we would like to do something like (example only, yes I'm aware of N+1):
Controller:
#product_line = ProductLine.find(params[:id])
#specification_categories = #product_line.specification_categories)
View:
#specification_categories.each do |specification_category|
...
specification_category.specifications.each do |specification|
...
end
end
The issue here is getting rails to filter the specifications by ProductLine. I've tried constructing queries to do this but it always generates a separate NEW query when the final association is called. Even though we aren't using the code above now (not a good idea here, since we could potentially run into an N+1 problem), I'd like to know if it's even possible to do a 3 way join with association filtering. Has anyone run across this scenario? Can you please provide an example of how I would accomplish this here?
Prevent the N+1 by altering your line to:
#specification_categories = #product_line.specification_categories).include(:specifications)
OR
Construct your own query using .joins(:association), and do the grouping yourself.

Rails: Display products based on multiple params

I'm learning rails and trying to set up a product library where the products will be displayed based on three elements: location, category and expiry date (products can have multiple locations and categories but just one expiry date). Products will be shown as long as their expiry date hasn't passed and location and category selection will be via dropdown menus.
I started writing this question while having difficulty with incorporating the location and category selection criteria which i found a solution to but any help on what could be done better is greatly appreciated.
I've used has_many through connections to create the connections between the products, location and categories.
Here's the models:
class Product < ActiveRecord::Base
has_many :categorizations
has_many :categories, :through => :categorizations
has_many :localizations
has_many :locations, :through => :localizations
end
class Categorization < ActiveRecord::Base
belongs_to :product
belongs_to :category
end
class Category < ActiveRecord::Base
has_many :categorizations
has_many :products, :through => :categorizations
end
class Localization < ActiveRecord::Base
belongs_to :product
belongs_to :location
end
class Location < ActiveRecord::Base
has_many :localizations
has_many :products, :through => :localizations
end
Here's my controller. Location & category ID's are passed as params and the expiry date of the products must be greater than the current time:
class LibraryController < ApplicationController
def index
#products = Product.find(:all, include => [ :locations, :categories ],
:conditions => ['expiry_date > ? AND locations.id = ? AND categories.id = ?',
Time.now, params[:location_id],params[:category_id]])
end
end
So by passing the location_id and category_id params in the URL I can list products by a combination of both.
Is there a better way of achieving what I'm trying to do?
This will also do what you want:
#products = Product.find_all_by_category_id_and_location_id(params[:category_id], params[:location_id])
You can also user Product.where which is supposedly better than find.
For more information, Google "dynamic finders".
Ok. No, I don't think there is a "better" way in this case. There certainly are "different" ways of doing what you want, but on the face of it, what you're doing is fine, and it doesn't scream out "this code is terrible!" or anything.
Questions of advice/style are tough to answer here, because ultimately the answer to them is, "search the web for what other people are doing in your situation, and evaluate/make the decision yourself if your solution seems conventional/logical," or these kinds of questions are answered via study of relevant books on the topic.
It's nearly impossible to answer a qualitative question like this, because:
There's several ways to solve every problem, many of which are neither "right" or "wrong"
There's always edge cases where people break the "rules", in which case even unconventional solutions can absolutely be the best way to do something
You're the developer, the one building the thing. To some extent you're expected to take a leadership role, and decide what's best
The reason I ask you to define "better" is primarily because of #1 - unless you give us a specific outcome you're trying to achieve, all you'll get are (a) answers that are full of opinion, and not directed toward a specific goal or (b) simply a different way of doing something which may or may not help you. Therefore, they aren't very useful in practical terms.
You could also improve upon your solution by using, "Product.where" (preferred over find in rails 3.1) and also turn them into named_scopes in Rails like and chain them as required.
scope :not_expired, where('expiry_date > ?', Time.now)

Should I denormalize a has_many has_many?

I have this:
class User < ActiveRecord::Base
has_many :serials
has_many :sites, :through => :series
end
class Serial < ActiveRecord::Base
belongs_to :user
belongs_to :site
has_many :episodes
end
class Site < ActiveRecord::Base
has_many :serials
has_many :users, :through => :serials
end
class Episode < ActiveRecord::Base
belongs_to :serial
end
I would like to do some operations on User.serials.episodes but I know this would mean all sorts of clever tricks. I could in theory just put all the episode data into serial (denormalize) and then group_by Site when needed.
If I have a lot of episodes that I need to query on would this be a bad idea?
thanks
I wouldn't bother denormalizing.
If you need to look at counts, you can check out counter_cache on the relationship to save querying for that.
Do you have proper indexes on your foreign keys? If so, pulling the data from one extra join shouldn't be that big of a deal, but you might need to drop down to SQL to get all the results in one query without iterating over .serials:
User.serials.collect { |s| s.episodes }.uniq # ack! this could be bad
It really depends on the scale you are needing out of this application. If the app isn't going to need to serve tons and tons of people then go for it. If you are getting a lot of benefit from the active record associations then go ahead and use them. As your application scales you may find yourself replacing specific instances of the association use with a more direct approach to handle your traffic load though.

Resources