ActiveRecord: How to order and retrieve records in a greatest-n-per-group situation - sql

I'm stuck with a classic greatest-n-per-group problem, where a cat can have many kittens, but I'm usually just interested in the youngest.
I already do know how to build a scope and a has_one relation for the Cat.
My question: Is there a way to...
list all cats' names together with their youngest kittens' names...
while at the same time ordering them by their respective youngest kitten's name...
...using just a single SELECT under the hood?
What I got so far:
class Cat < ApplicationRecord
has_many :kittens
has_one :youngest_kitten, -> { merge(Kitten.youngest) }, foreign_key: :cat_id, class_name: :Kitten
scope :with_youngest_kittens, lambda {
joins(:kittens)
.joins(Kitten.younger_kittens_sql("cats.id"))
.where(younger_kittens: { id: nil })
}
end
class Kitten
belongs_to :cat
scope :youngest, lambda {
joins(Kitten.younger_kittens_sql("kittens.cat_id"))
.where(younger_kittens: { id: nil })
}
def self.younger_kittens_sql(cat_field_name)
%{
LEFT OUTER JOIN kittens AS younger_kittens
ON younger_kittens.cat_id = #{cat_field_name}
AND younger_kittens.created_at > kittens.created_at
}
end
end
When I run Cat.with_latest_kittens.order('kittens.name').map(&:name) everything looks fine: I get all the cats' names with just a single SELECT.
But when I run Cat.with_latest_kittens.order('kittens.name').map {|cat| cat.youngest_kitten.name}, I get the right result too, but a superfluous additional SELECT per cat is executed. Which is just logical, because the with_youngest_kittens doesn't know it should populate youngest_kitten. Is there a way to tell it or am I going about this all wrong?

I think adding an includes to your :with_youngest_kittens scope will fix the problem. Try changing the scope to
scope :with_youngest_kittens, lambda {
includes(:youngest_kitten)
.joins(:kittens)
.joins(Kitten.younger_kittens_sql("cats.id"))
.where(younger_kittens: { id: nil })
}
This should prevent Rails from making a separate database query for every kitten.

I found a solution that produces no extra SELECT, however it is quite ugly, so I'll actually go for localarrow's solution as it's more readable!
I thought I'd still post it for the sake of completeness (If someone needs the few ms extra performance):
First I add custom tailored select fields for each kitten column to the Cat.with_youngest_kitten scope:
scope :with_youngest_kittens, lambda {
kitten_columns = Kitten
.column_names
.map { |column_name| "kittens.#{column_name} AS `youngest_kittens.#{column_name}`" }
.join(', ')
joins(:kittens)
.joins(Kitten.latest_outer_join_sql("cats.id"))
.where(later_kittens: { id: nil })
.select("cats.*, #{kitten_columns}")
}
Then I override the has_one youngest_kitten relation with a method, that retrieves those custom selects and calls super if no data has been retrieved:
def youngest_kitten
return super if self[:'youngest_kittens.id'].nil?
kitten_hash = Hash[Kitten.column_names.collect { |column_name| [column_name, self[:"youngest_kittens.#{column_name}"]] }]
kitten_hash[:cat] = self
Kitten.new(kitten_hash)
end

Related

Accessing an attribute on a foreign table by joining two tables in Rails ActiveRecord

I have a model, Message that belongs to the model User and the User model has an attribute name.
Message:
user_id
message_body
1
"hello world"
User:
user_id
name.
1
"johndoe"
The result I want is a complete list of all the messages and the respective user name that created each of those messages.
the api controller endpoint looks something like:
def index
#messages = Message.all
render json: { messages: #messages }
end
The issue is that when I return #messages it only contains the user_id that each message belongs to. What I really want is the user name
I could loop through every message and construct an entirely new object that looks something like:
#object = [
{
name: #messages[0].user.name,
message_body: #messages[0].body
},
{
name: #messages[1].user.name,
message_body: #messages[1].body
},
etc.
]
and then call render json: { messages: #object }
This would probably work fine, but it seems inefficient. Is there a better method for joining these tables for this result?
name
message body
"johndoe"
"hello world"
I was hoping the above example would be enough to get the answer I'm looking for. This is a simplified version of my architecture. In reality it's a bit more complicated:
LeagueChatMessage belongs_to LeagueChat
LeagueChatMessage belongs_to User
LeagueChat belongs_to League
League has_one LeagueChat
so this is really what the controller looks like
def index
#league = League.find_by(id: 1)
render json: { messages: #league.league_chat.league_chat_messages }
end
it works fine. It returns all the league chat messages for the league with the id: 1 but it returns the user_id for each message instead of the user name
Use following logic
#data = Message.includes(:user)
Now you can use like below
#data.each do |msg|
puts "Message #{msg.body}"
puts "User #{msg.user.name}"
end
I used puts for understanding but you can use this object in views as you want. And your approach leads to an n+1 query problem, so I used the includes, which helps remove the n+1 query. Try this and let me know if you have any queries.

Spree, Rails LineItems get only those that belongs_to completed Order

I am trying to search for all LineItems that Order is completed.
# /spree/order.rb
def self.complete
where.not(completed_at: nil)
end
I tried:
product.orders.complete.map { |o| o.line_items }.flatten
but it returns an Array and I can't do .where(variant_id: ID).
or:
product.orders.includes(:line_items).complete.where(line_items: { variant_id: 263})
but it says: PG::UndefinedTable: ERROR: missing FROM-clause entry for table "line_items"
Then I tried with:
product.line_items.includes(:order).where(variant_id: ID).where.not(order: { completed_at: nil })
and it returns ActiveRecord::ConfigurationError: Association named 'orders' was not found on Spree::LineItem; perhaps you misspelled it?
This way is ok too: product.orders.complete ... but don't know how to search for LineItems by its variant_id.
How can I solve it? How can I return all Product's LineItems that belongs to completed Order?
Many thanks!
Ahh. Ok, figured out.
Finally solved it with this, notice I changed table name from orders to spree_orders:
product.line_items.joins(:order).where.not(spree_orders: { completed_at: nil })
And, can be done with .includes(:order) but it seems to make longer query and takes a little bit longer than .joins(:order).
You can do it very easily this way:
# in Order Model (order.rb)
scope :completed, -> { where.not(completed_at: nil) }
# in Product Model (product.rb)
has_many :orders
delegate :completed, to: :orders, prefix: true
Now, you will be able to call orders_completed on a product:
product.orders_completed
and, then you can chain any where with this:
product.orders_completed.where(...)

Nested select in rails (SQL to Rails conversion)

I have this rails logic that uses partial SQL query code. I was wondering if there was a way a better way or a cleaner way to do the same thing (i.e. use rails's methods to replace the SQL code)?
#servers = Server
.select("*", "(SELECT AVG('reviews'.'average') FROM 'reviews' WHERE 'reviews'.'server_id' = 'servers'.'id') AS s_avg")
.order("s_avg DESC")
.paginate(:page => params[:page], :per_page => 25)
First good thing is to move that code from view or controller to model and wrap it in scope. Moreover, scopes can be chained.
class Server < ActiveRecord::Base
scope :averaged, -> { where(SQL CODE HERE) }
scope :expensive, -> { where('price > ?', price) }
scope :latest, -> { where('created_at > ?', Date.today - 3.days.ago) }
scope :active, -> { where(active: true) }
end
Only then you can pass and chain it in controller:
#servers = Server.latest.averaged
So, simply try to brake your SQL on several parts, move these parts to model and wrap them with scopes.
You can find a lot of useful examples of query methods without pure SQL here:
http://guides.rubyonrails.org/active_record_querying.html

How do I clear a Model's :has_many associations without writing to the database in ActiveRecord?

For the sake of this question, let's say I have a very simple model:
class DystopianFuture::Human < ActiveRecord::Base
has_many :hobbies
validates :hobbies, :presence => {message: 'Please pick at least 1 Hobby!!'}
end
The problem is that when a human is updating their hobbies on a form and they don't pick any hobbies, there's no way for me to reflect this in the code without actually deleting all the associations.
So, say the action looks like this:
def update
hobbies = params[:hobbies]
human = Human.find(params[:id])
#ideally here I'd like to go
human.hobbies.clear
#but this updates the db immediately
if hobbies && hobbies.any?
human.hobbies.build(hobbies)
end
if human.save
#great
else
#crap
end
end
Notice the human.hobbies.clear line. I'd like to call this to make sure I'm only saving the new hobbies. It means I can also check to see if the user hasn't checked any hobbies on the form.
But I can't do that as it clears the db. I don't want to write anything to the database unless I know the model is valid.
What am I doing wrong here?
Initialy I also did this same way. Then found out one solution for this issue.
You need to do something like this
params[:heman][:hobby_ids]=[] if params[:human][:hobby_ids].nil?
Then check
if human.update_attributes(params[:human])
Hope you will get some idea...
EDIT:
Make hobbies params like this
hobbies = { hobbies_attributes: [
{ title: 'h1' },
{ title: 'h2' },
{ title: 'h3', _destroy: '1' } # existing hobby
]}
if Human.update_atttributes(hobbies) # use this condition
For this you need to declare accepts_nested_attributes_for :hobbies, allow_destroy: true in your Human model.
See more about this here http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
You can try https://github.com/ryanb/nested_form for this purpose..

Is it possible to "add" SQL clauses related to a `scope` method in a `where` method / clause?

I am using Ruby on Rails 3.2.2 and I am experimenting the Squeel gem. I would like to know if (in some way, by using the Squeel gem or not) it is possible to "add" SQL clauses related to a scope method "directly" in a where clause. That is, I have:
class Article < ActiveRecord::Base
# Note: This is a scope method.
def self.created_by(user)
where(:user_id => user.id)
end
# I would like to use a scope method like the following.
#
# Note: Code in the following method doesn't work, but it should help
# understanding what I mean.
def self.scope_method_name(user)
where{ created_by(user) | ... & ... }
end
end
So, when I run Article.scope_method_name(#current_user).to_sql then it should return something like the following:
SELECT articles.* FROM articles WHERE articles.user_id = 1 OR ... AND ...
I tryed sifters but those (at least for me) are intended to be used exclusively in other Squeel statements. That is, if I state a sifter then I cannot use that to scope ActiveRecords because that sifter returns a Squeel::Nodes::Predicate object instead of an ActiveRecord::Relation.
You have to drop down into more raw AREL for OR operations
def self.scope_method_name(user)
t = arel_table
where(
(t[:user_id].eq(user.id).or(
t[:blah].eq('otherthing')
).and([:bleh].eq('thirdthing'))
)
end
Or something along those lines.
You can chain scopes like Article.by_author(user).by_editor() but this joins all the conditions with ANDs. So, to get around this, you can write individual scopes (not chaining them) using Squeel like:
class Article < ActiveRecord::Base
scope :by_author, ->(user) { where{author_id == user.id} }
scope :by_editor, ->(user) { where{editor_id == user.id} }
scope :by_title, ->(token) { where{title =~ "%#{token}%"} }
scope :by_author_or_editor, ->(user) { where{(author_id == user.id)|(editor_id == user.id)} }
scope :by_author_or_editor_and_title, ->(user, token) { where{((author_id == user.id)|(editor_id == user.id))&(title =~ "%#{token}%")} }
end
Or you can use sifters:
class Article < ActiveRecord::Base
sifter :sift_author do |user|
author_id == user.id
end
sifter :sift_editor do |user|
editor_id == user.id
end
sift :sift_title do |token|
title =~ "%#{token}%"
end
scope :by_author, ->(user) { where{sift :sift_author, user} }
scope :by_editor, ->(user) { where{sift :sift_editor, user} }
scope :by_title, ->(token) { where{sift :sift_title, token} }
scope :by_author_or_editor, -> (user) { where{(sift :sift_author, user)|(sift :sift_editor, user)} }
scope :by_author_or_editor_and_title, ->(user, token) { where{((sift :sift_author, user)|(sift :sift_editor, user))&(sift :sift_title, token)} }
end
This gives you your scopes that return an ActiveRecord::Relation, so you can in theory further chain them.