🦔

Railsでmodelから特定のテンプレートをレンダリングしてJSONを出力する

2022/02/06に公開

前提

  • RailsでAPIを作っています
  • データをJSONで返却するcontrollerのactionがあります
    • この記事ではjbuilderを使っています

やりたいこと

  • controller以外で、特定のcontrollerのactionと同じテンプレートを使い回してJSONを出力したい
    • 今回はmodelでレンダリングします
      • JSONを出力してファイルをアップロードする機能の開発でした

サンプルアプリケーションの説明

以下のようにAPIモードでrails newして、scaffoldを使って簡易的に作りました。
Ruby3.1, Rails7.0.1を使用しています。

rails new sample_app --api

# (省略)jbuilderとrspecをbundle installしています

bin/rails generate scaffold Products name:string price:integer

invoke  active_record
create    db/migrate/20220205143831_create_products.rb
create    app/models/product.rb
invoke    rspec
create      spec/models/product_spec.rb
invoke  resource_route
  route    resources :products
invoke  scaffold_controller
create    app/controllers/products_controller.rb
invoke    resource_route
invoke    rspec
create      spec/requests/products_spec.rb
create      spec/routing/products_routing_spec.rb
invoke    jbuilder
create      app/views/products
create      app/views/products/index.json.jbuilder
create      app/views/products/show.json.jbuilder
create      app/views/products/_product.json.jbuilder

説明に必要なコードを貼ります。

class ProductsController < ApplicationController
  def index
    @products = Product.all
  end
end
# index.json.jbuilder
json.array! @products, partial: "products/product", as: :product
# _product.json.jbuilder
json.extract! product, :id, :name, :price, :created_at, :updated_at
json.display_info "商品名: #{product.name}, 価格: #{product.price}"

方法

ActionController::Rendererを使うことで、任意のテンプレートをcontrollerのactionを介さずにレンダリングできます。

サンプルアプリケーションのProductsController#indexと同じJSONをmodelでレンダリングします。

class JsonRenderer
  def render_products
    # controllerのactionを指定して、レンダリングで使われるデータを渡します
    ProductsController.render :index, assigns: { products: Product.all }
  end
end

上の書き方以外には、以下のような方法もあります。

# actionではなくtemplateを指定する方法もあります
ApplicationController.render template: '/products/index', assigns: { products: Product.all }

# #forを使うことでcontrollerを引数に渡してレンダリングすることもできます
ActionController::Renderer.for(ProductsController).render :index, assigns: { products: Product.all }

rails consoleで確認したときのログです。

ProductsController.render :index, assigns: { products: Product.all }
  Rendering products/index.json.jbuilder
   (0.1ms)  SELECT sqlite_version(*)
  Product Load (0.4ms)  SELECT "products".* FROM "products"
  Rendered collection of products/_product.json.jbuilder [3 times] (Duration: 0.8ms | Allocations: 448)
  Rendered products/index.json.jbuilder (Duration: 7.4ms | Allocations: 2960)
=> "[{\"id\":1,\"name\":\"イヤホン\",\"price\":5000,\"created_at\":\"2022-02-05T14:48:09.618Z\",\"updated_at\":\"2022-02-05T14:48:09.618Z\",\"display_info\":\"商品名: イヤホン, 価格: 5000\"},{\"id\":2,\"name\":\"スピーカー\",\"price\":20000,\"created_at\":\"2022-02-05T14:48:18.642Z\",\"updated_at\":\"2022-02-05T14:48:18.642Z\",\"display_info\":\"商品名: スピーカー, 価格: 20000\"},{\"id\":3,\"name\":\"カメラ\",\"price\":50000,\"created_at\":\"2022-02-05T14:48:35.626Z\",\"updated_at\":\"2022-02-05T14:48:35.626Z\",\"display_info\":\"商品名: カメラ, 価格: 50000\"}]"

Discussion