DEV Community

Cover image for How to implement Laravel's whenLoaded function in Ruby on Rails
whchi
whchi

Posted on • Edited on

How to implement Laravel's whenLoaded function in Ruby on Rails

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,
    ];
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • CommentEntity.rb
module Entities
  class CommentEntity < Grape::Entity
    expose :id, documentation: { type: 'Integer', desc: 'id' }
    expose :title, documentation: { type: 'String', desc: 'title' }
  end
end
Enter fullscreen mode Exit fullscreen mode

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

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

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
Enter fullscreen mode Exit fullscreen mode

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}]} 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

2. 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
Enter fullscreen mode Exit fullscreen mode

3. 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}]} 
Enter fullscreen mode Exit fullscreen mode

Top comments (0)