Thinking sphinx index with Multiple joins and conditions - thinking-sphinx

I have 3 models : Users has_one Service and Users Has_many Phones (verified or not, destroyed or not)
Class Phone < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_one :service
has_many :phones
has_many :verified_phones, -> {where('verified_at IS NOT NULL AND destroyed_at IS NULL')}, class_name: 'Phone'
end
class Service < ActiveRecord::Base
belongs_to :user
has_many :phones, through: :user
has_many :verified_phones, through: :user
end
I'd like to define an TS-index on the model Service.
I want an boolean-faceted attribute which represents Service whose user has one or more verified phone.
So after reading this post I tried :
join verified_phones
has 'COUNT(verified_phones.id) > 0', as: :user_phone_verified, type: :boolean, facet: true
but send me the error "Unknown column 'verified_phones.id' in 'field list' " (the same error occured in the post)
Then I tried
join verified_phones
has 'COUNT(phones.id) > 0', as: :user_phone_verified, type: :boolean, facet: true
-> but result are wrong : attribute is true for every user that have a phone, verified or not, destroyed or not.
Then I tried
join phones
has 'COUNT(phones.verified_at IS NOT NULL AND phones.destroyed_at IS NULL) > 0', as: :user_phone_verified, type: :boolean, facet: true
-> same problem : attribute is true for every user that have a phone, verified or not, destroyed or not.
I'm not good with SQL syntax, could anybody help me to solve this ?
Thanks

The second approach is what you're after - even though you're joining on the special association, the table alias will be the table name by default - hence, phones.id.
That said, what's being returned doesn't seem to be right. Perhaps the following instead:
join verified_phones
has "SUM(CASE WHEN phones.id IS NULL THEN 0 ELSE 1 END) > 0",
as: :user_phone_verified, type: :boolean, facet: true

I got it :
I should have been more precise with the syntax of my association definition :
has_many :verified_phones, -> {where('verified_at IS NOT NULL AND destroyed_at IS NULL')}, class_name: 'Phone'
should have been:
has_many :verified_phones, -> {where('phones.verified_at IS NOT NULL AND phones.destroyed_at IS NULL')}, class_name: 'Phone'
My User model has the same attribute "destroyed_at" -> the generated SQL was looking this attribute in the wrong table.
So with this correction both syntax work:
my initial :
join verified_phones
has 'COUNT(phones.id) > 0', as: :user_phone_verified, type: :boolean, facet: true
or the suggested by #Pat :
has "SUM(CASE WHEN phones.id IS NULL THEN 0 ELSE 1 END) > 0",
as: :user_phone_verified, type: :boolean, facet: true
But I'm not very confortable with this because I see possible confusion with this table alias still named "phones". What happen if I need to join in the same index phones and verified_phones?
But for now it solve my problem, so thank you #Pat !

Related

Rails active record object save action fails because of a malformed query

I have a problem in Rails in the part of my app that handles landing pages for registration emails. When a user is invited to the app, an Invitation active record instance is created, and user is sent an email with a link containing the id of the invitation (a random string token).
Then, when the link is clicked, I store that token in session, and at one point in the service layer, I update it's 'status' attribute to 'clicked', something like this:
#invitation = Invitation.find_by_id(session[:registration][:invitation_token])
unless #invitation.blank?
session[:registration][:account_details]['referer'] = #invitation.promoter.username
unless #invitation.status == APP_CONFIG['invitation_status']['clicked']
#invitation.status = APP_CONFIG['invitation_status']['clicked']
#invitation.save
end
end
Upon executing the #invitation.save line, I get an active record error:
!! #<ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'invitations.' in 'where clause': UPDATE `invitations` SET `status` = 'clicked', `updated_at` = '2015-11-11 11:07:24' WHERE `invitations`.`` = 'fd05ee5a-e790-48cc-9e7e-d30d3e88919b'>
The Invitation's id column name seems to be ommited from the query for some reason, and I can't figure out why. Not sure what is wrong or what to do.
Invitation migration:
class CreateInvitations < ActiveRecord::Migration
def change
create_table :invitations, id: false do |t|
t.string :id, limit: 36, :auto_increment => false
t.string :promoter_id
t.string :email
t.string :status
t.timestamps
end
end
end
Invitation model:
class Invitation < ActiveRecord::Base
#region Callbacks
before_create :set_uid
#endregion
belongs_to :promoter, class_name: 'User', foreign_key: 'promoter_id'
private
def set_uid
self.id = SecureRandom.uuid unless self.id
end
end
You should mark the id column as the primary_key:
create_table :invitations, id: false do |t|
t.string :id, limit: 36, primary_key: true, auto_increment: false
...
I think that in your case the best way to solve this problem is to drop the table and create it again with correct migration. But if there is data that you don't want to loose, you can also try to do it like this:
class MarkIdAsPrimaryKey < ActiveRecord::Migration
def change
change_column :invitations, :id, :string, limit: 36, primary_key: true, auto_increment: false
end
end
Keep in mind that this migration is not reversible, it remains under your responsibility if you will use it ) Good luck !

Rails searches the mistaken has_and_belongs_to_many table

I want to show all types which are related to a specific organisation in a select box of my document form. Types are part of the Ar engine. Organisations are part of another existing engine.
module Ar
module OrganisationPatch
extend ActiveSupport::Concern
included do
attr_accessible :ar_document_id
has_many :ar_documents, :class_name => 'Ar::Document'
has_and_belongs_to_many :ar_types, :class_name => 'Ar::Type'
end
end
end
module Ar
class Type < ActiveRecord::Base
attr_accessible :name
has_many :documents
has_and_belongs_to_many :organisations
end
end
class CreateTypeOrganisations < ActiveRecord::Migration
def change
create_table :ar_type_organisations, id: false do |t|
t.uuid :type_id, index: true
t.uuid :organisation_id, index: true
end
end
end
In my documents_controller I load types for forms about the before filter. The superior returns the organisation object:
def load_form_objects
unless current_user.admin?
#types = current_user.superior.ar_types
else
#types = Type.all
end
end
Calling the page I get this error and ask me why he is looking for a table called organisations_types:
ActiveRecord::StatementInvalid in Ar/documents#new
Mysql2::Error: Table 'portal.organisations_types' doesn't exist:
SELECT ar_types.* FROM ar_types INNER JOIN organisations_types
ON ar_types.id = organisations_types.type_id WHERE
organisations_types.organisation_id =
x'891c3986b33845d08d3951645a4f27d5'
Someone knows what I am doing wrong here?
Your table name isn’t map with lexical order what has_and_belongs_to_many expect. ( Expected order is organisations_types )
So you have to add :join_table option in both model's association. Like this,
has_and_belongs_to_many :ar_types, :class_name => 'Ar::Type', join_table: "ar_type_organisations"
has_and_belongs_to_many :organisations, join_table: "ar_type_organisations"
Reference

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.

Converting rails query from Mysql to PostgreSQL

Can anyone tell me how to convert this query to PostgreSQL
routes_controller.rb
#routes = Route.joins([:departure_location, :destination_location]).where("mdm_locations.name like ? or destination_locations_mdm_routes.name like ?" , "%#{k}%", "%#{k}%")
routes.rb (model)
module Mdm
class Route < ActiveRecord::Base
belongs_to :uom
belongs_to :distance_uom, :class_name => "Uom", :foreign_key => "distance_uom_id"
belongs_to :location
belongs_to :departure_location, :class_name => "Location", :foreign_key => "departure"
belongs_to :destination_location, :class_name => "Location", :foreign_key => "destination"
has_many :voyages, :dependent => :restrict
attr_accessible :description, :distance, :distance_uom_id, :departure, :std_consm, :destination, :uom_id
validates_presence_of :departure, :destination
end
end
Error :
PG::Error: ERROR: operator does not exist: integer = character varying
LINE 1: ...NNER JOIN "mdm_locations" ON "mdm_locations"."id" = "mdm_rou...
^
HINT: No operator matches the given name and argument type(s). You might need to add explicit type casts.
: SELECT COUNT(*) FROM "mdm_routes" INNER JOIN "mdm_locations" ON "mdm_locations"."id" = "mdm_routes"."departure" INNER JOIN "mdm_locations" "destination_locations_mdm_routes" ON "destination_locations_mdm_routes"."id" = "mdm_routes"."destination" WHERE (LOWER(mdm_locations.name) like '%futong%' or LOWER(destination_locations_mdm_routes.name) like '%futong%')
Your error message says:
operator does not exist: integer = character varying
and points you at this part of the SQL:
INNER JOIN "mdm_locations" ON "mdm_locations"."id" = "mdm_routes"."departure"
-- ------------------------------------------------^
Combining those tells us that mdnm_locations.id is an integer (as expected) but mdm_routes.departure is a varchar. You can't compare integers and strings in SQL without explicitly casting one of them to make the types compatible.
You need to fix your schema, mdm_routes.departure should be an integer column, not a string.
MySQL tries to be friendly by attempting to guess your intent and lets you get away with a lot of sloppy practices. PostgreSQL tries to be friendly by forcing you to say exactly what you mean to avoid confusions, incorrect guesses, and hidden bugs.

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.