Improve Efficiency of Ruby Model Queries - ruby-on-rails-3

Problem:
In my customer model, I want to sum up all transactions where each payments' status is successful and either the customer_id matches the id of the current instance or the current instance's parent_id. First, I was unable to get this to work at all because I was trying to do it all in one query. Now I have what I'm looking for, but I'm afraid I'm not being nearly as efficient as I could be.
Context:
The transactions table has a customer_id column
the customers table has a parent_id column.
Customer class has_many :transactions
Transactions class belongs_to :customer
Transactions class has_many :payments
Here are my models:
class Customer < ActiveRecord::Base
has_many :transactions
end
class Transaction < ActiveRecord::Base
belongs_to :customer
has_many :payments
end
class Payment < ActiveRecord::Base
has_one :transaction
end
Question:
If this is the model property I'm using currently, how can I improve its performance, if at all?
def total_spent
if customer_type == 1 # "child" customer
Transaction.joins(:payments).where('payments.status' => 3).sum(:amount, :conditions => ['customer_id = ?', id])
elsif customer_type == 2 # "parent" customer
temp_transactions = Transaction.joins(:payments).where('payments.status' => 3)
temp = temp_transactions.sum(:amount, :conditions => ['customer_id = ?', id])
Customer.find_all_by_parent_group_id(id).each do |c|
temp = temp + temp_transactions.sum(:amount, :conditions => ['customer_id = ?', c.id])
end
temp
end
end

Transaction.joins(:payments)
.joins(:customer)
.where(:payments => {:status => 3})
.where("customers.id = ? or customers.parent_id = ?", 5, 5)
.sum(:amount)
SELECT SUM(amount) AS sum_id
FROM "transactions"
INNER JOIN "payments" ON "payments"."transaction_id" = "transactions"."id"
INNER JOIN "customers" ON "customers"."id" = "transactions"."customer_id"
WHERE
"payments"."status" = 3
AND (customers.id = 5 or customers.parent_id = 5)

Related

How to group by attribute but display value of nested attribute?

I've got these models in my Rails 6 application:
class Client < ApplicationRecord
belongs_to :account
has_many :people
end
class Person < ApplicationRecord
belongs_to :client
end
class Payment < ApplicationRecord
belongs_to :client
end
In my SharesController I am trying to generate the total payments for each client and show them as a pie chart:
class SharesController < ApplicationController
def index
#clients = current_account.clients.joins(:payments)
.where(:payments => {:date => #range, :currency => #currency})
.order("sum_payments_#{#part} DESC")
.group("clients.id", "clients.name")
.having("sum_payments_#{#part} > 0")
.sum("payments.#{#part}")
end
end
The problem with this is that it groups by client correctly. However, rather than showing each client's name I want to show the last_name of each client's first nested person.
How can this be done?
Thanks for any help.
You should try to create a join between Client and Person and then use uniq to avoid duplicated clients.
You could try something like this (I'm not sure if this code works but just to make it clearer what I mean)
#clients = current_account.clients.joins(:payments, :people)
.where(:payments => {:date => #range, :currency => #currency})
.order("sum_payments_#{#part} DESC")
.group("clients.id", "people.last_name")
.having("sum_payments_#{#part} > 0")
.sum("payments.#{#part}")

ActiveRecord query based on multiple objects via has_many relationship

I have a Product class that has_many Gender through Connection class instances. I want to query to find products that have both end_a and end_b present. The current class method works with 2 caveats:
Fails to return correctly if searching where end_a and end_b are the same. Instead should search if product has 2 instances, not just one of object.
Returns an Array when I want an ActiveRecord_Relation.
The class method .query is below, any feedback or ideas are appreciated.
class Product < ActiveRecord::Base
has_many :connections, dependent: :destroy, as: :connectionable
has_many :genders, through: :connections
def self.query(end_a, end_b)
search_base = active.joins(:connections)
end_a_search = search_base.where(connections: { gender_id: end_a } )
end_a_search & search_base.where(connections: { gender_id: end_b } )
end
end
ps: Once this is figured out will likely move this to a scope for Product
class Product < ActiveRecord::Base
has_many :connections, dependent: :destroy, as: :connectionable
has_many :genders, through: :connections
scope :with_genders, -> (end_a, end_b) {
relation = joins('INNER JOIN connections c1 ON c1.connectionable_id = products.id AND c1.connectionable_type = \'Product\'')
.joins('INNER JOIN connections c2 ON c1.connectionable_id = c2.connectionable_id AND c2.connectionable_type = \'Product\'')
.where(c1: {gender_id: end_a}, c2: {gender_id: end_b})
.group('products.id')
end_a == end_b ? relation.having('COUNT(products.id) > 1') : relation
}
end

has_many through instead of join query

I have relationship between User models defined through Friendship model. (ROR 4)
User
class User < ActiveRecord::Base
has_many :friendships, ->(object) { where('user_id = :id OR friend_id = :id', id: object.id) }
has_many :friends, ->(object) { where(friendships: {status: 'accepted'}).where('user_id = :id OR friend_id = :id', id: object.id) }, through: :friendships, source: :friend
has_many :requested_friends, -> { where(friendships: {status: 'pending'}) }, through: :friendships, source: :friend
end
Friendship
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, class_name: 'User'
def self.request(user, friend)
unless user == friend or find_friendship(user, friend) != nil
create(user: user, friend: friend, status: 'pending')
end
end
def self.find_friendship(user, friend)
ids = [user.id, friend.id]
where(user_id: ids, friend_id: ids).first
end
end
However, this does not work and my tests are failing because of SQL queries produced.
Friendships relation
> user.friendships
Query:
SELECT "friendships".* FROM "friendships"
WHERE "friendships"."user_id" = ?
AND (user_id = 1 OR friend_id = 1) [["user_id", 1]]
So part of WHERE before AND "kills" my actual where. I made a workaround by making instance method:
def friendships
self.class
.select('friendships.* FROM `friendships`')
.where('user_id = :id OR friend_id = :id', id)
end
Is there a way I can remove my instance method and modify has_many relation to produce the SQL I want?
Requested_friends relation
> Friendship.request(user, friend)
> friend.requested_friends
Query:
SELECT "users".* FROM "users"
INNER JOIN "friendships" ON "users"."id" = "friendships"."friend_id"
WHERE "friendships"."status" = 'pending'
AND "friendships"."user_id" = ?
AND (user_id = 2 OR friend_id = 2) [["user_id", 2]]
It obviously isn't what I need so I made a workaround by removing has_many :requested_friends and making an instance method:
def requested_friends
self.class
.joins('JOIN `friendships` friendships ON users.id = friendships.user_id')
.where('friendships.status = ?', 'pending')
.where('friendships.friend_id = ?', id)
end
Is there any way I can modify my has_many :requested_friends relation to produce same SQL as my instance method?
Very confusing - I'd do something like this:
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :friendships, class_name: "user_friendships", association_foreign_key: "user_id", foreign_key: "friend_id",
has_many :friends, class_name: "User", through: :friendships
end
#app/models/user_friendship.rb
Class UserFriendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, class_name: "User"
end
You'd have a join table which looks like this:
user_friendships
id | user_id | friend_id | other | info | created_at | updated_at
This should work (I'm not sure about the self referential association). If it does, it will allow you to call:
#user.friends
I hope this helps?
You might also benefit from this gem
you cannot achieve the SQL you want using has_many method with condition. The reason is that the block you pass to the method is only additional condition, on top of the standard query which checks if user_id = ?.
Instead you can simplify your instance method a little bit
def friendships
Friendship.where('user_id = :id or friend_id = :id', id)
end

rails nested has_many with foreign scope join misses to join table: sql error

A self running template with all models to test by yourself is available in this github gist - run it and it triggers the error.
To visualize it, the structure looks like this:
Colors
|n|
|:|
|1| -----
Houses n:n Conditions
|n| -----
|:|
|1|
People
Starting from the blank db I create some test data (console commands, return values omitted to keep it clear):
irb(main):001:0> Condition.create(condition: :damaged)
irb(main):002:0> house = House.create(conditions: [Condition.first])
irb(main):003:0> person = Person.create
irb(main):004:0> house.person = person
irb(main):005:0> house.save
So now I have some test data. Let's retrieve the person's houses (which are only the damaged ones by definition):
irb(main):006:0> person.damaged_houses
House Load (0.2ms)
SELECT "houses".* FROM "houses"
INNER JOIN "conditions_houses" ON "conditions_houses"."house_id" = "houses"."id"
INNER JOIN "conditions" ON "conditions"."id" = "conditions_houses"."condition_id"
WHERE "houses"."person_id" = ? AND "conditions"."condition" = 'damaged'
[["person_id", 1]]
=> #<ActiveRecord::Associations::CollectionProxy [#<House id: 1, person_id: 1>]>
All good, the damaged house is returned and the sql joined the conditions table correctly. Now I want to get all colors of the person, which is defined as all colors of the houses, where the houses are still only the damaged ones. This should return an empty collection (since no colors are in the db yet).
irb(main):007:0> person.damaged_colors
Color Load (0.4ms)
SELECT "colors".* FROM "colors"
INNER JOIN "houses" ON "colors"."house_id" = "houses"."id"
WHERE "houses"."person_id" = ? AND "conditions"."condition" = 'damaged'
[["person_id", 1]]
SQLite3::SQLException: no such column: conditions.condition:
SELECT "colors".* FROM "colors"
INNER JOIN "houses" ON "colors"."house_id" = "houses"."id"
WHERE "houses"."person_id" = ? AND "conditions"."condition" = 'damaged'
It's clear from the sql string that the join table conditions is missing and therefore conditions.condition is not available. If I see it correctly, simply this string from the query before is missing:
INNER JOIN "conditions_houses" ON "conditions_houses"."house_id" = "houses"."id"
INNER JOIN "conditions" ON "conditions"."id" = "conditions_houses"."condition_id"
So the query should be:
SELECT "colors".* FROM "colors"
INNER JOIN "houses" ON "colors"."house_id" = "houses"."id"
INNER JOIN "conditions_houses" ON "conditions_houses"."house_id" = "houses"."id"
INNER JOIN "conditions" ON "conditions"."id" = "conditions_houses"."condition_id"
WHERE "houses"."person_id" = ? AND "conditions"."condition" = 'damaged'
Is this a rails bug or am I doing it wrong? Why is the join conditions missing?
The code:
class Color < ActiveRecord::Base
belongs_to :house
end
class Condition < ActiveRecord::Base
has_and_belongs_to_many :houses
end
class House < ActiveRecord::Base
has_many :colors
belongs_to :person
has_and_belongs_to_many :conditions
scope :damaged, -> { joins(:conditions).where(:'conditions.condition' => 'damaged') }
end
class Person < ActiveRecord::Base
has_many :damaged_houses, -> { damaged }, :class_name => "House"
has_many :damaged_colors, through: :damaged_houses, :source => :colors
end
So I was able to get your gist to run by modifying the following:
has_many :damaged_colors, through: :damaged_houses, :source => :colors
to
has_many :damaged_colors, -> { joins({house: :conditions}).where(:'conditions.condition' => 'damaged') }, through: :damaged_houses, :source => :colors
It does appear to be an ActiveRecord bug on HABTM associations as alluded to in the comments... Hope this helps in the mean time...
Funny thing was, I converted your gist to a hm:hm and had the same issue...

Ruby ActiveRecord has_one with conditions

Let's say I have some Items for sale, and I'm keeping track of their Cost historically:
class Cost < ActiveRecord::Base
belongs_to :item
# eg: costs.amount = 123.45; costs.item_id = 1; costs.created_at = 2011-08-11 16:28
end
class Item < ActiveRecord::Base
has_many :costs
# eg: items.id = 1; items.name = Cheese Sandwich
end
This code works, I can pull out all the previous costs for the item I'm selling.
I feel like it should be possible to have a second clause for Item so that I can pull out the current price directly:
class Item < ActiveRecord::Base
has_many :costs
has_one :current_cost, :class_name => :costs, :conditions => 'MAX(created_at)'
end
my_item.current_cost # => <£123.45, 45 minutes ago>
Any ideas how to achieve this?
class Item < ActiveRecord::Base
has_many :costs
def current_cost
self.costs.order("created_at DESC").first
end
end
my_item.current_cost
has_one :current_cost, :class_name => :costs, :order => 'create_at DESC'
You can use the scope:
class Item < ActiveRecord::Base
has_many :costs
scope :current_cost, limit(1).order("created_at DESC")
end
usage:
my_item.current_cost