How to Implement Laravels Whenloaded Function in Ror

The ORM library in Laravel provides support for a conditional loading association method known as whenLoaded.

From the Laravel official document:

The whenLoaded method may be used to conditionally load a relationship. In order to avoid unnecessarily loading relationships, this method accepts the name of the relationship instead of the relationship itself.

Here’s an example from Laravel’s official document, use it with “ResourceResponse” can significantly enhance productivity.

use App\Http\Resources\PostResource;

/**
 * Transform the resource into an array.
 *
 * @return array<string, mixed>
 */
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        'posts' => PostResource::collection($this->whenLoaded('posts')),
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

How to implement it in RoR?

To achieve what “ResourceResponse” does, we need the “grape” gem.

Given two entities, PostEntity and CommentEntity:

  • PostEntity.rb
module Entities
  class PostEntity < Grape::Entity
    expose :id, documentation: { type: 'Integer', desc: 'id' }
    expose :title, documentation: { type: 'String', desc: 'title' }
    expose :comments,
           using: CommentEntities,
           documentation: { is_array: true, desc: 'post comments' }
  end
end
  • CommentEntity.rb
module Entities
  class CommentEntity < Grape::Entity
    expose :id, documentation: { type: 'Integer', desc: 'id' }
    expose :title, documentation: { type: 'String', desc: 'title' }
  end
end

When we present PostEntity, we will receive a response with comments.

present Post.all with PostEntity # {id, title, comments: [{id, title}, {id, title}]}

However, if we don’t want to retrieve comments with every single request, we can modify it as follows:

module Entities
# ...
    expose :comments,
           using: CommentEntities,
           documentation: { is_array: true, desc: 'post comments' },
           if: -> (instance, _options) { instance.association(:comments).loaded? }
end

This way, comments will only be displayed if explicitly included:

#
present Post.all with PostEntity # {id, title}
present Post.includes(:comments).all with PostEntity # {id, title, comments: [{id, title}, {id, title}]}

bi-direction association

What if we want to load PostEntity from CommentEntity?

  1. add an association to CommentEntity
module Entities
# ...
    # this will cause circular error
    expose :comments,
           using: CommentEntities,
           documentation: { is_array: true, desc: 'post comments' },
           if: -> (instance, _options) { instance.association(:post).loaded? }
end
  1. fix circular error
module Entities
# ...
    expose :comments,
           using: CommentEntities,
           documentation: { is_array: true, desc: 'post comments' },
           if: -> (instance, options) {
             options[:parent] == instance.class.name &&
             instance.association(:post).loaded?
           }
end
  1. present
present Comment.includes(:post).all with CommentEntity, parent: 'Comment'
# {id, title, post: {id, title}}

present Post.includes(:comments).all with PostEntity, parent: 'Post'
 # {id, title, comments: [{id, title}, {id, title}]}
cmd + /