Rails Sort by join table row - ruby-on-rails

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.

Related

Rails ActiveRecord includes with run-time parameter

I have a few models...
class Game < ActiveRecord::Base
belongs_to :manager, class_name: 'User'
has_many :votes
end
class Vote < ActiveRecord::Base
belongs_to :game
belongs_to :voter, class_name: 'User'
end
class User < ActiveRecord::Base
has_many :games, dependent: :destroy
has_many :votes, dependent: :destroy
end
In my controller, I have the following code...
user = User.find(params[:userId])
games = Game.includes(:manager, :votes)
I would like to add an attribute/method voted_on_by_user to game that takes a user_id parameter and returns true/false. I'm relatively new to Rails and Ruby in general so I haven't been able to come up with a clean way of accomplishing this. Ideally I'd like to avoid the N+1 queries problem of just adding something like this on my Game model...
def voted_on_by_user(user)
votes.where(voter: user).exists?
end
but I'm not savvy enough with Ruby/Rails to figure out a way to do it with just one database roundtrip. Any suggestions?
Some things I've tried/researched
Specifying conditions on Eager Loaded Associations
I'm not sure how to specify this or give the includes a different name like voted_on_by_user. This doesn't give me what I want...
Game.includes(:manager, :votes).includes(:votes).where(votes: {voter: user})
Getting clever with joins. So maybe something like...
Game.includes(:manager, :votes).joins("as voted_on_by_user LEFT OUTER JOIN votes ON votes.voter_id = #{userId}")
Since you are already includeing votes, you can just count votes using non-db operations: game.votes.select{|vote| vote.user_id == user_id}.present? does not perform any additional queries if votes is preloaded.
If you necessarily want to put the field in the query, you might try to do a LEFT JOIN and a GROUP BY in a very similar vein to your second idea (though you omitted game_id from the joins):
Game.includes(:manager, :votes).joins("LEFT OUTER JOIN votes ON votes.voter_id = #{userId} AND votes.game_id = games.id").group("games.id").select("games.*, count(votes.id) > 0 as voted_on_by_user")

Ordering with multi level nested attributes in 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

Find records which assoicated records do not belong to certain record

In my system I have a following structure:
class Worker
has_many :worker_memberships
end
class WorkerMembership
belongs_to :worker
belongs_to :event
end
class Event
has_many :worker_memberships
end
Imagine I have a certain #event. How can I find all workers that have NO worker_memberships belonging to this #event?
This is pretty much synthesis of both other answers.
First: stick to has_many through as #TheChamp suggests. You're probably using it already, just forgot to write it, otherwise it just wouldn't work. Well, you've been warned.
I generally do my best to avoid raw SQL in my queries whatsoever. The hint about select I provided above produces a working solution, but does some unneessary stuff, such as join when there's no practical need for it. So, let's avoid poking an association. Not this time.
Here comes the reason why I prefer has_many through to has_and_belongs_to_many in many-to-many associations: we can query the join model itself without raw SQL:
WorkerMembership.select(:worker_id).where(event: #event)
It's not the result yet, but it gets us the list of worker_ids we don't want. Then we just wrap this query into a "give me all but these guys":
Worker.where.not(id: <...> )
So the final query is:
Worker.where.not(id: WorkerMembership.select(:worker_id).where(event: #event) )
And it outputs a single query (on #event with id equal to 1):
SELECT `workers`.* FROM `workers` WHERE (`workers`.`id` NOT IN (SELECT `worker_memberships`.`worker_id` FROM `worker_memberships` WHERE `worker_memberships`.`event_id` = 1))
I also give credit to #apneadiving for his solution and a hint about mysql2's explain. SQLite's explain is horrible! My solution, if I read the explain's result correctly, is as performant as #apneadiving's.
#TheChamp also provided performance costs for all answers' queries. Check out the comments for a comparison.
Since you want to set up a many to many relationship between Worker and Event, I'd suggest you use the through association.
Your resulting models would be.
class Worker
has_many :worker_memberships
has_many :events, :through => :worker_memberships
end
class WorkerMembership
belongs_to :worker
belongs_to :event
end
class Event
has_many :worker_memberships
has_many :workers, :through => :worker_memberships
end
Now you can just call #event.workers to get all the workers associated to the event.
To find all workers that don't belong to the #event you can use:
# get all the id's of workers associated to the event
#worker_ids = #event.workers.select(:id)
# get all workers except the ones belonging to the event
Worker.where.not(:id => #worker_ids)
The one-liner
Worker.where.not(:id => #event.workers.select(:id))
Try this:
Worker.where(WorkerMembership.where("workers.id = worker_memberships.worker_id").where("worker_memberships.event_i = ?", #event.id).exists.not)
Or shorter and reusable:
class WorkerMembership
belongs_to :worker
belongs_to :event
scope :event, ->(event){ where(event_id: event.id) }
end
Worker.where(WorkerMembership.where("workers.id = worker_memberships.worker_id").event(#event.id).exists.not)
(I assumed table and column names from conventions)

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