ruby-grape / ruby-grape/grape-entity

Presenting a collection with an entity fetches each table twice (2 SQL statements)

Open
#71 1 comment 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug?
Dominant language
Ruby
Stars
729
Forks
154
PR merge metrics
No merged PRs in 30d

Description

I have a Location model that has_one :address. The data is stored in Postgres. Here's the Grape API endpoint definition for fetching all locations:

resource "locations" do
  # GET /locations
  desc 'Returns all locations, 30 per page by default'
  params do
    optional :page, type: Integer, default: 1
    optional :per_page, type: Integer, default: 30
  end
  get do
    locations = Location.includes(:address).
                        page(params[:page]).per(params[:per_page])
    present locations, with: Entities::Location
  end
end

My (simplified) Location Entities:

module Entities
  class Location < Grape::Entity
    expose :address, using: Address::Entity
    expose :name
  end
end

The Address::Entity is defined within the Address model:

include Grape::Entity::DSL
entity do
  expose :id
  expose :street
  expose :city
  expose :state
  expose :zip
end

When I visit http://localhost:8080/api/locations, I get the following output in the log:

Location Load (0.6ms)  SELECT "locations".* FROM "locations" ORDER BY "locations"."id" ASC LIMIT 1 OFFSET 0
Address Load (0.5ms)  SELECT "addresses".* FROM "addresses" WHERE "addresses"."location_id" IN (2)
Location Load (6.0ms)  SELECT "locations".* FROM "locations" LIMIT 30 OFFSET 0
Address Load (0.8ms)  SELECT "addresses".* FROM "addresses" WHERE "addresses"."location_id" IN (2, 17, 41, 138, 321, 54, 61, 325, 1146, 531, 139, 326, 965, 72, 238, 140, 228, 330, 1631, 271, 241, 331, 335, 6, 1460, 336, 75, 90, 101, 818)

Note that there are two calls to each table: one to fetch the first record in the results, and then another to fetch all the results (up to the per_page amount).

If I change the endpoint definition to just return locations without any grape-entity representation, and instead, use my own as_json method in the Locations model, like this:

def as_json(options={})
  {
    name: name,
    address: address
  }
end

then, I only end up with one call to each table, as opposed to the two that grape-entity made:

Location Load (6.7ms)  SELECT "locations".* FROM "locations" LIMIT 30 OFFSET 0
Address Load (4.4ms)  SELECT "addresses".* FROM "addresses" WHERE "addresses"."location_id" IN (2, 17, 41, 138, 321, 54, 61, 325, 1146, 531, 139, 326, 965, 72, 238, 140, 228, 330, 1631, 271, 241, 331, 335, 6, 1460, 336, 75, 90, 101, 818)

This is reproducible every time I switch back and forth between the two methods of representing the JSON. It also happens if the JSON doesn't include data from other tables, and even when you're just calling Location.all (not that you would do that). It always makes 2 calls to the Locations table.

In the actual app, I need to include 8 other tables, so having an extra call to each one adds up!

Has anyone noticed this before? Is there a way to write a spec for this?

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the GET /locations endpoint with Location.includes(:address), pagination, and Entities::Location, then compare its SQL with the as_json variant. Inspect the Location entity and Address entity definitions shown in the report. Done means identifying why the first record is loaded separately, preventing the duplicate queries, and adding a regression spec for the behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
postgresql, ruby
Domain
api, backend, databases
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.