Description
Hello! Thank you for creating such an amazing gem!
I’m currently facing an issue that I believe has been discussed in the past, but I couldn’t find any specific topics related to it. I’d like to ask for guidance on how others are addressing this challenge in the following use case.
Context
Model
erDiagram
User ||--|{ Post : ""
Post }|--|| Book : ""
Serializer
For models like the above, the serializers would typically look like this:
class UserSerializer < ActiveModelSerializers::Model
attributes :id, :name, :post_count, :thumbnail
has_many :posts, serializer: PostSerializer
end
class PostSerializer < ActiveModelSerializers::Model
attributes :id, :comment, :rake
belongs_to :user, serializer: UserSerializer
belongs_to :book, serializer: BookSerializer
end
class BookSerializer < ActiveModelSerializers::Model
attributes :id, :name, :avg_rate, :review_count, :book_image
has_many :posts, serializer: PostSerializer
end
The Issue
The main challenge I’m facing is around the design and usage of serializers. Many articles I’ve seen tend to solve problems using a single, monolithic serializer. This approach often lacks clear separation of concerns and results in overly complex interdependencies. I’d like to know how others tackle the following issues:
Unnecessary Relationships in Different Use Cases
For example, in the /api/post/{:post_id}
endpoint, I’d like the response to look like this:
{
"id": 12345,
"comment": "dummy text",
"rate": 4.5,
"user": {
"id": 12345,
"name": "jackson",
"post_count": 15,
"thumbnail": "http://xxxxxxxx.xxxxx/yyy"
},
"book": {
"id": 435,
"name": "The Little Prince",
"avg_rate": 3.9,
"review_count": 231,
"book_image": "http://xxxxxxxx.xxxxx/xxx"
}
}
However, for /api/book/{:book_id}
, the response should look like this:
{
"id": 435,
"name": "The Little Prince",
"avg_rate": 3.9,
"review_count": 231,
"book_image": "http://xxxxxxxx.xxxxx/xxx",
"last_review": {
"id": 12345,
"comment": "dummy text",
"rate": 4.5,
"user": {
"id": 12345,
"name": "jackson",
"post_count": 15,
"thumbnail": "http://xxxxxxxx.xxxxx/yyy"
}
}
}
The problem becomes more apparent with endpoints like /api/book/{:book_id}/reviews
or /api/book/{:book_id}/review/{:post_id}
. In these cases, since the book is already identified by the endpoint, including the book details in every review unnecessarily duplicates data and adds noise to the response.
Sudden N+1 Query Issues
AMS often introduces N+1 query problems as new associations are added to models. For example, the /api/book/{:book_id}/reviews
endpoint might be optimized like this:
class Api::Book::ReviewController < ActionController::Base
def index
book = Book.find_by(id: params[:book_id]).includes()
raise NotFound if book.nil?
@reviews = Review.where(book_id: book.id)
.includes(:user, :book)
render json: @reviews, each_serializer: PostSerializer
end
end
However, as the product grows, adding a new Author model and updating the serializer like this:
class PostSerializer < ActiveModelSerializers::Model
attributes :id, :comment, :rate
belongs_to :user, serializer: UserSerializer
belongs_to :book, serializer: BookSerializer
belongs_to :author, serializer: AuthorSerializer # new association
end
...suddenly introduces N+1 queries for Author in /api/book/{:book_id}/reviews
. Since some legacy endpoints don’t use includes consistently, fixing this across the codebase can be challenging.
My Proposed Solution
One strength of AMS is its model-based approach. To maintain this advantage, I propose the following design:
class Base::PostSerializer < ActiveModelSerializers::Model
attributes :id, :comment, :rate
end
class PostSerializer < Base::PostSerializer
belongs_to :user, serializer: UserSerializer
belongs_to :book, serializer: BookSerializer
end
class PostWithAuthorSerializer < Base::PostSerializer
belongs_to :user, serializer: UserSerializer
belongs_to :book, serializer: BookSerializer
belongs_to :author, serializer: AuthorSerializer # specific to this use case
end
By defining a Base::PostSerializer
for attributes and using separate serializers for specific use cases, we can minimize the impact on existing features. One key rule: never inherit from anything other than the base class to avoid complex, hard-to-manage hierarchies.
Final Question
How are you all addressing these kinds of design challenges with AMS? I’d love to hear your approaches and experiences. Thank you in advance!