lasciva blog

開発して得た知見やwebビジネスのストック

RailsとiOSアプリのAPIにProtocol bufferを導入した

導入のモチベーション

  • 型安全にしたい
    • 但し、APIの実装コストはjsonに比べると少し増えた。
  • 表示速度を向上させたい
  • ドキュメント管理を簡単にしたい
    • 部分的には緩和されたが、Qiitaの記事とかにも挙がってるようにoptionalの表現が悩ましいので、コメントも併記する運用になりそう。

環境

  • サーバ
  • クライアント
    • iOS
    • swift 4.2.1

導入手順

protoファイル

多くのプロジェクトでは、専用のレポジトリで管理することになると思う。

ディレクトリ構成

products/product_detail_message.protoのようにentityの種類毎に区切った。

※ 注意点

  • ディレクトリが別であっても、message名はユニークに命名すること。
    • 自動生成されるクラスの都合上、名前空間みたいなものが考慮されないので、クラス名がユニークでないといけない模様。
    • もしかしたら回避できるオプションがあるかもしれないが、調べたところ引っかからなかった。
  • messageのkey名に descriptionを指定できない。
    • Swift側で提供される関数に競合してしまうみたいだった。

サーバ

ライブラリ

これがググってもあまり引っかからなくて、なかなかハマった。。

# Gemfile
gem 'grpc'       # 1.17.0で動作確認
gem 'grpc-tools' # 1.17.0で動作確認

ruby_protobufを使う記事等があったが、上述のgem以外だと proto3の形式に対応していないみたいだった。
proto2の形式なら、他のgemで大丈夫そう。

実装

1 . protoファイルから対応するrbファイルを作成

# このあたりの構成はプロジェクト次第
mkdir app/messages
bundle exec grpc_tools_ruby_protoc -I ./../protobuf --ruby_out=./app/messages products/product_detail_message.proto

※ ↑の構成だと、ファイル名と自動生成されるクラス名が異なったりして、読み込み周りの解決が色々大変だった。

2 . decoratorやpresenterで適切に加工して返す

# app/controllers/products_controller.rb

def show
  product = Product.find(params[:id])
  message = ProductDetailMessage.new(id: product.id, name: product.name)
  render plain: message.to_proto
end

キャッシュする場合は、seriarizedした結果をキャッシュした方が良さそうだった。

def product_message(product)
  serialized_proto = cached_proto(product)
  message = ProductDetailMessage.decode(serialized_proto)
  message.is_bookmarked = @current_user.bookmarked?(product)
  message
end

def cached_proto(product)
  Rails.cache.fetch('iikanji_no_key', expires_in: 5.minutes) do
    ProductDetailMessage.new(id: product.id, name: product.name).to_proto
  end
end

jsonと併用する場合

1 . mime_typeを登録

# config/initializers/mime_types.rb
Mime::Type.register 'application/protobuf', :protobuf

2 . controllerで制御

# app/controllers/products_controller.rb

def show
  product = Product.find(params[:id])
  message = ProductDetailMessage.new(id: product.id, name: product.name)
  respond_to do |format|
    format.json { render json: message.to_h }
    format.protobuf { render plain: message.to_proto }
  end
end

アプリ

インストール

1 . protobufのインストール

brew install protobuf

2 . protoc-gen-swiftのインストール

git clone https://github.com/apple/swift-protobuf
cd swift-protobuf

# Cartfileと同じバージョンを指定する
git checkout 1.3.1
swift build

# いい感じにPATHを通す
# 以下は例
cp .build/debug/protoc-gen-swift /usr/local/bin/

3 . iOSのプロジェクトにライブラリ追加

carthageやcocoapodsで以下のライブラリを追加

GitHub - apple/swift-protobuf: Plugin and runtime library for using protobuf with Swift

実装

1 . 最新のprotoファイルから対応するswiftファイルを作成

protoc -I ../protobuf --swift_out=./project_name/proto products/product_detail_message.proto

2 . 生成されたswiftファイルをプロジェクトに追加

3 . APIリクエストのヘッダに application/protobufを指定してリクエス

4 . APIレスポンスを自動生成されたmessageのclassでパース