DEV Community

Cover image for Optimizing Ruby on Rails Serializers: Efficient Hash Unpacking in Serializer
Myungwoo Song
Myungwoo Song

Posted on

Optimizing Ruby on Rails Serializers: Efficient Hash Unpacking in Serializer

Introduction

In the world of Ruby on Rails development, serializers play a crucial role in transforming ActiveRecord objects into JSON formats suitable for client-side consumption. However, while invaluable, serializers can sometimes introduce performance bottlenecks. In this article, I share my experience in overcoming a specific challenge: efficiently unpacking results from a resource-intensive method.

The performance issues

Imagine a scenario common in high-traffic web applications, like an e-commerce platform, where efficiency in data handling is not just a luxury but a necessity. You have a method in your Rails model that needs to be invoked, and its results - a collection of various fields - must be flattened.

Here's the method in question:

# In a Rails Model
  # @return [Hash]
  #  [Number] total_paid_price
  #  [Number] total_unpaid_price
def total_book_prices
  query_result = book_suppliers.joins(book: :invoice)
                               .group("CASE WHEN invoices.stage < #{Invoice.stages[:approved]} THEN 'unpaid' ELSE 'paid' END")
                               .sum("invoices.total_price")
  {
    total_paid_price: query_result["paid"] || 0,
    total_unpaid_price: query_result["unpaid"] || 0,
  }
end

Enter fullscreen mode Exit fullscreen mode

A naive approach might be to simply call this method twice within your serializer:

class SupplierListSerializer < ActiveModel::Serializer
  attributes :id, :title, :price, :author, ... , :total_paid_price, :total_unpaid_price

  def total_paid_price
    total_book_prices[:total_paid_price]
  end

  def total_unpaid_price
    total_book_prices[:total_unpaid_price]
  end
end
Enter fullscreen mode Exit fullscreen mode

However, this leads to redundant database queries, a cardinal sin in performance-sensitive applications. Particularly with complex queries or large datasets, this inefficiency can significantly impact performance.

Solution I found

To circumvent this problem, I implemented a caching strategy within the serializer itself. By storing the result of the first method call in an instance variable, subsequent calls can use this cached result instead of hitting the database again. Here's how it looks:

class SupplierListSerializer < ActiveModel::Serializer
  attributes :id, :name, :number_of_books, ... , :total_paid_price, :total_unpaid_price

  ...

  def total_paid_price
    total_book_prices[:total_paid_price]
  end

  def total_unpaid_price
    total_book_prices[:total_unpaid_price]
  end

  private

  # Caching the result in an instance variable
  def total_book_prices
    @total_book_prices ||= object.total_book_prices
  end
end
Enter fullscreen mode Exit fullscreen mode

This simple yet effective change ensures that our method total_book_prices` is called only once per serializer instance, significantly reducing database load and improving overall performance.

Conclusion

By implementing this caching technique, we've successfully navigated around one of the many performance pitfalls in Rails serializers. However, it's just the tip of the iceberg. There are other challenges, such as the infamous N+1 query issue, which can severely impact performance if not handled properly. In my next article, I'll delve into strategies to overcome this widespread issue.

Thank you for reading, and I encourage you to share your experiences or alternative solutions in the comments below!

Top comments (0)