1

I have a Rails 4 API. When a user search in the view for boats, this method is executed getting all the boats matching the search filters and return an array of boat models as json using render ActiveModel and the :include and :only like this:

render :json => @boats, :include => { :mainPhoto => {:only => [:name, :mime]},
                                      :year => {:only => :name},
                                      # other includes...}

This is working great.

But, additional to this information, in the view, I would like to show the total count of boats like "showing 1 - 20 of 80 boats" because there is a pagination funcionality. So, the point is I need to provide the 80 boats. I would like to avoid send two requests that execute almost the same logic, so the idea is to run the searchBoats method just once and in the result provide the list of boats and the total number of boats in a variable numTotalBoats. I understand numTotalBoats is not a boat model attribute. So, I think it should go in an independent variable in the render result. Something like:

render :json => {boats: @boats with all the includes, numTotalBoats: @NumTotalBoats}

I tried thousands of combinations, but or I´m getting syntax errors or none of them is returning the expected result, something like

{boats: [boat1 with all the includes, boat2 with all the includes, ... boatN with all the includes], numTotalBoats: N}
10
  • 1
    have you try like this? render :json => {boats: @boats, numTotalBoats: @boats.size} Commented Sep 26, 2018 at 1:05
  • Did you mean you are using active_model_serializers gem? The AMS can be used as PORO, which mean you can get the serializable object beforehand, then passing it to render method. You can find the idea from AMS document How to serialize a Plain-Old Ruby Object (PORO) and Using AMS Outside Of A Controller Commented Sep 26, 2018 at 6:11
  • I don´t think so. I didn´t install the gem. Just default active model provided by ror. Commented Sep 26, 2018 at 7:56
  • What is numTotalBoats:? Is it a model or just a method? Commented Sep 28, 2018 at 3:53
  • It´s not a model, it´s just a variable I need to pass with the count of all boats. Commented Sep 28, 2018 at 8:31

1 Answer 1

4
+50

Without adding any gems:

def index
  boats = Boat.includes(:year)

  render json: {
    boats: boats.as_json(include: { year: { only: :name } }),
    numTotalBoats: boats.count
  }
end

At some point though, I believe you should use stand-alone serializers:

Note: Depending on whether you're using pagination gem or not, you might need to change .count calls below to .total_count (for Kaminari) or something else that will read the count correctly from paginated collection.

I recommend using ActiveModel Serializers and this is how it would be accomplished for your case.

Start by adding the gem to Gemfile:

gem 'active_model_serializers', '~-> 0.10'

Overwrite the adapter in config/initializers/active_model_serializer.rb:

ActiveModelSerializers.config.adapter = :json

Define serializers for your models,

# app/serializers/boat_serializer.rb
class BoatSerializer < ActiveModel::Serializer
  attributes :name

  has_one :year
end

# app/serializers/year_serializer.rb
class YearSerializer < ActiveModel::Serializer
  attributes :name
end

And finally, in your controller:

boats = Boat.includes(:year)

render json: boats, meta: boats.count, meta_key: "numTotalBoats"

And you will achieve:

{
  "boats": [
    {
      "name": "Boaty McBoatFace",
      "year": {
        "name": "2018"
      }
    },
    {
      "name": "Titanic",
      "year": {
        "name": "1911"
      }
    }
  ],
  "numTotalBoats": 2
}

Adding that count in each index controller is a bit tedious, so I usually end up defining my own adapters or collection serializers in order to take care of that automatically (Tested with Rails 5, not 4).

# lib/active_model_serializers/adapter/json_extended.rb
module ActiveModelSerializers
  module Adapter
    class JsonExtended < Json
      def meta
        if serializer.object.is_a?(ActiveRecord::Relation)
          { total_count: serializer.object.count }
        end.to_h.merge(instance_options.fetch(:meta, {})).presence
      end
    end
  end
end

# config/initializers/active_model_serializer.rb
ActiveModelSerializers.config.adapter = ActiveModelSerializers::Adapter::JsonExtended

# make sure to add lib to eager load paths
# config/application.rb
config.eager_load_paths << Rails.root.join("lib")

And now your index action can look like this

def index
  boats = Boat.includes(:year)

  render json: boats
end

And output:

{
  "boats": [
    {
      "name": "Boaty McBoatFace",
      "year": {
        "name": "2018"
      }
    },
    {
      "name": "Titanic",
      "year": {
        "name": "1911"
      }
    }
  ],
  "meta": {
    "total_count": 2
  }
}

I think it's a little easier to parse this count for different endpoints and you will get it automatically while responding with a collection, so your controllers will be a little simpler.

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks! Looks good. Regarding your last sentence. Do you mean keep the logic of search boats and the count separated in two different endpoints, even when the query the need to run is the same, and with one answer with the collection of boats and the other with the count?
Use one endpoint. What I meant was to keep the count key independent of model name, so if you have more endpoints you can simply refer to count rather than numTotalBoats, numTotalYears, etc. About the extended adapter, if you had 3 index actions, you'd need to provide these meta keys in all of them explicitly, which is a lot of repetition and should be DRY'ed up.
@Rober I've added an answer that does not need any gems at the beginning.
Thanks. Let me check it. I tried it, but I think with more than two includes it made strange things. But, let me check it and get back to you!
Your answer is right! That was the answer I was looking for. It´s the approach I tried but for some reason my syntax was wrong and didn´t parse properly. Now it´s working.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.