RubyでRESTfulなAPIサーバを作るのに役立つGrapeですが、ぜひとも一緒に使いたいのがGrape::Entityです
Grape::EntityはView的な役割を果たし、DBの値をpresent
するだけで、Entityで指定したデータ構造を作ってくれ、最終的な出力整形をGrapeが行ってくれます
これで、DBからの値をmap等を駆使してHash構造を作る...そんな人生とは決別です
日本語情報が少ないのですが、コードサンプルやspecが充実しているので、実装で困ることはありませんが、一対多なデータ構造を表現する方法が乏しかったので、メモ代わりに残しておきます
- Ruby 2.1.5
- Grape 0.11.0
- Grape::Entity 0.4.4
データ構造
ParentとChildrenテーブルを用意、Parent/Childというモデル名で定義しています
Parent:Child = 1:N です
※ORMにSequelを使っていますが、ActiveRecordでも同じです
Sequel.migration do
up do
create_table :children do
primary_key :id
Integer :parent_id
String :name
String :birth_day
end
create_table :parents do
primary_key :id
String :name
String :addr
end
end
down do
drop_table :children
drop_table :parents
end
end
class Child < Sequel::Model
many_to_one :parent
end
class Parent < Sequel::Model
one_to_many :child
end
API実装
Grape自体のインストール等はconfig.ruについては割愛
module API::Entities
class Child < Grape::Entity
expose :name, as: "name"
# birth_day column in DB does not expose (hide)
end
class Parent < Grape::Entity
expose :name, as: "parent_name"
expose :addr
expose :child, as: "children", using: API::Entities::Child
end
end
class MyAPI < Grape::API
prefix "/api"
format :json
desc "Testing collection expose w/ grape-entity"
get "/collection/:name" do
pt = Parent.find(name: params[:name])
present pt, with: API::Entities::Parent
end
end
これで、下記の通りchildren
というHash keyの下に配列でchildが並びます
$ curl http://localhost:9292/api/collection/pname1
{"parent_name":"pname1","addr":"Streat Name","children":[{"name":"c1"},{"name":"c2"}]}
RSpec
curlでテストするのは忍びないので、RSpecも置いておきます
spec/spec_helper.rb
は割愛しますが、このあたりのgemを使っています
require "rack/test"
require "rspec/its"
require "rspec/json_matcher"
RSpec.configuration.include RSpec::JsonMatcher
require "json"
require "database_cleaner"
require "spec_helper"
describe MyAPI do
include Rack::Test::Methods
def app ; described_class ; end
before {
p1 = Parent.create(name: "pname1", addr: "Streat Name")
p1.add_child(name: "c1")
p1.add_child(name: "c2")
Parent.create(name: "p2")
}
context "get" do
subject { get "/api/collection/pname1" }
let(:result) do
{
parent_name: "pname1",
addr: "Streat Name",
children: [
{name: "c1"},
{name: "c2"}
]
}
end
its(:body) { should be_json_as result }
end
end
まとめ
実装コードが、たった2行というところが非常によろしいです
pt = Parent.find(name: params[:name])
present pt, with: API::Entities::Parent
また、DBにはchildren.birth_day
というカラムを作成していますが、API::Entities::Child
ではexposeの指定をしていないので、出力を隠すことに成功しています
DBからは取得されてしまっているので通信量では不利ですが、宣言的にコードが書けるため保守性は極めて高いのではないでしょうか