Relationships and Nested Queries
Let’s say we want to fetch a Post and all of its Comments:
/posts?include=commentsUsing the default ActiveRecord Adapter, we would add this code to our
PostResource:
# app/resources/post_resource.rb
has_many :comments,
scope: -> { Comment.all },
resource: CommentResource,
foreign_key: :post_idNote: we’d have to whitelist
commentsin our serializer as well.
To understand this code, we first have to realize that this is a Macro - code that is generating lower-level code for the purposes of removing boilerplate. Let’s understand the lower-level DSL before breaking down the macro.
allow_sideload :comments, resource: CommentResource do
scope do |posts|
# ... code ...
end
assign do |posts, comments|
# ... code ...
end
endThis is the lower-level allow_sideload DSL. There are four things
going on. To begin with:
- We’ve whitelisted
comments. Without this, the request would raise the errorJsonapiCompliable::Errors::InvalidInclude. This ensures clients can’t arbitrarily pull back data that could introduce performance problems or security risks. - We’ve said, “when retrieving comments, re-use the logic defined in
CommentResource”. This way all the filter, sorting, etc query logic at the/commentsendpoint can be reused when sideloading comments from the/posts?include=commentsendpoint.
That brings us to the scope and assign hooks. When querying a
relationship, we need to answer two questions:
- Given a list of parents (
posts), how should we scope the request for children (comments)? This is thescopeblock. In a relational database, we’d usually scope based on foreign and primary keys. - Given a list of parents (
posts) and a list of children (comments), how do you want to assign these objects together? This is theassignblock. In a relational database, we’d usually compare foreign and primary keys.
In other words, the code would look similar to this for ActiveRecord:
scope do |posts|
Comment.where(post_id: posts.map(&:id))
end
assign do |posts, comments|
posts.each do |post|
post.comments = comments.select { |c| c.post_id == post.id }
end
endNote that scope hasn’t actually fired a query - we take the result of
this block and pass it to CommentResource so that further query logic
(filtering, sorting, etc) can be applied and re-used across endpoints.
Of course, the code above would be very tedious to write by hand every
time. That’s why we have Macros like has_many, belongs_to etc -
configure only the parts you need, and avoid the boilerplate:
# app/resources/post_resource.rb
has_many :comments,
scope: -> { Comment.all },
resource: CommentResource,
foreign_key: :post_id
# primary_key defaults to 'id'Given the above options, we can auto-generate allow_sideload code. You
can always write allow_sideload directly if you have highly customized
logic. You can also pass a block to the macros to customize:
# app/resources/post_resource.rb
has_many :comments,
scope: -> { Comment.all },
resource: CommentResource,
foreign_key: :post_id do
assign do |posts, comments|
# some custom code to associate these objects
Post.associate(posts, comments)
end
endAgain, nested queries come for free. This code allows for nested queries
like “give me the post, and its active comments”:
/posts/1?include=comments&filter[comments][active]=true