acts_as_api and bullet N+1 queries - mysql

I am using acts_as_api to provide a JSON response for some models in my system. My API related code is (reduced to make the example easier):
# File app/modes/item.rb
# API
acts_as_api
api_accessible :v1_list do |template|
template.add :id
template.add :client_name
end
This API is working as expected. It is important to know that client_name is a method containing:
def client_name
client.name
end
That is, the client name is not included in the item model but in the client model. Thus, this info is not contained in the items table.
Using Bullet gem I have noticed that a N+1 query is being performed in the clients table. For each item, a SQL query to the clients table is also performed.
I know that ActiveRecord has some utilities in the API that avoids N+1 queries, and I would like to know if there is a way to use that ActiveRecord feature together with acts_as_api gem.

The gem documentation shows this
def index
#users = User.all
#Note that it’s wise to add a root param when rendering lists.
respond_to do |format|
format.xml { render_for_api :name_only, :xml => #users, :root => :users }
format.json { render_for_api :name_only, :json => #users, :root => :users }
end
end
So for your case you should simply eager load the client association
def index
#items = Item.includes(:client).all
# Note that it’s wise to add a root param when rendering lists.
respond_to do |format|
format.xml { render_for_api :name_only, :xml => #items, :root => :items }
format.json { render_for_api :name_only, :json => #items, :root => :items }
end
end

Related

rendering json gets error no template found for controller rendering head no_content 204

I have a controller that render a json with data from models. When i enter route to get it it do nothing and just show and error in console.
Controller:
class Api::ForecastController < ApplicationController
before_action :set_hourly_forecast, only: %i[ show edit update destroy ]
def index
respond_to do |format|
format.html do
#hourly_forecasts = HourlyForecast.where(forecast_location_id: params[:forecast_location_id]).paginate(:page => params[:page], :per_page=>24) if params[:forecast_location_id].present?
end
format.json do
weather_service = WeatherService.where(name: params[:name])
#forecast_location = ForecastLocation.where(weather_service_id: weather_service)#& municipality: #municipality.name)
#hourly_forecasts = HourlyForecast.where(forecast_location_id: forecast_location.id ).paginate(:page => params[:page], :per_page=>24) if params[:forecast_location_id].present?
end
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_hourly_forecast
#hourly_forecast = HourlyForecast.find(params[:id])
end
# Only allow a list of trusted parameters through.
def hourly_forecast_params
params.require(:hourly_forecast).permit(:forecast_location_id, :date, :temperature, :humidity, :rain, :rain_probability, :wind_speed, :wind_direction)
end
end
Error:
> Started GET "/api/forecast.json?name=dark_sky" for 127.0.0.1 at 2022-04-20 18:33:29 +0200
Processing by Api::ForecastController#index as JSON
Parameters: {"name"=>"dark_sky"}
No template found for Api::ForecastController#index, rendering head :no_content
Completed 204 No Content in 53ms (ActiveRecord: 6.2ms | Allocations: 4983)
The route i use its
127.0.0.1:3000/api/forecast.json?name=dark_sky
So the output should be a json with all hourly model.
But it just do nothing and on console it does get and the select but jumps this error of template, i dont understand it since im new on rails.
If need more controllers, models or anything ask in comments.
You have to have a separate template file to render json index.json.jbuilder for example.
# app/views/api/forecasts/index.json.jbuilder
json.array! #hourly_forecasts do |hourly_forecast|
json.extract! hourly_forecast, :id, :forecast_location_id, :date, :temperature, :humidity, :rain, :rain_probability, :wind_speed, :wind_direction
json.url api_forecast_url(hourly_forecast, format: :json)
end
https://github.com/rails/jbuilder
If you don't need to customize rendered json too much, render json inline in the controller
format.json do
weather_service = WeatherService.where(name: params[:name])
#forecast_location = ForecastLocation.where(weather_service_id: weather_service)#& municipality: #municipality.name)
#hourly_forecasts = HourlyForecast.where(forecast_location_id: forecast_location.id ).paginate(:page => params[:page], :per_page=>24) if params[:forecast_location_id].present?
render json: #hourly_forecasts
end
https://guides.rubyonrails.org/layouts_and_rendering.html#rendering-json
https://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html

how to display total records in Postgresql DB based on attribute

I am trying to display a numerical value for total number of Active and Pending calls. Currently i have these broken out into two panels and would like to include a visible number to show how many of each type are in que.
I have searched this out but its all very convoluted and makes not much sense.
the column name is "status" and in the call create page the have the option to select "assigned" and "pending"
below is my controller for reference.
Calls Controller:
class CallsController < ApplicationController
before_action :set_call, only: [:show, :edit, :update, :destroy]
# GET /calls
# GET /calls.json
def index
#calls = Call.all
#active_calls = Call.where(status: 'active')
#pending_calls = Call.where(status: 'pending')
end
# GET /calls/1
# GET /calls/1.json
def show
end
# GET /calls/new
def new
#call = Call.new
end
# GET /calls/1/edit
def edit
end
# POST /calls
# POST /calls.json
def create
#call = Call.new(call_params)
respond_to do |format|
if #call.save
format.html { redirect_to #call, notice: 'Call was successfully created.' }
format.json { render :show, status: :created, location: #call }
else
format.html { render :new }
format.json { render json: #call.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /calls/1
# PATCH/PUT /calls/1.json
def update
respond_to do |format|
if #call.update(call_params)
format.html { redirect_to #call, notice: 'Call was successfully updated.' }
format.json { render :show, status: :ok, location: #call }
else
format.html { render :edit }
format.json { render json: #call.errors, status: :unprocessable_entity }
end
end
end
# DELETE /calls/1
# DELETE /calls/1.json
def destroy
#call.destroy
respond_to do |format|
format.html { redirect_to calls_url, notice: 'Call was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_call
#call = Call.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def call_params
params.require(:call).permit(:call_time, :status, :primary_type, :secondary_type, :site, :address, :unit_1, :unit_2, :unit_3, :unit_4, :call_details, :unit_on_scene, :unit_clear, :call_num, :site_id)
end
end
the Postgresql side of things is my weakness and i am currently working to strengthen this.
Thanks in advance for you assistance.
Depends on where (which controller action) you are looking to perform the counts.
If it's in index:
It seems like you already load the related records into memory with:
#active_calls = Call.where(status: 'active')
#pending_calls = Call.where(status: 'pending')
So you don't need to issue a new query, you can simply call Ruby's length method like this:
#active_calls.length
#pending_calls.length
By the way, I would probably avoid running three queries like you do at the moment and do something like this:
#calls = Call.all
#active_calls = #calls.select{|x| x.status == 'active'}
#pending_calls = #calls.select{|x| x.status == 'pending'}
And use the #active_calls.length and #pending_calls.length as before. The reason is that every trip to the database comes with an overhead. It might not seem much, but these latencies add up and make your controller action slower. So generally you want to reduce the number of trips to the database as much as you can. The code above uses Ruby's select method to go over the array and pick the elements for which the block is evaluated as true; this happens in memory and does not require a trip to the database.
If it's in a different controller action:
Assuming that in that other controller action you do not load an array of Call Active Record objects, you probably want to issue COUNT(*) queries. These are generally faster than loading all the records and performing the count in memory (e.g. with length), because the database server simply needs to return you a number instead of all the records data, and you also save the overhead of building Active Record objects.
The way to do this with Active Record is simply:
Call.where(status: 'active').count
Call.where(status: 'pending').count
If you want want to do one COUNT query with a GROUP BY instead of two separate queries (good idea, as it saves an extra trip to the database), you could do this:
Call.group(:status).count
This would result in a hash such as {'active' => 150, 'pending' => 120} which you can use in your view.

Rails 4 and Mongoid: Retrieving embedded document as json

I want to retrieve all the embedded documents of a document to return as a "list" of json elements.
I have documents as follows:
class Parent
include Mongoid::Document
field :name, :type => String
embeds_many :kids
class Kid
include Mongoid::Document
field :kidname, :type => String
embedded_in :parent, :inverse_of => :kids
I have defined two routes
get 'parents/:kidname' => 'parents#getparents'
where getparents is defined as
#retval = Parent.where("kids.kidname" => params[:kidname])
respond_to do |format|
format.html # index.html.erb
format.json { render json: #retval}
end
This gives me the correct output, i.e., the parent whose kid's name is params[:kidname]
However when I try to do the reverse, i.e., retrieve the list of all kids whose parent's name is params[:name], that doesn't work! The route is
get 'kids/:name' => 'parents#getkids'
and getkids is defined as
def getkids
#parent = Parent.where("name" => params[:name])
#kids = #parent.kids
respond_to do |format|
format.html # index.html.erb
format.json { render json: #kids}
end
end
What am I doing wrong? Does it matter where getkids is defined .. I defined it in parents_controller, should it be in kids_controller? Please help!
Thanks.
if you re not getting the results or getting some error change this line #parent = Parent.where("name" => params[:name]) to #parent = Parent.where("name" => params[:name]).first

Rspec - GET show with respond_to on specific json view

I'm trying to use Rspec-rails on my application but I fail when I try to verify the response of my request.
This is my code :
# app/controllers/user_controller.rb
# GET api/users/:id
def show
#user = User.find(:id)
respond_to do |format|
format.json {render template: "/api/users/detailed"}
end
end
...
# spec/controllers/api/users_controller_spec.rb
require "spec_helper"
describe Api::UsersController do
include Devise::TestHelpers
describe "GET #show" do
user = FactoryGirl.build(:user)
it "assigns the requested user to #user" do
get :show, :format => 'json', :id => user.id
assigns(:user).should eq(#user)
end
it "renders the #detailed view" do
get :show, :format => 'json', :id => user.id
response.should be_respond_to("api/users/detailed")
#expect(response).to render_template("api/users/detailed")
#expect(subject).to render_template("/api/users/detailed")
#render(:template => "/api/users/detailed.json.rabl", :format => "json")
#response.should render_template("/api/users/detailed.json.rabl")
end
end
I commented a part of what I have tested.
I just want to be sure that my controller respond_to a template called "api/users/detailed".
Most often, I have the following error :
expecting <"api/users/detailed"> but rendering with <"">
I'm pretty sure my request called the controller because the first describe "assigns the requested user to #user" works.

How can I convert MySQL latitude / longitude data to JSON using Ruby on Rails

Thanks in advance for any help you can provide on this question: How can I use Ruby on Rails to create a JSON object from the "latitude" and "longitude" data in my MySQL table, "Locations"?
My ultimate goal is to create a Google Map with multiple markers pulled from a MySQL database. I know that I can do this through PHP and the Google tutorial (https://developers.google.com/maps/articles/phpsqlsearch_v3), but I'm not strong in PHP and am looking for a shorter way to do it through Rails.
I have tried this other tutorial (http://mokisystemsblog.blogspot.com/2013/04/add-markers-to-google-map-with-ruby-on.html), and here is my code for my controller:
class MapallController < ApplicationController
# GET /mapall
# GET /mapall.xml
# GET /mapall.js
def index
respond_to do |format|
format.html do
#locations = Location.find(:all)
end
format.xml { render :xml => #locations }
format.js do
ne = params[:ne].split(',').collect{|e|e.to_f}
sw = params[:sw].split(',').collect{|e|e.to_f}
#locations = Location.find(:all, :limit => 100, :bounds => [sw, ne])
render :json => #locations.to_json
end
end
end
end
However, when I visit www.example.com/mapall.js, I get an error code. I expect that this link would give me the complete set of results from my database.
Again, I appreciate your advice and patience as I learn this process!
EDIT 1 - ERROR CODE
Below is the log for what happens when I visit example.com/mapall, example.com/mapall.js, and example.com/mapall.xml. When I visit example.com/mapall, I expect a Google Map that renders all of my locations from the MySQL database. Barring that, I expected to see the lat / long data when I visited mapall.js. From the log below, I'm guessing the reason that I'm getting a 404 is that there is no route to mapall.js in my routes file. Is the solution to create a route to the routes file, and if so, how should that read?
Thanks again for your help!
Processing MapallController#index (for IP at DATE TIME) [GET]
Rendering template within layouts/mapall
Rendering mapall/index
Completed in 779ms (View: 7, DB: 100) | 200 OK [http://example.com/mapall]
Processing ApplicationController#index (for IP at DATE TIME) [GET]
ActionController::RoutingError (No route matches "/mapall.js" with {:method=>:get}):
/phusion_passenger ERROR INFO
Rendering /home/example/web/current/public/404.html (404 Not Found)
Processing ApplicationController#index (for IP at DATE TIME) [GET]
ActionController::RoutingError (No route matches "/mapall.xml" with {:method=>:get}):
/phusion_passenger ERROR INFO
Rendering /home/example/web/current/public/404.html (404 Not Found)
EDIT 2 - New Controller
Thanks to #kyllo's feedback, I've been able to update the controller so that I can get ALL of my location data to appear at http://example.com/mapall.js. Only one step left: getting just the nickname, latitude, and longitude fields to appear. The below shows ALL of the data. How should I change this to show only the nickname, latitude, and longitudinal fields?
class MapallController < ApplicationController
# GET /mapall
# GET /mapall.xml
# GET /mapall.js
def index
respond_to do |format|
#masterlocation = Masterlocation.find(:all)
format.html do
end
format.xml { render :xml => #masterlocation }
format.js do
#masterlocation = Masterlocation.find(:all)
render :json => #masterlocation.to_json
end
end
end
end
I see a few problems here.
Yes, you are getting a "no route matches" error so you need a route in routes.rb similar to this, so that you can access :format as a parameter to respond to:
match 'mapall.:format'
Then in your controller, you have the following:
def index
respond_to do |format|
format.html do
#locations = Location.find(:all)
end
format.xml { render :xml => #locations }
format.js do
ne = params[:ne].split(',').collect{|e|e.to_f}
sw = params[:sw].split(',').collect{|e|e.to_f}
#locations = Location.find(:all, :limit => 100, :bounds => [sw, ne])
render :json => #locations.to_json
end
end
end
Your #locations = Location.find(:all) needs to be outside of the format.html do block, otherwise #locations is undefined inside of your format.xml block and you will get an error.
format.js do will allow your user to access the JSON API by visiting http:/www.example.com/mapall.js. If you use format.json do then they can access it at http:/www.example.com/mapall.json. The choice is yours.
Hope that helps to put you on the right track!