id nil when select model without no relation - ruby-on-rails

I have a model with no relation.
class GridConfig < ActiveRecord::Base
end
my question is why result of query are appended id:nil columns?
GridConfig.select(:fontSize)
result is
#<GridConfig id: nil, fontSize: "12px">
is there any options for this?
thank you.
I want find some records and pick certain columns. and send to client.
user_key = params[:user_key]
grid_id = params[:grid_id]
#config = GridConfig.where(['user_key = ? and grid_id = ?', user_key, grid_id])
.select(:model_id, :fontSize, :displayCount, :columnModel)
# i checked #config variables at this point and found nil:id...
#config = #config.index_by(&:model_id)
# and i want to this makes indexed by model_id like [{"model":{...}},{"model2" : {...}}, {}]
respond_to do |format|
format.json { render json: #config }
end

You can use the pluck method to select only certain columns into an array, then index by the first column.
#config = GridConfig.where(user_key: params[:user_key], grid_id: params[:grid_id])
.pluck(:model_id, :fontSize, :displayCount, :columnModel)
#config = #config.index_by{ |x| x[0] }

Related

Too many checks for empty params. How to optimize queries to ActiveRecord in Rails5?

I'm doing checks for empty parameters before do the query.
There is only 1 check for params[:car_model_id]. I can imagine if I will add more checks for other params, then there will be a mess of if-else statements. It doesn't look nice and I think it can be optimized. But how? Here is the code of controller:
class CarsController < ApplicationController
def search
if params[:car_model_id].empty?
#cars = Car.where(
used: ActiveRecord::Type::Boolean.new.cast(params[:used]),
year: params[:year_from]..params[:year_to],
price: params[:price_from]..params[:price_to],
condition: params[:condition]
)
else
#cars = Car.where(
used: ActiveRecord::Type::Boolean.new.cast(params[:used]),
car_model_id: params[:car_model_id],
year: params[:year_from]..params[:year_to],
price: params[:price_from]..params[:price_to],
condition: params[:condition]
)
end
if #cars
render json: #cars
else
render json: #cars.errors, status: :unprocessable_entity
end
end
end
The trick would be to remove the blank values, do a little bit of pre-processing (and possibly validation) of the data, and then pass the params to the where clause.
To help with the processing of the date ranges, you can create a method that checks both dates are provided and are converted to a range:
def convert_to_range(start_date, end_date)
if start_date && end_date
price_from = Date.parse(price_from)
price_to = Date.parse(price_to)
price_from..price_to
end
rescue ArgumentError => e
# If you're code reaches here then the user has invalid date and you
# need to work out how to handle this.
end
Then your controller action could look something like this:
# select only the params that are need
car_params = params.slice(:car_model_id, :used, :year_from, :year_to, :price_from, :price_to, :condition)
# do some processing of the data
year_from = car_params.delete(:year_from).presence
year_to = car_params.delete(:year_to).presence
car_params[:price] = convert_to_range(year_from, year_to)
price_from = car_params.delete(:price_from).presence
price_to = car_params.delete(:price_to).presence
car_params[:price] = convert_to_range(price_from, price_to)
# select only params that are present
car_params = car_params.select {|k, v| v.present? }
# search for the cars
#cars = Car.where(car_params)
Also, I'm pretty sure that the used value will automatically get cast to boolean for you when its provided to the where.
Also, #cars is an ActiveRecord::Relation which does not have an errors method. Perhaps you mean to give different results based on whether there are any cars returned?
E.g: #cars.any? (or #cars.load.any? if you don't want to execute two queries to fetch the cars and check if cars exist)
Edit:
As mentioned by mu is too short you can also clean up your code by chaining where conditions and scopes. Scopes help to move functionality out of the controller and into the model which increases re-usability of functionality.
E.g.
class Car > ActiveRecord::Base
scope :year_between, ->(from, to) { where(year: from..to) }
scope :price_between, ->(from, to) { where(price: from..to) }
scope :used, ->(value = true) { where(used: used) }
end
Then in your controller:
# initial condition is all cars
cars = Cars.all
# refine results with params provided by user
cars = cars.where(car_model_id: params[:car_model_id]) if params[:car_model_id].present?
cars = cars.year_between(params[:year_from], params[:year_to])
cars = cars.price_between(params[:price_from], params[:price_to])
cars = cars.used(params[:used])
cars = cars.where(condition: params[:condition]) if params[:condition].present?
#cars = cars

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:

Sort! seems to ignore custom attribute

I have this method to place 'The' at the end of a string if it is at the beginning of the string in Novel.class:
def sort_name
display_name = self.name
if display_name.match(/^the/i)
arr = display_name.split(/^the/i)
display_name = "#{arr[1]}, The"
end
display_name
I have this index method in NovelController:
def index
#novels = Novel.all
#novels.to_a.sort! { |a,b| a.sort_name.downcase <=> b.sort_name.downcase }
respond_to do |format|
format.html # index.html.erb
format.json { render :json => #novels }
end
end
In the view I just display the sort_name. The sort_name is being displayed but the novels are still ordered by name. Does anybody see a flaw? Thanks.
The problem here is that sorted array is discarded.
#novels.to_a
This returns a temporary array (which isn't saved anywhere). That temp array is then sorted in-place and forgotten, because you don't have any references to it.
Solution: save it into a variable.
#novels = Novel.all.sort { |a,b| a.sort_name.downcase <=> b.sort_name.downcase }
Also, you have a bug in your sort_name code. It returns values like these:
# for name "The Yellow God"
display_name # => " Yellow God, The"

Repacking query result from multiple models. Rails-way

I have a controller that renders json. Here's code:
class AppLaunchDataController < ApiController
def index
service_types = []
vendors = []
tariffs = []
fields = []
vendors_hash = {}
service_types_hash = {}
tariffs_hash = {}
fields_hash = {}
#service_types = ServiceType.select("title, id").all.each do |service_type|
service_types_hash = {id: service_type.id, title: service_type.title}
service_types << service_types_hash
#vendors = service_type.vendors.select("title, id").all.each do |vendor|
vendors_hash = {id: vendor.id, title: vendor.title}
vendors << vendors_hash
#tariff = vendor.tariffs.select("title, id").all.each do |tariff|
tariffs_hash = {id: tariff.id, title: tariff.title}
tariffs << tariffs_hash
#fields = tariff.fields.select("id, current_value, value_list").all.each do |field|
fields_hash = {id: field.id, current_value: field.current_value, value_list: field.value_list}
fields << fields_hash
end
tariffs_hash[:fields] = fields
fields = []
end
vendors_hash[:tariffs] = tariffs
tariffs = []
end
service_types_hash[:vendors] = vendors
vendors = []
end
render json: service_types
end
end
Return value looks like this:
[{"id":1,"title":"Water",
"vendors":[{"id":1,"title":"Vendor_1",
"tariffs":[{"id":1,"title":"Unlim",
"fields":[{"id":1,"current_value":"200","value_list":null},{"id":2,"current_value":"Value_1","value_list":"Value_1, Value_2, Value_3"}]},{"id":2,"title":"Volume",
"fields":[]}]},
{"id":2,"title":"Vendor_2",
"tariffs":[]}]},
{"id":2,"title":"Gas",
"vendors":[]},
{"id":3,"title":"Internet",
"vendors":[]}]
It works, but I'm sure there's another (more rails-) way to get the result.
If anyone dealt with it before, please help. Thanks.
just use
# for eager-loading :
#service_types = ServiceType.includes( vendors: {tariffs: :fields} )
# now for the json :
#service_types.to_json( include: {vendors: {include: {tariffs: { include: :fields}}}} )
if your ServiceType object will always have this kind of representation, just override the model's as_json method:
class ServiceType
def as_json( options={} )
super( {include: :vendors }.merge(options) ) # vendors, etc.
end
end
this is encouraged way to do it in rails : calling to_json on the model will just call as_json, possibly with additional options. In fact, as_json describes the canonical json representation for this model. See the api dock on to_json for more insight.
If your needs are more peculiar ( as using selects for a faster query ), you can always roll your own to_json_for_app_launch_data method on the model (using or not as_json), or even better on a presenter

Add extra filter parameters in RoR

Suppose I have a method in a controller:
def my_find(is_published, count)
items = Idea.where(published: is_published)
#......
end
Sometimes I want to pass some extra filter arguments
def my_find(is_published, count, some_extra_filter = nil)
items = Idea.where(published: is_published) #.where (some_extra_filter)
#......
end
where some_extra_filter can be lambda or just an plain sql "where" string and it can also be nil or "".
So how do I concatenate .where(published: is_published) with where (some_extra_filter) to get what I need?
This is actually very easy using scopes:
def my_find
#items = Idea.scoped
#items = #items.where(published: is_published) unless is_published.nil?
#items = #items.where(other: other_param) if other_params < 10
# etc, etc
end

Resources