Aliasing the source table - ruby-on-rails

Is there a way to alias the source table in the context of a single scope ?
I tried this :
scope = User.all
scope.arel.source.left.table_alias = "toto"
scope.where(firstname: nil) => "SELECT `toto`.* FROM `users` `toto` WHERE `toto`.`firstname` IS NULL"
The problem is that the model class keeps the alias for all subsequent queries :
User.all => "SELECT `toto`.* FROM `users` `toto`"
Edit
I added this method to ApplicationRecord
def self.alias_source(table_alias)
klass = Class.new(self)
klass.all.source.left.table_alias = table_alias
klass
end
Now, I can do :
User.alias_source(:toto).where(firstname: nil) => "SELECT `toto`.* FROM `users` `toto` WHERE `toto`.`firstname` IS NULL"

I added this method to ApplicationRecord
def self.alias_source(table_alias)
klass = Class.new(self)
klass.all.source.left.table_alias = table_alias
klass
end
Now, I can do :
User.alias_source(:toto).where(firstname: nil) => "SELECT `toto`.* FROM `users` `toto` WHERE `toto`.`firstname` IS NULL"

Since creating Anonymous Classes that inherit from ActiveRecord::Base will cause memory bloat, I am not sure I would recommend it (See Here: https://github.com/rails/rails/issues/31395).
Maybe a better implementation would be to execute in a block form instead e.g. yield the aliased table to the block then set it back afterwards?
e.g.
def self.with_table_alias(table_alias, &block)
begin
self.all.source.left.table_alias = table_alias
block.call(self)
ensure
self.all.source.left.table_alias = nil
end
end
Usage
User.with_table_alias(:toto) do |scope|
puts scope.joins(:posts).where(firstname: "Ruur").to_sql
end
# "SELECT `toto`.*
# FROM
# `users` `toto`
# INNER JOIN `posts` ON `posts`.`user_id` = `toto`.`id`
# WHERE
# `toto`.`firstname` = 'Ruur'"
puts User.all.to_sql
# "SELECT `users`.* FROM `users`
WARNING: THIS HAS NOT BE TESTED BEYOND WHAT IS SHOWN HERE AND I WOULD NOT RECOMMEND THIS IN A PRODUCTION ENVIRONMENT WITHOUT EXTENSIVE TESTING FOR EDGE CASES AND OTHER GOTCHAS
Update To address desired implementation
module ARTableAlias
def alias_table_name=(val)
#alias_table_name = val
end
def alias_table_name
#alias_table_name ||= "#{self.table_name}_alias"
end
def alias_source
#alias_klass ||= Class.new(self).tap do |k|
k.all.source.left.table_alias = alias_table_name
end
end
end
Then usage as:
class User < ApplicationRecord
extend ARTableAlias
alias_table_name = :toto
end
User.alias_source.where(first_name = 'Ruur')
#=> SELECT `toto`.* FROM `users` `toto` WHERE `toto`.`first_name` = 'Ruur'
User.where(first_name = 'Ruur')
#=> SELECT `users`.* FROM `users` WHERE `users`.`first_name` = 'Ruur'
User.alias_source === User.alias_source
#=> true

Related

sort_link asc desc not working for associated relation

I have a method full_name in owner.rb, I want to sort owner asc and desc in properties index, either it does asc or will do desc, but in this case, I have tried many ways to sort this but none of this is working, I'm quite new using ransack, can anyone please let me how to make this work with associated relations, here property belongs to owner and owner has many properties.
def full_name
[first_name,last_name].join(" ").squish
end
which I am calling in properties index
td.special-td
- unless property.owner.nil?
a href=manager_user_owner_petty_cash_path(property.owner.id) #{property.owner.full_name}
I am trying to apply sort_link on the table column:
th = sort_link(#q, :owners_full_name, "Landlord")
while this sorting is not working here is my properties index controller
def index
authorize Property
if !current_user.manager?
unit_ids = current_user.unit_ids
total_units_limit = " units.id in (#{unit_ids.join(',')}) or properties.user_id = #{current_user.id} "
properties_ids = company.properties.joins(:units).where(total_units_limit).group(:id).pluck(:id).join(",")
if properties_ids.blank?
properties_ids = 0
end
owners_limit = " properties.id in (#{properties_ids}) "
end
pagination_limit_check
#q = company.properties.joins("left join units on units.property_id = properties.id ").includes(:units, :owner, :contracts, :tags)
.where(total_units_limit).ransack(params[:q])
#q.sorts = 'name asc' if #q.sorts.empty?
self.properties = #q.result(distinct: true).page(params[:page]).per(#page_limit)
self.owners = Owner.joins(:properties).where(owners_limit)
end

Rails active record where chaining losing scope

Model Food has scope expired:
Food.rb
class Food < ApplicationRecord
default_scope { where.not(status: 'DELETED') }
scope :expired, -> { where('exp_date <= ?', DateTime.now) }
belongs_to :user
end
In my controller I'm chaining where conditions to filter foods by user and status:
query_type.rb
def my_listing_connection(filter)
user = context[:current_user]
scope = Food.where(user_id: user.id)
if filter[:status] == 'ARCHIVED'
# Line 149
scope = scope.where(
Food.expired.or(Food.where(status: 'COMPLETED'))
)
else
scope = scope.where(status: filter[:status])
end
scope.order(created_at: :desc, id: :desc)
# LINE 157
scope
end
Here is the rails log:
Food Load (2.7ms) SELECT `foods`.* FROM `foods` WHERE `foods`.`status` !=
'DELETED'
AND ((exp_date <= '2020-07-02 09:58:16.435609') OR `foods`.`status` = 'COMPLETED')
↳ app/graphql/types/query_type.rb:149
Food Load (1.6ms) SELECT `foods`.* FROM `foods` WHERE `foods`.`status` != 'DELETED'
AND `foods`.`user_id` = 1 ORDER BY `foods`.`created_at` DESC, `foods`.`id` DESC
↳ app/graphql/types/query_type.rb:157
Why does active records query loses expired scope (and a condition) in line 157?
It is ignored because where doesn't expect scopes like that. But you can use merge instead. Replace
scope = scope.where(
Food.expired.or(Food.where(status: 'COMPLETED'))
)
with
scope = scope.merge(Food.expired)
.or(Food.where(status: 'COMPLETED'))
or
scope = scope.where(status: 'COMPLETED').or(Food.expired)

Error when combining search scopes

I have a web-service that allows clients to search for articles with query parameters.It works fine if only one parameter is included but fails if I combine search_query and category. This is based on Comfortable_Mexican_Sofa where for_category is found. Even if I remove the order statement i get this error.
error
PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY
expressions must appear in select list LINE 1:
...ms_categories"."label" = 'Company News' ORDER BY pg_search_...
^ : SELECT DISTINCT "comfy_cms_pages".* FROM "comfy_cms_pages" INNER JOIN
"comfy_cms_categorizations" ON
"comfy_cms_categorizations"."categorized_id" = "comfy_cms_pages"."id"
AND "comfy_cms_categorizations"."categorized_type" = $1 INNER JOIN
"comfy_cms_categories" ON "comfy_cms_categories"."id" =
"comfy_cms_categorizations"."category_id" INNER JOIN (SELECT
"comfy_cms_pages"."id" AS pg_search_id,
(ts_rank((to_tsvector('simple',
coalesce("comfy_cms_pages"."content_cache"::text, '')) ||
to_tsvector('simple', coalesce("comfy_cms_pages"."label"::text, ''))),
(to_tsquery('simple', ''' ' || 'austin' || ' ''' || ':')), 0)) AS
rank FROM "comfy_cms_pages" WHERE (((to_tsvector('simple',
coalesce("comfy_cms_pages"."content_cache"::text, '')) ||
to_tsvector('simple', coalesce("comfy_cms_pages"."label"::text, '')))
## (to_tsquery('simple', ''' ' || 'austin' || ' ''' || ':')))))
pg_search_comfy_cms_pages ON "comfy_cms_pages"."id" =
pg_search_comfy_cms_pages.pg_search_id WHERE (layout_id = '1' AND
is_published = 't') AND "comfy_cms_categories"."label" = 'Company
News' ORDER BY pg_search_comfy_cms_pages.rank DESC,
"comfy_cms_pages"."id" ASC, "comfy_cms_pages"."created_at" DESC
app/models/article.rb
class Article < Comfy::Cms::Page
cms_is_categorized
include PgSearch
pg_search_scope :search_by_keywords, against: [:content_cache, :label], using: { tsearch: { any_word: true, prefix: true } }
app/commands/search_articles_command.rb
class SearchArticlesCommand
def initialize(params = {})
#since = params[:since_date]
#keys = params[:search_query]
#category = params[:category]
end
def execute
Article.unscoped do
query = if #since.present?
Article.article.since_date(#since)
else
Article.published_article
end
query = query.for_category(#category) if #category.present?
query = query.search_by_keywords(#keys) if #keys.present?
query.where('').order(created_at: :desc)
end
end
end
comfortable-mexican-sofa/lib/comfortable_mexican_sofa/extensions/is_categorized.rb
module ComfortableMexicanSofa::IsCategorized
def self.included(base)
base.send :extend, ClassMethods
end
module ClassMethods
def cms_is_categorized
include ComfortableMexicanSofa::IsCategorized::InstanceMethods
has_many :categorizations,
:as => :categorized,
:class_name => 'Comfy::Cms::Categorization',
:dependent => :destroy
has_many :categories,
:through => :categorizations,
:class_name => 'Comfy::Cms::Category'
attr_accessor :category_ids
after_save :sync_categories
scope :for_category, lambda { |*categories|
if (categories = [categories].flatten.compact).present?
self.distinct.
joins(:categorizations => :category).
where('comfy_cms_categories.label' => categories)
end
}
end
end
module InstanceMethods
def sync_categories
(self.category_ids || {}).each do |category_id, flag|
case flag.to_i
when 1
if category = Comfy::Cms::Category.find_by_id(category_id)
category.categorizations.create(:categorized => self)
end
when 0
self.categorizations.where(:category_id => category_id).destroy_all
end
end
end
end
end
ActiveRecord::Base.send :include, ComfortableMexicanSofa::IsCategorized
Updated Error
PG::SyntaxError: ERROR: syntax error at or near "."
LINE 4: ...e = 'Class' AND categorized_id = 'comfy_cms_pages'.'id' AND ...
^
: SELECT "comfy_cms_pages".* FROM "comfy_cms_pages" INNER JOIN (SELECT "comfy_cms_pages"."id" AS pg_search_id, (ts_rank((to_tsvector('simple', coalesce("comfy_cms_pages"."content_cache"::text, '')) || to_tsvector('simple', coalesce("comfy_cms_pages"."label"::text, ''))), (to_tsquery('simple', ''' ' || 'austin' || ' ''' || ':*')), 0)) AS rank FROM "comfy_cms_pages" WHERE (((to_tsvector('simple', coalesce("comfy_cms_pages"."content_cache"::text, '')) || to_tsvector('simple', coalesce("comfy_cms_pages"."label"::text, ''))) ## (to_tsquery('simple', ''' ' || 'austin' || ' ''' || ':*'))))) pg_search_comfy_cms_pages ON "comfy_cms_pages"."id" = pg_search_comfy_cms_pages.pg_search_id WHERE "comfy_cms_pages"."layout_id" = $1 AND "comfy_cms_pages"."is_published" = $2 AND (
EXISTS (
SELECT 1 FROM categorizations
WHERE categorized_type = 'Class' AND categorized_id = 'comfy_cms_pages'.'id' AND category_id IN (2)
)) ORDER BY pg_search_comfy_cms_pages.rank DESC, "comfy_cms_pages"."id" ASC
working solution but not a scope and have to be careful of order its being called
def self.for_category(_category)
Comfy::Cms::Categorization.includes(:category).references(:category).select(:categorized).pluck(:categorized_id)
find(ids)
end
I think it's best to override for_category built-in filter of your CMS. Too many joins in that query.
Override for_category like this:
scope :for_category, lambda { |*categories|
if (categories = [categories].flatten.compact).present?
self_ids = "{connection.quote_table_name(self.table_name)}.#{connection.quote_column_name(self.primary_key)}"
self.where(
"EXISTS (" +
Comfy::Cms::Categorization.select('1').
where(categorized_type: self.name).
where('categorized_id' => self_ids).
where(category_id: Comfy::Cms::Category.where(label: categories).pluck(:id)).to_sql +
")"
)
end
}
More on SQL EXISTS usage in Rails you can read in my Rails: SQL EXISTS brief how-to.
More on why you bump into that error you can read in question and answer here.
Specifically, pg_search wants order your results by rank. And for_category wants to select distinct fields of Article only, and doesn't care about search rank. Changing its code to use simple EXISTS instead of complex JOIN query will fix that.
I was able to solve this problem by applying reorder to the result of the pg_search result.
search_command.rb
class SearchArticlesCommand
def initialize(params = {})
#since = params['since_date']
#keys = params['search_query']
#category = params['category']
end
def execute
Article.unscoped do
query = Article.article
query = if #since.present?
query.since_date(#since)
else
query.published
end
query = query.for_category(#category) if #category.present?
query = query.search_by_keywords(#keys).reorder('updated_at DESC') if #keys.present?
query
end
end
end
I also overrode for_category (not required)
article.rb
scope :for_category, (lambda do |category|
published
.joins(:categories)
.group(:id)
.where('comfy_cms_categories.label' => category)
.select('comfy_cms_pages.*')
end)

Virtual Column to count record

First, sorry for my English, I am totally new in ruby on rails even in very basic thing, so I hope you all can help me.
I have table Role and RoleUser
table Role have has_many relationship to RoleUser with role_id as foreign key
in table RoleUser is contain user_id, so I can call it 1 role have many users
and I want is to show all record in Role with additional field in every record called total_users,
total_users is in every record have role_id and count the user_id for every role, and put it in total_users,
I know this is must use the join table, but in rails I absolutely knew nothing about that, can you all give me a simple example how to do that.
and one more, same with case above, can I do for example Role.all and then the total_users in include in that without added it in database? is that use virtual column?
anyone have a good source of link to learn of that
I have following code in model
def with_filtering(params, holding_company_id)
order = []
if params[:sort].present?
JSON.parse(params[:sort]).each do |data|
order << "#{data['property']} #{data['direction']}"
end
end
order = 'id ASC' if order.blank?
if self.column_names.include? "holding_company_id"
string_conditions = ["holding_company_id = :holding_company_id"]
placeholder_conditions = { holding_company_id: holding_company_id.id }
else
string_conditions = []
placeholder_conditions = {}
end
if params[:filter].present?
JSON.parse(params[:filter]).each do |filter|
if filter['operation'] == 'between'
string_conditions << "#{filter['property']} >= :start_#{filter['property']} AND #{filter['property']} <= :end_#{filter['property']}"
placeholder_conditions["start_#{filter['property']}".to_sym] = filter['value1']
placeholder_conditions["end_#{filter['property']}".to_sym] = filter['value2']
elsif filter['operation'] == 'like'
string_conditions << "#{filter['property']} ilike :#{filter['property']}"
placeholder_conditions["#{filter['property']}".to_sym] = "%#{filter['value1']}%"
else
string_conditions << "#{filter['property']} = :#{filter['property']}"
placeholder_conditions["#{filter['property']}".to_sym] = filter['value1']
end
end
end
conditions = [string_conditions.join(' AND '), placeholder_conditions]
total_count = where(conditions).count
if params[:limit].blank? && params[:offset].blank?
data = where(conditions).order(order)
else
data = where(conditions).limit(params[:limit].to_i).offset(params[:offset].to_i).order(order)
end
return data, total_count.to_s
end
And I have follwing code in controllers
def crud_index(model)
data, total = Role.with_filtering(params, current_holding_company)
respond_to do |format|
format.json { render json: { data: data, total_count: total }.to_json, status: 200 }
end
end
My only purpose is to add virtual field called total_users, but i want added it in model and combine it with data in method with_filtering
If you have the models like this:
Class Role < ActiveRecord::Base
has_many :role_users
end
Class RoleUser < ActiveRecord::Base
belong_to :role
end
You could use select and joins to generate summary columns, but all the Role's attributes should be include in group.
roles = Role.select("roles.*, count(role_users.id) as total_users")
.joins(:role_users)
.group("roles.id")
Type those scripts in Rails console, Rails will generate a sql like :
SELECT roles.id, count(role_users.id) as total_users
FROM roles
INNER JOIN role_users
ON roles.id = role_users.role_id
GROUP BY roles.id
Then you can use roles.to_json to see the result. The summary column total_users can be accessed in every member of roles.
And there are many other way can match your requirement. Such as this. There is a reference of counter cache.
My suggestion is after searching, you can test those method by rails console, it's a useful tool.
UPDATE
According to OP's update and comment, seems you have more works to do.
STEP1: move with_filtering class method to controller
with_filtering handle a lot of parameter things to get conditions, it should be handled in controller instead of model. So we can transfer with_filtering into conditions and orders in controller.
class RolesController < ApplicationController
def conditions(params, holding_company_id)
if self.column_names.include? "holding_company_id"
string_conditions = ["holding_company_id = :holding_company_id"]
placeholder_conditions = { holding_company_id: holding_company_id.id }
else
string_conditions = []
placeholder_conditions = {}
end
if params[:filter].present?
JSON.parse(params[:filter]).each do |filter|
if filter['operation'] == 'between'
string_conditions << "#{filter['property']} >= :start_#{filter['property']} AND #{filter['property']} <= :end_#{filter['property']}"
placeholder_conditions["start_#{filter['property']}".to_sym] = filter['value1']
placeholder_conditions["end_#{filter['property']}".to_sym] = filter['value2']
elsif filter['operation'] == 'like'
string_conditions << "#{filter['property']} ilike :#{filter['property']}"
placeholder_conditions["#{filter['property']}".to_sym] = "%#{filter['value1']}%"
else
string_conditions << "#{filter['property']} = :#{filter['property']}"
placeholder_conditions["#{filter['property']}".to_sym] = filter['value1']
end
end
end
return [string_conditions.join(' AND '), placeholder_conditions]
end
def orders(params)
ord = []
if params[:sort].present?
JSON.parse(params[:sort]).each do |data|
ord << "#{data['property']} #{data['direction']}"
end
end
ord = 'id ASC' if ord.blank?
return ord
end
end
STEP2: update action crud_index with conditions and orders to get total_count of Roles.
class AnswersController < ApplicationController
def crud_index(model)
total = Role.where(conditions(params, current_holding_company)).count
if params[:limit].blank? && params[:offset].blank?
data = Role.where(conditions(params, current_holding_company)).order(orders(params))
else
data = Role.where(conditions(params, current_holding_company)).limit(params[:limit].to_i).offset(params[:offset].to_i).order(orders(params))
end
respond_to do |format|
format.json { render json: { data: data, total_count: total }.to_json, status: 200 }
end
end
end
STEP3: update action crud_index to get total_users by every role.
Make sure the two previous steps is pass the test.
class AnswersController < ApplicationController
def crud_index(model)
total = Role.where(conditions(params, current_holding_company)).count
if params[:limit].blank? && params[:offset].blank?
data =
Role.select(Role.column_names.map{|x| "Roles.#{x}"}.join(",") + " ,count(role_users.id) as total_users")
.joins(:role_users)
.group(Role.column_names.map{|x| "Roles.#{x}"}.join(","))
.where(conditions(params, current_holding_company))
.order(orders(params))
else
data =
Role.select(Role.column_names.map{|x| "Roles.#{x}"}.join(",") + " ,count(role_users.id) as total_users")
.joins(:role_users)
.group(Role.column_names.map{|x| "Roles.#{x}"}.join(","))
.where(conditions(params, current_holding_company))
.order(orders(params))
.limit(params[:limit].to_i)
.offset(params[:offset].to_i).order(orders(params))
end
respond_to do |format|
format.json { render json: { data: data, total_count: total }.to_json, status: 200 }
end
end
end
NOTE: step3 may need you to modify conditions and orders method to generate column_name with table_name prefix to avoid column name ambiguous error
If you can make these steps through, I suggest you can try will_paginate to simplify the part of your code about total_count ,limit and offset.
With what you explained, you could do something like this:
class Role < ActiveRecord::Base
has_many :role_users
has_many :users
def total_users
self.users.count
end
end
So you just need to call the total_users method on roles object which should get you what you desire. Something like this:
Role.first.total_users
# this will give you the total users for the first role found in your database.
Hope it helps
You might want to watch this Railscast too:
#app/models/role.rb
Class Role < ActiveRecord::Base
has_many :role_users
has_many :users, -> { select "users.*", "role_users.*", "count(role_users.user_id) as total_users" }, through: :role_users
end
This will allow you to call:
#roles = Role.find params[:id]
#roles.users.each do |role|
role.total_users
end
You can see more about how this works with a question I wrote some time ago - Using Delegate With has_many In Rails?
--
It's where I learnt about Alias columns, which Ryan Bates uses to count certain values:

Render json and sort issue

This following is my model, For easier reading, I cut some code:
class Meeting < ActiveRecord::Base
def Joiners
# Never mind , this is a complex sql, i think you do not need to read it.
Joiners = Ploy.connection.select_all("SELECT count(users.id) as joiner FROM `ploys` INNER JOIN `participants` ON `participants`.`ploy_id` = `ploys`.`id` INNER JOIN `users` ON `users`.`id` = `participants`.`user_id` where `participants`.`ploy_id`= #{self.id}")
Joiners.rows[0][0]
end
def as_json(options={})
super(methods: :Joiners)
end
If i use
render json: #Meeting
It will render a JSON with Joiners attribute .
{
"Meeting":{
...
"Joiners":15,
...
}
"Meeting":{
...
"Joiners":13,
...
}
}
So:
How can i sort by Joiners ?
Thanks
You'll have to order it in a query result by using SQL ORDER BY

Resources