Spree, Rails LineItems get only those that belongs_to completed Order - sql

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(...)

Related

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

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

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.

How would you replace this SQL query in Ruby so that you only use ActiveRecord code?

This is actually working (it returns the 5 artists with the most amount of tracks in a database)
def top_five_artists(genre_name)
# TODO: return the 5 artists with the more tracks of genre `genre_name`
Artist.joins(albums: { tracks: :genre }).where(genres: { name: genre_name }).group(:name).order("COUNT(*) DESC").limit(5)
end
but throws the following warning:
DEPRECATION WARNING: Dangerous query method (method whose arguments
are used as raw SQL) called with non-attribute argument(s): "COUNT(*)
DESC". Non-attribute arguments will be disallowed in Rails 6.1. This
method should not be called with user-provided values, such as request
parameters or model attributes. Known-safe values can be passed by
wrapping them in Arel.sql(). (called from top_five_artists at
/home/maxichalbaud/code/maxichalbaud/fullstack-challenges/03-AR-Database/03-ActiveRecord-Basics/Optional-01-Mapping-Existing-Database/app/queries.rb:18)
How would you refactor the "COUNT(*) DESC" line?
You can try this :
def top_five_artists(genre_name)
Artist.joins(albums: { tracks: :genre })
.where(genres: { name: genre_name })
.group(:name)
.order(Arel.sql('count(*) DESC'))
.limit(5)
end
First, you can use order(Arel.sql('count(*) DESC')) to get rid of the warning of deprecation as mu_is_too_short mentioned above, then, you can write your code in multiple lines so that RuboCop won’t bother you with "This line is too long".

Argument error in model scope

I'm trying to refactor the Companies_Controller#index method to encompass less logic by moving most of the query into a scope, company_search_params.
What is the best way to pass the param to the model scope? I'm getting an error thrown back, wrong number of arguments (given 0, expected 1). I'm relatively new to writing scopes and couldn't find much on passing arguments/conditions that was applicable in the Rails Guide.
Companies Controller
def index
params[:name_contains] ||= ''
Company.company_search_params(params[:name_contains])
#search = Company.company_search_params
#companies = #search.page(params[:page])
end
Company Model
scope :company_search_params, ->(name_contains){
where(
<<-SQL
"name LIKE :match OR subdomain LIKE :match", { match: "%#{name_contains}%" }
SQL
).where(is_archived: false).order(name: :asc)
}
Thanks for your help.
using named_scope sample and info
scope :named_scope, lambda {
|variable1, variable2|
where...
order...
}
#when you call it from your controller
Model.named_scope("value 1","value 2")
for your problem
in your company.rb
scope :company_search_params, lambda {
|name_contains|
where(
<<-SQL
"name LIKE :match OR subdomain LIKE :match", { match: "%#{name_contains}%" }
SQL
).where(is_archived: false).order(name: :asc)
}
company_controller.rb
def index
#search = Company.company_search_params(params[:name_contains])
#companies = #search.page(params[:page])
end

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..