| 
1 | 1 | # Grape::Entity::Preloader  | 
2 | 2 | 
 
  | 
3 |  | -TODO: Delete this and the text below, and describe your gem  | 
4 |  | - | 
5 |  | -Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/grape/entity/preloader`. To experiment with that code, run `bin/console` for an interactive prompt.  | 
 | 3 | +Grape::Entity::Preloader allows preload associations and callbacks for avoiding N+1 operations in Grape::Entity.  | 
6 | 4 | 
 
  | 
7 | 5 | ## Installation  | 
8 | 6 | 
 
  | 
9 |  | -TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.  | 
10 |  | - | 
11 |  | -Install the gem and add to the application's Gemfile by executing:  | 
12 |  | - | 
13 | 7 | ```bash  | 
14 |  | -bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG  | 
 | 8 | +bundle add grape-entity-preloader  | 
15 | 9 | ```  | 
16 | 10 | 
 
  | 
17 | 11 | If bundler is not being used to manage dependencies, install the gem by executing:  | 
18 | 12 | 
 
  | 
19 | 13 | ```bash  | 
20 |  | -gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG  | 
 | 14 | +gem install grape-entity-preloader  | 
21 | 15 | ```  | 
22 | 16 | 
 
  | 
23 | 17 | ## Usage  | 
24 | 18 | 
 
  | 
25 |  | -TODO: Write usage instructions here  | 
 | 19 | +### Activation  | 
 | 20 | + | 
 | 21 | +#### Global Activation  | 
 | 22 | + | 
 | 23 | +You can enable the preloader globally. This is useful in environments where you want preloading to be the default behavior.  | 
 | 24 | + | 
 | 25 | +```ruby  | 
 | 26 | +# config/initializers/grape_entity_preloader.rb  | 
 | 27 | +Grape::Entity::Preloader.enabled!  | 
 | 28 | +```  | 
 | 29 | + | 
 | 30 | +#### Local Activation and Deactivation  | 
 | 31 | + | 
 | 32 | +You can control preloading for specific `represent` calls.  | 
 | 33 | + | 
 | 34 | +##### 1. Using options  | 
 | 35 | + | 
 | 36 | +Pass `grape_entity_preloader: :enabled` or `grape_entity_preloader: :disabled` to the options hash. This overrides the global setting.  | 
 | 37 | + | 
 | 38 | +```ruby  | 
 | 39 | +# Locally enable  | 
 | 40 | +MyEntity.represent(users, grape_entity_preloader: :enabled)  | 
 | 41 | + | 
 | 42 | +# Locally disable  | 
 | 43 | +MyEntity.represent(users, grape_entity_preloader: :disabled)  | 
 | 44 | +```  | 
 | 45 | + | 
 | 46 | +##### 2. Using a block  | 
 | 47 | + | 
 | 48 | +For a specific block of code, you can use `with_enable` or `with_disable`. This is useful in contexts like API endpoints or middlewares.  | 
 | 49 | + | 
 | 50 | +```ruby  | 
 | 51 | +Grape::Entity::Preloader.with_enable do  | 
 | 52 | +  # Preloading is enabled for all represent calls inside this block  | 
 | 53 | +  MyAPI::Entities::User.represent(User.all)  | 
 | 54 | +end  | 
 | 55 | + | 
 | 56 | +Grape::Entity::Preloader.with_disable do  | 
 | 57 | +  # Preloading is disabled for all represent calls inside this block  | 
 | 58 | +  MyAPI::Entities::User.represent(User.all)  | 
 | 59 | +end  | 
 | 60 | +```  | 
 | 61 | + | 
 | 62 | +### `preload_association`  | 
 | 63 | + | 
 | 64 | +Use `preload_association` to preload ActiveRecord associations. This helps to avoid N+1 queries when an exposure represents an association.  | 
 | 65 | + | 
 | 66 | +```ruby  | 
 | 67 | +class UserEntity < Grape::Entity  | 
 | 68 | +  expose :id  | 
 | 69 | +  expose :name  | 
 | 70 | +  # This will preload the `books` association for all users being represented.  | 
 | 71 | +  expose :books, using: BookEntity, preload_association: :books  | 
 | 72 | +end  | 
 | 73 | + | 
 | 74 | +# In your API  | 
 | 75 | +users = User.limit(10)  | 
 | 76 | +# When UserEntity represents users, it will execute two queries:  | 
 | 77 | +# 1. SELECT * FROM users LIMIT 10  | 
 | 78 | +# 2. SELECT * FROM books WHERE books.user_id IN (...)  | 
 | 79 | +UserEntity.represent(users)  | 
 | 80 | +```  | 
 | 81 | + | 
 | 82 | +For nested preloading:  | 
 | 83 | + | 
 | 84 | +```ruby  | 
 | 85 | +class BookEntity < Grape::Entity  | 
 | 86 | +  expose :id  | 
 | 87 | +  expose :title  | 
 | 88 | +  # This will preload tags for each book  | 
 | 89 | +  expose :tags, using: TagEntity, preload_association: :tags  | 
 | 90 | +end  | 
 | 91 | + | 
 | 92 | +class UserEntity < Grape::Entity  | 
 | 93 | +  expose :id  | 
 | 94 | +  expose :name  | 
 | 95 | +  expose :books, using: BookEntity, preload_association: :books  | 
 | 96 | +end  | 
 | 97 | + | 
 | 98 | +# It will generate 3 queries instead of 1 + 10 (for books) + N (for tags)  | 
 | 99 | +UserEntity.represent(User.limit(10))  | 
 | 100 | +```  | 
 | 101 | + | 
 | 102 | +### `preload_callback`  | 
 | 103 | + | 
 | 104 | +For more complex scenarios that `preload_association` doesn't cover (e.g., loading data from other services, custom caching logic), you can use `preload_callback`.  | 
 | 105 | + | 
 | 106 | +It must be a `Proc` that accepts two arguments:  | 
 | 107 | +1. `objects`: An array of the parent objects being represented.  | 
 | 108 | +2. `options`: The `Grape::Entity::Options` object for the current representation context.  | 
 | 109 | + | 
 | 110 | +**The `Proc` should return an array of objects that will be used for the nested entity representation. These returned objects will then be passed to the preloader for that nested entity, allowing for further nested preloading.**  | 
 | 111 | + | 
 | 112 | +```ruby  | 
 | 113 | +class UserStatsEntity < Grape::Entity  | 
 | 114 | +  expose :likes  | 
 | 115 | +  expose :followers  | 
 | 116 | +end  | 
 | 117 | + | 
 | 118 | +class UserEntity < Grape::Entity  | 
 | 119 | +  expose :id  | 
 | 120 | +  expose :name  | 
 | 121 | + | 
 | 122 | +  expose :stats, using: UserStatsEntity, preload_callback: ->(users, _options) do  | 
 | 123 | +    # `users` is an array of User objects.  | 
 | 124 | +    # Here you can fetch stats for all users in one batch.  | 
 | 125 | +    user_ids = users.map(&:id)  | 
 | 126 | +    stats_data = StatsService.batch_get_by_user_ids(user_ids) # returns a hash { user_id => stats_object }  | 
 | 127 | + | 
 | 128 | +    # The preloader needs to associate the loaded data back to the original objects.  | 
 | 129 | +    # A common pattern is to attach the data to a new attribute on the object.  | 
 | 130 | +    users.each { |user| user.instance_variable_set(:@stats, stats_data[user.id]) }  | 
 | 131 | + | 
 | 132 | +    # The block must return the objects that will be presented by the nested entity.  | 
 | 133 | +    # In this case, it's the stats objects we just loaded.  | 
 | 134 | +    users.map { |user| user.instance_variable_get(:@stats) }  | 
 | 135 | +  end  | 
 | 136 | +end  | 
 | 137 | + | 
 | 138 | +# In the entity, you need to define how to access the preloaded data.  | 
 | 139 | +class UserEntity < Grape::Entity  | 
 | 140 | +  # ...  | 
 | 141 | +  expose :stats, using: UserStatsEntity, preload_callback: ... do |user, _options|  | 
 | 142 | +    user.instance_variable_get(:@stats)  | 
 | 143 | +  end  | 
 | 144 | +end  | 
 | 145 | +```  | 
 | 146 | + | 
 | 147 | +### `preload_condition`  | 
 | 148 | + | 
 | 149 | +Use `preload_condition` to conditionally enable or disable preloading for an exposure. It must be a `Proc` that accepts one argument: `options`, which is the `Grape::Entity::Options` object.  | 
 | 150 | + | 
 | 151 | +If the `Proc` returns a falsy value, preloading for that exposure will be skipped.  | 
 | 152 | + | 
 | 153 | +```ruby  | 
 | 154 | +class UserEntity < Grape::Entity  | 
 | 155 | +  expose :id  | 
 | 156 | +  expose :name  | 
 | 157 | + | 
 | 158 | +  # The :audit_log association will only be preloaded if `include_audit_log` is true in the options.  | 
 | 159 | +  expose :audit_log,  | 
 | 160 | +         using: AuditLogEntity,  | 
 | 161 | +         preload_association: :audit_log,  | 
 | 162 | +         preload_condition: ->(options) { options[:include_audit_log] }  | 
 | 163 | +end  | 
 | 164 | + | 
 | 165 | +# Preloading for :audit_log is skipped  | 
 | 166 | +UserEntity.represent(user)  | 
 | 167 | + | 
 | 168 | +# Preloading for :audit_log is executed  | 
 | 169 | +UserEntity.represent(user, include_audit_log: true)  | 
 | 170 | +```  | 
26 | 171 | 
 
  | 
27 | 172 | ## Development  | 
28 | 173 | 
 
  | 
 | 
0 commit comments