Rails API: as_json with includes query the DB despite previous includes - ruby-on-rails

I am struggling to find the best way to render records. So far, I did it the following way but, despite having includes when I fetch the main object, I get tons of DB queries when calling as_json for the child record that was included. What am I missing? Is there even a better way too do what I want to do?
I don't see how to have a better render since I want to decide what attributes and methods to serialise and use custom scopes on arrays of associate records.
My controller
def show
# The include below seems to be useless, the DB is queried again on render.
#grandParent = GrandParent.includes(parents: { children: %i[grand_children friends] })
.find_by_name(params[:name])
return head :not_found unless #grandParent
render json: grand_parent_as_json, status: :ok
end
private
def grand_parent_as_json
json = #grandParent.as_json(
only: %i[attr1 attr2],
methods: %i[meth1 meth2]
)
# I don't see a better way to render it since I want to use a custom scope on parents
json[:parents] = #grandParent.parents.ordered_by_birthdate(:desc).map do |parent|
parent_as_json parent
end
json
end
# The include below seem to be the one responsible for querying the DB again.
def parent_as_json(parent)
parent.as_json(
only: %i[attr1 attr2],
methods: %i[meth1 meth2],
include: [
children: {
only: %i[attr1 attr2],
include: [
grand_children: { %i[attr1 attr2] }
]
}
]
)
end

Pretty sure there is a more elegant way to fix this but the issue is indeed the scope being used here:
#grandParent.parents.ordered_by_birthdate(:desc)
The reason being is the scope is guaranteed to return a new ActiveRecord::Relation which when accessed hits the DB.
It may not be the best answer but it will work by changing your initial query, to include an .order for the birthdate field.
#grandParent = GrandParent
.includes(parents: { children: %I[grand_children friends] })
.order("parents.birthdate DESC")
.find_by_name(params[:name])
Then remove the .ordered_by_birthdate as you map the parent objects as they are already in the order you wanted. This has the disadvantage not using the scope ordered_by_birthdate defined on Parent. This might be ok depending on how you view responsibilities of controller vs model.
Alternatively the above code snippet could also be part part of a scope on GrandParent e.g.
class GrandParent
scope :family_tree, -> { includes(parents: { children: %I[grand_children friends] }).order("parents.birthdate DESC") }
end
Then you could do:
GrandParent.family_tree.find_by_name(params[:name])

Related

Rails controller variable mess

I have a controller that I feel has too many instance variables.
The controller is pulling data from various places and it feels really sloppy.
I have watched some Sandi Metz talks, read books, and other research, and I want to have good practice but I just don't know what to do here.
This method is pulling all the data and sending it to my view and I am able to get it to work, I just know this isn't a good way to go about it and I am hoping someone can point me to some code samples, documentation, videos, or help me understand how to implement a better style.
I have searched on SO and Google but I mostly find people saying to send a hash or JSON to the view, and I want to know if that is ideal before I start on that.
The Client, Project, Person, Role controllers and models have really similar code and I am working on refactoring it to be more DRY.
For example the Client, Project, Person, and Role financial controllers have almost the exact same controller index code as this. :(
I would be happy to add more code if that would help!
This is the project_financials_controller#index
It's pretty much taking in the data from the view and pulling a bunch of data from the database and sending it to a view. I'm currently using only the index method because it was only supposed to be a 'view' but now we can add filters such as time, different clients, etc so I think I need to break it out somehow.
I do have a financial_reports_nav model that this is calling that I could maybe use more, Or even make a financial_reports_controller that pulls the data from the appropriate model and I wont even need the 4 different controllers...
I am totally open to any input/criticism!
def index
# CPPR = Client, Project, Person, Role
#financial_type = 'project'
#financial_params = params
# This pulls the timeframe from the view and figures out the dates requested. (eg. "Last Week")
#timeframe = Financial.time_frame(#financial_params[:timeframe], current_company.timezone, params[:start_date], params[:end_date])
# This grabs all the data required to recall this financial report view at a later time
#financial_nav = FinancialReportNav.set_financial_type(#current_user.id,#financial_type, #start_date, #end_date)
# Grab all active and inactive people for client
#people = Person.active.all
#deleted_people = Person.inactive.all
# This sends over all the info needed to generate the financial reports
#project_financial_populate = Financial.new(#financial_params, #financial_type).populate_project_financials(current_company.default_hourly_cost, current_company.billing_rate, #timeframe[:start_date],#timeframe[:end_date])
# This just pulls all the data from the database that the #project_financial_populate just populated (Can't we just use that??)
#financial_rows = ProjectFinancial.all.map { |p| [ p.project_id, p.billable_hours, p.revenue,p.real_rate, p.hourly_expense, p.labor_expense_total, p.salary_expense, p.gross_profit, p.profit_margin, p.missing_hourly_expense, p.missing_billable_rate ] }
# Using the same view for CPPR's
# Clients has an items count, so we just stuff everything into the first array slot
#items = [1]
# If these are not null then they show an option to change the financial filter type.
#filter_by_client = Client.find_by('id = ?', #financial_params[:filter_by_client])
#filter_by_project = Project.find_by('id = ?', #financial_params[:filter_by_project])
#filter_by_person = Person.find_by('id = ?', #financial_params[:filter_by_person])
#filter_by_role = PersonRole.find_by('id = ?', #financial_params[:filter_by_role])
# This pulls a list of CPPR's that have tracked time in the requested timeframe
#project_list = Financial.project_list(#timeframe[:start_date], #timeframe[:end_date])
#client_list = Financial.client_list(#timeframe[:start_date], #timeframe[:end_date])
#people_list = Financial.people_list(#timeframe[:start_date], #timeframe[:end_date])
end
I always tend to refactor code to be DRY whenever I noticed I have at least 3 instances of duplicate code, but I needed to future-proof the new code to be flexible enough for possible future changes; all of this considered however time permits.
Given your already current code and having told my preferences, this is what I would do:
Model Inheritance
Controller Inheritance
Shared template
Routes
config/routes.rb
resources :client_financial
resources :project_financial
resources :person_financial
resources :role_financial
Models
app/models/financial_record.rb
class FinancialRecord < ActiveRecord::Base # or ApplicationRecord if > Rails 5
self.abstract_class = true
# your shared "financials" model logic here
end
app/models/client_financial.rb
class ClientFinancial < FinancialRecord
# override "financials" methods here if necessary
# or, add new model specific methods / implementation
end
app/models/project_financial.rb
class ProjectFinancial < FinancialRecord
# override "financials" methods here if necessary
# or, add new model specific methods / implementation
end
app/models/person_financial.rb
class PersonFinancial < FinancialRecord
# override "financials" methods here if necessary
# or, add new model specific methods / implementation
end
app/models/role_financial.rb
class RoleFinancial < FinancialRecord
# override "financials" methods here if necessary
# or, add new model specific methods / implementation
end
Controllers
app/controllers/financial_controller.rb
class FinancialController < ApplicationController
before_action :set_instance_variables, only: :index
protected
def set_instance_variables
# strips the last "Controller" substring and change to underscore: i.e. ProjectFinancialsController becomes project_financials
#financial_type = controller_name[0..(-'Controller'.length - 1)].underscore
# get the corresponding Model class
model = #financial_type.camelcase.constantize
# get the correspond Financial Model class
financial_model = "#{#financial_type.camelcase}Financial".constantize
#financial_params = params
#timeframe = Financial.time_frame(#financial_params[:timeframe], current_company.timezone, params[:start_date], params[:end_date])
# I dont know where you set #start_date and #end_date
#financial_nav = FinancialReportNav.set_financial_type(#current_user.id,#financial_type, #start_date, #end_date)
# renamed (or you can set this instance variable name dynamically)
#records = model.active.all
# renamed (or you can set this instance variable name dynamically)
#deleted_records = model.inactive.all
#financial_populate = Financial.new(#financial_params, #financial_type).populate_project_financials(current_company.default_hourly_cost, current_company.billing_rate, #timeframe[:start_date],#timeframe[:end_date])
#financial_rows = financial_model.all.map { |p| [ p.project_id, p.billable_hours, p.revenue,p.real_rate, p.hourly_expense, p.labor_expense_total, p.salary_expense, p.gross_profit, p.profit_margin, p.missing_hourly_expense, p.missing_billable_rate ] }
#items = [1]
#filter_by_client = Client.find_by('id = ?', #financial_params[:filter_by_client])
#filter_by_project = Project.find_by('id = ?', #financial_params[:filter_by_project])
#filter_by_person = Person.find_by('id = ?', #financial_params[:filter_by_person])
#filter_by_role = PersonRole.find_by('id = ?', #financial_params[:filter_by_role])
#project_list = Financial.project_list(#timeframe[:start_date], #timeframe[:end_date])
#client_list = Financial.client_list(#timeframe[:start_date], #timeframe[:end_date])
#people_list = Financial.people_list(#timeframe[:start_date], #timeframe[:end_date])
end
end
app/controllers/client_financials_controller.rb
class ClientFinancialsController < FinancialController
def index
render template: 'financials/index'
end
end
app/controllers/project_financials_controller.rb
class ProjectFinancialsController < FinancialController
def index
render template: 'financials/index'
end
end
app/controllers/person_financials_controller.rb
class ProjectFinancialsController < FinancialController
def index
render template: 'financials/index'
end
end
app/controllers/role_financials_controller.rb
class ProjectFinancialsController < FinancialController
def index
render template: 'financials/index'
end
end
Views
app/views/financials/index.html.erb
<!-- YOUR SHARED "FINANCIALS" INDEX HTML HERE -->
P.S. This is just a simple refactor. Without knowing the fuller scope of the project, and future plans, I'll just do this one. Having said this, I would consider using "polymorpic" associations, and then just have one routes endpoint (i.e. resources :financials) and then just pass in a params filter like: params[:financial_type] which directly already map the financial_type polymorphic column name.

assigning and reusing variables in Rails - design pattern

I've been using the same pattern for returning json code (see example below). I'm getting a collection of photos and storing it in the variable. If tag param is present, I'm getting a more specific collection and reassigning it to the same variable. Then returning it as json. What would be a better design pattern to achieve the same thing?
photos = collection_of_photos
if params[:tag]
photos = photos.find_all {|photo| some condition}
end
render json: photos
If the photos are ActiveRecord objects you should use a scope to generate the appropriate query for the exact data you need. Otherwise, Rails will load all of the photos instead of the subset you need. Assuming your photos records have a single tag attribute, you can do something like the following:
class Photo < ApplicationRecord
scope :with_tag, (tag) -> { where(tag: tag) }
end
In your controller you'd only need the call that scope since ActiveRecord scopes return an #all scope if no parameters are provided:
render json: Photo.with_tag(params[:tag])
# Equivalent when params[:tag] is nil
render json: Photo.all
Now say you're not dealing with ActiveRecord objects. Say you have an array of hashes:
photos = [{ name: '1.jpg', tag: 'flower'}, ... ]
You can create a helper method to perform the filtering:
class PhotosController < ApplicationController
def index
photos = [{ name: '1.jpg', tag: 'flower'}, ... ]
render json: select_photos_by_tag(photos, params[:tag])
end
private
def select_photos_by_tag(photos, tag)
if tag
photos.select { |photo| photo[:tag] == tag }
else
photos
end
end
end

How to refactor complex search logic in a Rails model

My search method is smelly and bloated, and I need some help refactoring it. I'm new to Ruby, and I haven't figured out how to leverage it effectively, which leads to bloated methods like this:
# discussion.rb
def self.search(params)
# If there is a search query, use Tire gem for fulltext search
if params[:query].present?
tire.search(load: true) do
query { string params[:query] }
end
# Otherwise grab all discussions based on category and/or filter
else
# Grab all discussions and include the author
discussions = self.includes(:author)
# Filter by category if there is one specified
discussions = discussions.where(category: params[:category]) if params[:category]
# If params[:filter] is provided, user it
if params[:filter]
case params[:filter]
when 'hot'
discussions = discussions.open.order_by_hot
when 'new'
discussions = discussions.open.order_by_new
when 'top'
discussions = discussions.open.order_by_top
else
# If params[:filter] does not match the above three states, it's probably a status
discussions = discussions.order_by_new.where(status: params[:filter])
end
else
# If no filter is passed, just grab discussions by hot
discussions = discussions.open.order_by_hot
end
end
end
STATUSES = {
question: %w[answered],
suggestion: %w[started completed declined],
problem: %w[solved]
}
scope :order_by_hot, order('...') DESC, created_at DESC")
scope :order_by_new, order('created_at DESC')
scope :order_by_top, order('votes_count DESC, created_at DESC')
This is a Discussion model that can be filtered (or not) by a category: question, problem, suggestion.
All discussions or a single category can be filtered further by hot, new, votes, or status. Status is a hash in the model and it has several values depending on the category (status filter only appears if params[:category] is present).
Complicating matters is a fulltext search feature using Tire
But my controller looks nice and tidy:
def index
#discussions = Discussion.search(params)
end
Can I dry this up/refactor it a little, maybe using meta programming or blocks? I managed to extract this out of the controller, but then ran out of ideas. I don't know Ruby well enough to take this further.
For starters, "Grab all discussions based on category and/or filter" can be a separate method.
params[:filter] is repeated many times, so take that out at the top:
filter = params[:filter]
You can use
if [:hot, :new, :top].incude? filter
discussions = discussions.open.send "order_by_#{filter}"
...
Also, factor out if then else if case else statements. I prefer break into separate methods and return early:
def do_something
return 'foo' if ...
return 'bar' if ...
'baz'
end
discussions = discussions... appears many times, but looks weird. Can you use return discussions... instead?
Why does the constant STATUSES appear at the end? Usually constants appear at the top of the model.
Be sure to write all your tests before refactoring.
To respond to the comment about return 'foo' if ...:
Consider:
def evaluate_something
if a==1
return 'foo'
elsif b==2
return 'bar'
else
return 'baz'
end
end
I suggest refactoring this to:
def evaluate_something
return 'foo' if a==1
return 'bar' if b==2
'baz'
end
Perhaps you can refactor some of your if..then..else..if statements.
Recommended book: Clean Code

Rails 3 multiple parameter filtering using scopes

Trying to do a basic filter in rails 3 using the url params. I'd like to have a white list of params that can be filtered by, and return all the items that match. I've set up some scopes (with many more to come):
# in the model:
scope :budget_min, lambda {|min| where("budget > ?", min)}
scope :budget_max, lambda {|max| where("budget < ?", max)}
...but what's the best way to use some, none, or all of these scopes based on the present params[]? I've gotten this far, but it doesn't extend to multiple options. Looking for a sort of "chain if present" type operation.
#jobs = Job.all
#jobs = Job.budget_min(params[:budget_min]) if params[:budget_min]
I think you are close. Something like this won't extend to multiple options?
query = Job.scoped
query = query.budget_min(params[:budget_min]) if params[:budget_min]
query = query.budget_max(params[:budget_max]) if params[:budget_max]
#jobs = query.all
Generally, I'd prefer hand-made solutions but, for this kind of problem, a code base could become a mess very quickly. So I would go for a gem like meta_search.
One way would be to put your conditionals into the scopes:
scope :budget_max, lambda { |max| where("budget < ?", max) unless max.nil? }
That would still become rather cumbersome since you'd end up with:
Job.budget_min(params[:budget_min]).budget_max(params[:budget_max]) ...
A slightly different approach would be using something like the following inside your model (based on code from here:
class << self
def search(q)
whitelisted_params = {
:budget_max => "budget > ?",
:budget_min => "budget < ?"
}
whitelisted_params.keys.inject(scoped) do |combined_scope, param|
if q[param].nil?
combined_scope
else
combined_scope.where(whitelisted_params[param], q[param])
end
end
end
end
You can then use that method as follows and it should use the whitelisted filters if they're present in params:
MyModel.search(params)

Simple model.. worth making an in memory class or just put it in the Database?

Sorry for the bad title, I've no idea what to call this question.
I have a simple class which basically just wraps an array and provides some custom access metods for it. Essentially all it stores is an array of doublets, each one holding a property type and it's uid.
My idea is that because this is such a simple usage, I can get away with just building this class and holding it in memory rather than generating a model and suffering the overhead of ActiveRecord (that may not be much, I'm not sure).
I'm worried I'm overthinking and I should just put it in the DB and forget about it?
class PropertyType
extend Enumerable
TYPES = [
['Site', 'Site'],
['New Home', 'NewHome'],
['Terraced House', 'Terraced'],
['Detached House', 'Detached'],
['Bungalow', 'Bungalow'],
['Townhouse', 'Townhouse'],
['End of Terrace House', 'EoTHouse'],
['Semi-Detached House', 'Semi-D'],
['New Development', 'NewDev'],
['Apartment', 'Apartment'],
['Duplex', 'Duplex'],
['House For Sale', 'House']
].freeze
def self.each
TYPES.each{|type| yield(type[0], type[1]) }
end
def self.convert_to_name(uid_to_lookup)
return if uid_to_lookup.blank?
TYPES.each{|type| return type[0] if type[1] == uid_to_lookup }
end
def self.convert_to_uid(name_to_lookup)
return if name_to_lookup.blank?
TYPES.each{|type| return type[1] if type[0] == name_to_lookup }
end
def self.each_uid
TYPES.each{|type| yield(type[1]) }
end
def self.each_name
TYPES.each{|type| yield(type[0]) }
end
def self.uids
TYPES.collect{|type| type[1]}
end
def self.names
TYPES.collect{|type| type[0]}
end
end
I think your class is fine, but personally I'd still put it in the database, just so I have an easy option for building an administrative interface for property types and letting someone else manage modifications to them over time.

Resources