MySQL / Ruby on Rails - How to "SUM" in a :has_many case - mysql

I have the following tables:
User :has_many Purchases
Item :has_many Purchases
Item has a column "amount" (can be + or -) and I need to find all Users that have a positive SUM of "Item.amounts" (over all Purchases each one has made).
How does this query look like? (I'm not sure how to handle "SUM" correctly, in this case.)
I started out with the following, but obviously, it's wrong... (it wouldn't "include" Purchases that have an Item with a negative Item.amount...)
#users = User.find(:all,
:include => {:purchases => :item},
:select => "SUM(item.amount)",
:order => "...",
:conditions => "...",
:group => "users.id",
:having => "SUM(item.amount) > 0"
)
Thanks for your help with this!
Tom

Try this:
User.all(:joins => items, :group => "users.id",
:having => "SUM(items.amount) > 0")

It sounds like this is a good case for some model methods.
I didn't test this but I think you want to do something similar to the following:
class User < ActiveRecord::Base
has_many :purchases
has_many :items, :through => :purchases
def items_total
#get all the items iterate over them to get the amount,
#compact to get rid of nils
#and reduce with a sum function to total and return
items.all.each{|item| item.amount}.compact.reduce(:+)
end
then
User.items_total

Related

Thinking Sphinx : Multiple indices search

I want to create two indices for the same model and search separately
I am using
gem 'thinking-sphinx', '3.2.0'
gem 'riddle', '1.5.11'
ThinkingSphinx::Index.define :product, :with => :active_record, :delta => ThinkingSphinx::Deltas::DelayedDelta do
indexes :field_a
end
ThinkingSphinx::Index.define :product, :name => "active_product", :with => :active_record, :delta => ThinkingSphinx::Deltas::DelayedDelta do
indexes :field_a
where "(active = 1)"
end
when i tried to search this way to get only the active products
Product.search_for_ids "", :match_mode => :extended, :index => "active_product_core, active_product_delta", :page => params[:page], :per_page => 50, :sort_mode => :extended, :order => "field_a desc"
But it is running query like this and listing all products
SELECT * FROM `product_core`, `product_delta` WHERE `sphinx_deleted` = 0 ORDER BY `field_a` desc LIMIT 0, 50 OPTION max_matches=50000
How can i get only the active products or to make sure query runs like this?
SELECT * FROM `active_product_core`, `active_product_delta` WHERE `sphinx_deleted` = 0 ORDER BY `field_a` desc LIMIT 0, 50 OPTION max_matches=50000
Note: Above feature was working fine in Thinking sphinx version 2
gem 'thinking-sphinx', '2.0.14'
gem 'riddle', '1.5.3'
In TS v3, the search option is now :indices rather than :index, and expects an array of index names. So, try the following:
Product.search_for_ids(
:indices => ["active_product_core", "active_product_delta"],
:page => params[:page],
:per_page => 50,
:order => "field_a desc"
)
I've removed :sort_mode and :match_mode from the options you were using - the extended approaches are the only approaches available with Sphinx's SphinxQL protocol (and that's what TS v3 uses), so you don't need to specify them.

Efficently querying multi language categories and category items

So I have a bit of a server response time issue - which I think is caused due to obsolete queries. One major query chain that I have takes up to 370ms, which is obviously causing an issue.
Here are the requirements:
5 Different languages
There are several Product Categories (i.e. Cat 1, Cat 2, Cat 3, etc.)
Categories displayed depend on language. For example whilst category 1 is displayed in all languages, category 2 is only displayed in Germany and France but not in the UK
Each category contains x number of items (has_many belongs_to relationship). Again some items are displayed in certain languages others are not. For example even category 2 is displayed in France and Germany, only in Germany you can buy Item 1 and hence Item 1 should not be displayed in France but Germany.
The categories and items do have boolean fields named after the locale. This way I can set via flag whether or not to display the category and item in a specific language.
My solution:
Building the solution is quiet easy. In controller I read out all the categories for the current locale:
application_controller.rb (since it is used on every single page)
#product_categories = ProductCategory.where("lang_" + I18n.locale.to_s + " = ?", true)
And in the view (the navigation) I do the following:
layouts/navs/productnav.html.haml
- #product_categories.each do |category|
...
- category.products.includes(:product_teasers).where("lang_" + I18n.locale.to_s + " = ? AND active = ?", true, true).in_groups_of(3).each do |group|
...
The issue with this solution is that each time I fire a lot of queries towards the database. Using "includes" does not solve it as I can not specify what items to pull. Furthermore I require the in_groups_of(3) in my loop to display the items correctly on the page.
I was also looking into memchached solutions to have the queries cached all together - i.e. Dalli however, this would require me to change a lot of code as I am guessing I would require to query all categories for each language and cache them. In addition to it I have to query each item for each langugage depending on language and store that somehow in an array ?!
My question:
How to approach this ? There must be a simpler and more efficient solution. How to efficiently query respectively cache this?
Thank you!
UPDATE:
As requested here are my two Models:
1.) ProductCategory
class ProductCategory < ActiveRecord::Base
translates :name, :description, :slug, :meta_keywords, :meta_description, :meta_title, :header, :teaser, :fallbacks_for_empty_translations => true
extend FriendlyId
friendly_id :name, :use => [:globalize, :slugged]
globalize_accessors :locales => [:at, :de, :ch_de, :ch_fr, :fr, :int_en, :int_fr], :attributes => [:slug]
has_paper_trail
has_many :products, :dependent => :destroy
validates :name, presence: true, uniqueness: { case_sensitive: false }
default_scope { includes(:translations) }
private
def slug_candidates
[
[:name]
]
end
end
2.) Product
And every product Category can have 0..n Products, and each Product must belongs to one category.
class Product < ActiveRecord::Base
translates :slug, :name, :meta_keywords, :meta_description, :meta_title, :teaser, :power_range, :product_page_teaser, :product_category_slider_teaser, :fallbacks_for_empty_translations => true
extend FriendlyId
friendly_id :name, :use => :globalize
before_save :change_file_name
searchable do
text :name, :teaser, :product_page_teaser, :product_category_slider_teaser
integer :product_category_id
end
belongs_to :product_category
has_many :product_teasers, :dependent => :destroy
has_many :product_videos, :dependent => :destroy
has_many :product_banners, :dependent => :destroy
has_many :product_documents, :dependent => :destroy
has_many :product_tabs, :dependent => :destroy
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent: :destroy
has_many :passive_relationships, class_name: "Relationship",
foreign_key: "followed_id",
dependent: :destroy
has_many :following, through: :active_relationships, source: :followed
has_many :followers, through: :passive_relationships, source: :follower
has_many :references
has_and_belongs_to_many :contacts
accepts_nested_attributes_for :product_teasers, :reject_if => :all_blank, :allow_destroy => true
accepts_nested_attributes_for :product_tabs, :reject_if => :all_blank, :allow_destroy => true
accepts_nested_attributes_for :product_videos, :reject_if => :all_blank, :allow_destroy => true
accepts_nested_attributes_for :product_banners, :reject_if => :all_blank, :allow_destroy => true
accepts_nested_attributes_for :product_documents, :reject_if => :all_blank, :allow_destroy => true
has_paper_trail
validates :name, presence: true, uniqueness: { case_sensitive: false }
default_scope {includes(:translations)}
.... a lot more going on here ...
end
Please note: That category contains language flags (booleans), i.e lang_at, lang_de, lang_fr, etc. and if set then this category is displayed in that particualar language. SAME applies to products, as certain products are not displayed in all langauges altough the category might be.
Examples:
#product_categories = ProductCategory.where("product_categories.lang_" + I18n.locale.to_s + " = ?", true)
#products = Product.where("product_categories.lang_" + I18n.locale.to_s + " = ?", true)
I skipped any includes on purpose above - it is just to demonstrate the language logic.
UPDATE
The system have spent a lot of times to loop data in nested loop. Avoid to fetch data in nested loop. You have to use join or includes to catch your data more effective. For example:
Controller
#products = Product.includes(:product_category).where("product_categories.lang_" + I18n.locale.to_s + " = ? AND product_categories.active = ?", true, true).group_by(&:category)
View
- #products.each do |category, products|
<%= category.name %>
- products.each do |product|
<%= product.title %>
It needs to fix with your necessary code. I just help the main query. For example: active = ? is for products field or product_categories field. I hope It can help you.

Rails search a model based on multiple parameters using a form

So... I've been working on creating a search form for a rails application. I've gone through the railscast episodes 37, 111, and 112.
While the simple text search with a text input field works. I need to be able to define more parameters to refine the search.
I've found a few other methods, some using scopes...I keep running into issues getting any of these working in my application....
What I have right now is a simple form defined on my home index that points at my assets index:
<% form_tag assets_path, :method => 'get' do %>
<%= text_field_tag :search, params[:search] %>
<%= collection_select(:type_id, :type_id, Type.where("type_for = 'asset'"), :id, :name) %>
<%= submit_tag "Search", :search => nil %>
<% end %>
my asset.rb model:
class Asset < ActiveRecord::Base
has_many :children_assets, :class_name => "Asset"
has_and_belongs_to_many :groups, :join_table => "assets_groups"
belongs_to :parent_asset,
:class_name => "Asset",
:foreign_key => "parent_asset_id"
belongs_to :asset_type,
:class_name => "Type",
:conditions => "type_for = 'asset'"
belongs_to :asset_status,
:class_name => "Status",
:conditions => "status_for = 'asset'"
belongs_to :location
belongs_to :funding_source
has_many :transactions
def self.search(search)
if search
find(:all, :conditions => ['nmc_name LIKE ? AND type_id = ?', "%#{search}%", "%#{search}"])
else
find(:all)
end
end
end
in the asset_controller.rb
def index
unless params[:search].nil?
#title = "Assets"
#search = params[:search]
#assets = Asset.search(params[:search]).paginate(page: params[:page], per_page: 25)
else
#title = "Assets"
#assets = Asset.where('').paginate(page: params[:page], per_page: 25)
end
end
I just dont understand what it is that I'm not seeing here. I can run a similar mysql query and get the result I want. I just dont know how to format this in rails...
Any guidance on this would be amazing right now. Thanks!
It looks as though you're trying to search for a specific type of asset, but your search method in the Asset model is only using one of the user supplied parameters.
Judging by the form you're using, your controller will be receiving the params
params = {
search: 'Search Text',
type_id: 1
}
In your controller, you're only using search, so I'd change your method to include this:
#assets = Asset.search(params[:search], params[:type_id]).paginate(page: params[:page], per_page: 25)
Then amend the Assets model to use it
def self.search(search, type_id)
if search
find(:all, :conditions => ['nmc_name LIKE ? AND type_id = ?', "%#{search}%", "%#{type_id}"])
else
find(:all)
end
end

Rails database specific queries for Heroku

I've been deploying some apps to Heroku recently. I run MySQL on my local dev machine and have spent a little while updating some of my scopes to work in PostgreSQL. However one i have received an error on is proving difficult to change.
For the time being i've got a database specific case statement in my model. I understand why the error regarding the MySQL date functions is occurring, but im not sure if this is the most efficient solution. Does anyone have a better way of implementing a fix that will work with both MySQL and PostgreSQL?
case ActiveRecord::Base.connection.adapter_name
when 'PostgreSQL'
named_scope :by_year, lambda { |*args| {:conditions => ["published = ? AND (date_part('year', created_at) = ?)", true, (args.first)], :order => "created_at DESC"} }
named_scope :by_month, lambda { |*args| {:conditions => ["published = ? AND (date_part('month', created_at) = ?)", true, (args.first)], :order => "created_at DESC"} }
named_scope :by_day, lambda { |*args| {:conditions => ["published = ? AND (date_part('day', created_at) = ?)", true, (args.first)], :order => "created_at DESC"} }
else
named_scope :by_year, lambda { |*args| {:conditions => ["published = ? AND (YEAR(created_at) = ?)", true, (args.first)], :order => "created_at DESC"} }
named_scope :by_month, lambda { |*args| {:conditions => ["published = ? AND (MONTH(created_at) = ?)", true, (args.first)], :order => "created_at DESC"} }
named_scope :by_day, lambda { |*args| {:conditions => ["published = ? AND (DAY(created_at) = ?)", true, (args.first)], :order => "created_at DESC"} }
end
FYI, this is the PostgreSQL error that i am getting:
PGError: ERROR: function month(timestamp without time zone) does not exist LINE 1: ...T * FROM "articles" WHERE (((published = 't' AND (MONTH(crea... ^ HINT: No function matches the given name and argument types. You might need to add explicit type casts. : SELECT * FROM "articles" WHERE (((published = 't' AND (MONTH(created_at) = '11')) AND (published = 't' AND (YEAR(created_at) = '2010'))) AND ("articles"."published" = 't')) ORDER BY created_at DESC LIMIT 5 OFFSET 0
Thanks in advance for any input anyone has.
You should be using the standard EXTRACT function:
named_scope :by_year, lambda { |*args| {:conditions => ["published = ? AND (extract(year from created_at) = ?)", true, (args.first)], :order => "created_at DESC"} }
Both PostgresSQL and MySQL support it.
Unfortunately this happens alot, however you have the general right idea.
Your first method of attack is to see if there is a function that exists both in MySQL and Postres, however this isn't possible in this case.
The one suggestion I would make is that there is a lot of code duplication in this solution. Considering the condition statement is the only compatible issue here, I would factor out the compatiablity check only for the condition:
Example (Semi-Psuedo Code):
named_scope :by_year, lambda { |*args| {:conditions => ["published = ? AND (#{by_year_condition} = ?)", true, (args.first)], :order => "created_at DESC"} }
#...code...
def by_year_condition
if postgres
"date_part('year', created_at)"
else
"YEAR(created_at)"
end
Another option would be to create computed columns for each of your date parts (day, month, and year) and to query directly against those. You could keep them up to date with your model code or with triggers. You'll also get the benefit of being able to index on various combinations on your year, month, and day columns. Databases are notoriously bad at correctly using indexes when you use a function in the where clause, especially when that function is pulling out a portion of data from the middle of the column.
The upside of having three separate columns is that your query will no longer rely on any vendor's implementations of SQL.

Tricky MySQL Query for messaging system in Rails - Please Help

I'm writing a facebook style messaging system for a Rails App and I'm having trouble selecting the Messages for the inbox (with will_paginate).
The messages are organized in threads, in the inbox the most recent message of a thread will appear with a link to it's thread. The thread is organized via a parent_id 1-n relationship with itself.
So far I'm using something like this:
class Message < ActiveRecord::Base
belongs_to :sender, :class_name => 'User', :foreign_key => "sender_id"
belongs_to :recipient, :class_name => 'User', :foreign_key => "recipient_id"
has_many :children, :class_name => "Message", :foreign_key => "parent_id"
belongs_to :thread, :class_name => "Message", :foreign_key => "parent_id"
end
class MessagesController < ApplicationController
def inbox
#messages = current_user.received_messages.paginate :page => params[:page], :per_page => 10, :order => "created_at DESC"
end
end
That gives me all the messages, but for one thread the thread itself and the most recent message will appear (and not only the most recent message). I can also not use the GROUP BY clause, because for the thread itself (the parent so to say) the parent_id = nil of course.
Anyone got an idea on how to solve this in an elegant way? I already thought about adding the parent_id to the parent itself and then group by parent_id, but I'm not sure if that works.
Thanks
My solution would be to get a list of threads (which I'm assuming could be obtained by messages with no parent id). Then on the Message model, add a method that will find the latest message in the thread and return it. You can then use that method to obtain the latest method in each thread and put in a link to the head of the thread easily.
(Pseudo-)code:
class Message < ActiveRecord::Base
belongs_to :sender, :class_name => 'User', :foreign_key => "sender_id"
belongs_to :recipient, :class_name => 'User', :foreign_key => "recipient_id"
has_many :children, :class_name => "Message", :foreign_key => "parent_id"
belongs_to :thread, :class_name => "Message", :foreign_key => "parent_id"
def get_last_message_in_thread()
last_message = self
children.each do |c|
message = c.get_last_message_in_thread()
last_message = message if message.created_at > last_message.created_at
end
return last_message
end
end
class MessagesController < ApplicationController
def inbox
#messages = current_user.received_messages.find_by_parent_id(Null).paginate :page => params[:page], :per_page => 10, :order => "created_at DESC"
end
end
You could probably do a lot better than having a recursive function to find the last message in the thread, but it's the simplest solution I can think of to demonstrate the idea. I'm also not sure I have the correct syntax for finding unset parent id's in the inbox function, which is why I marked the code as pseudo code :)
giving the parent itself as parent makes it very easy to create queries that operate on the whole thread, because you can group (or anything similar) by parent_id.
if you handle the parents differently, all your queries have to take care of this too
The only efficient way would be to have a Thread model and use GROUP BY as you mentioned - Anything else would require iteration over the messages.
read update in comments
I figured the only good solution is using a second model to store the most recent messages for every thread (because of performance issues when using GROUP BY with a subselect, see my comments). It won't take a lot of space in the DB because we're only storing id's and no text or even blobs.
The RecentMessages Model would look something like this:
create_table :recent_messages do |t|
t.integer :sender_id
t.integer :recipient_id
t.integer :message_id
t.integer :message_thread_id
t.timestamps
end
class RecentMessage < ActiveRecord::Base
belongs_to :message
belongs_to :message_thread, :class_name => 'Message'
belongs_to :sender, :class_name => 'User', :foreign_key => "sender_id"
belongs_to :recipient, :class_name => 'User', :foreign_key => "recipient_id"
end
The main idea is: All the messages are stored in one model (Messages). Whenever a new message is added to a thread (or a thread is created), two things happen (e.g. with a after_save callback):
Store the new message in the RecentMessages model (that means sender_id, recipient_id, message_id, message_thread_id (= parent_id || id))
Get the most recent message (from this thread in messages), where sender_id == recipient_id and vice versa (note: This only works if the message model should only support messages between 2 users) and store it in the RecentMessages model as well (if found and if it's not already there)
Of course there should only be max. 2 recent_messages stored in the DB for every message_thread at any given time.
If one wants to show i.e. the inbox, the following has to happen:
#messages = current_user.recent_received_messages.paginate :page => params[:page], :per_page => 10, :order => "created_at DESC", :include => :message
That's the best I figured out so far. I still think it's ugly but it's fast and it works. If anyone comes up with a better solution, I'll be gratefull!
I don't know how to accomplish this in Rails, but this is how I did it directly in MySQL:
select * from messages where message_id in (
select max(message_id) from messages where to_uid = 51 group by thread_id
) order by timestamp desc
I used a subquery to grab the most recent message in a thread and then the main query to grab all of the fields for the messages found in the subquery.