4
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Ruby on Rails】Google Books APIを叩く際の5つのTips

Last updated at Posted at 2020-03-09

想定読者

  • Railsで読書系ポートフォリオを作っている方
$ ruby -v                     
ruby 2.6.5
$ rails -v
Rails 5.2.4.1

その1.APIを叩くロジックはcontrollerから切り分ける

まず以下記事(私の前記事です)のようにAPIを叩くわけですが、これをcontrollerに書いたらあっという間にFat controllerになりました。
Ruby on RailsでGoogle Books APIを叩く

APIを叩くロジックは、以下を参考にmodlueとしてapp/libに置きました。
Rails 5 で自作のモジュールを読み込む方法

APIを叩く自作module

app/lib/google_books_api.rb
module GoogleBooksApi
  def get_json_from_url(url)
    JSON.parse(Net::HTTP.get(URI.parse(Addressable::URI.encode(url))))
  end

  # ①検索するAPIを叩く
  def url_from_keyword(keyword)
    "https://www.googleapis.com/books/v1/volumes?q=#{keyword}&country=JP&maxResults=20"
  end

  # ②IDから本の情報を取得するAPIを叩く
  def url_from_id(googlebooksapi_id)
    "https://www.googleapis.com/books/v1/volumes/#{googlebooksapi_id}"
  end
end

controllerにincludeする

app/controllers/books_controller.rb
class BooksController < ApplicationController
  include GoogleBooksApi
 ()
end

books_controller内で、GoogleBooksAPIというmoduleをincludeしたので、そのmoduleの関数が使えます。

app/libというディレクトリが自作なので、
「そもそもgoogle_books_api.rbを読み込んでくれるの?」
と不安に思うかもしれません。

しかし、実はRailsでは自動的にapp/〇〇(ディレクトリ)/〇〇.rbを読み込んでくれるという仕様があるようです。
※ さらに階層が深くなるとNGみたいです。

その2.APIの構造のうち、itemを理解する

Google Books APIからは下記のように色々な情報が入ってるので、APIに慣れていないと混乱するかもしれません。
https://www.googleapis.com/books/v1/volumes?q=Rails

先に結論を書きます。
https://www.googleapis.com/books/v1/volumes?q=#{keyword}では以下が取り出せます。
{ (他のハッシュ),
"items" => [{ item1 }, { item2 }, { item3 }, ....] }

https://www.googleapis.com/books/v1/volumes/#{googlebooksapi_id}
では以下が取り出せます。
{ item }

①本を検索するAPIも、②IDから本情報を取得するAPIも、結局itemを取り出せるわけです。
(このitemの中に、本1つの情報が入っています)
このitemに対してのロジックを書くだけで、①本を検索するAPIに対しても、②IDから本情報を取得するAPIに対しても、共通のロジックを使うことができます。
よって①本を検索するAPIに対しては、以下のように処理するのが良いと思われます。

(検索するAPIから得たjson文字列)["items"].each do |item|
  (itemに対するロジック)
end

その3.検索時にはActiveRecordのオブジェクトを作らないようにする

これは私がやってしまったアンチパターンです。

最終的には検索した本をActionViewで使う際に、
render @books
あるいは
@books.each do |book|
のような繰り返し処理を書きたいかと思います。

その際に、以下のようにActiveRecordのオブジェクトを大量生成するロジックを作ってしまいました。

ActiveRecordから@booksを作る(アンチパターン)

books_controller
def search
  @books = []
  (items).each do |item|
    (中略)
    @books << Book.new(
      author: (itemから引っ張ってきた著者)
      title: (itemから引っ張ってきたタイトル)
      (その他 )
    )
  end
end

Fat controllerになる以外にも、これの問題点は2つあります。

  1. ActiveRecordのインスタンス生成(Book.new)はコストが高いのに、それを繰り返し処理させている
  2. モデルと同じattirbuteを使わなくてはいけない

1の解説

実際にやってみるとわかります。
検索し始めてから結果が表示されるまで5〜10秒くらいかかってて、UXが悪かったです。

参考(理解はできてません笑):
ActiveRecordのパフォーマンス・チューニング

2の解説

例えば検索結果としては詳細な情報が表示できるようにしたいが、アプリのDBに保存させるつもりは無い、というような場合があります。
パッと思いつくのは、
averageRating: 4.0
amount: 3960.0
みたいな情報でしょうか。レーティングや値段はその時々で変わるので、DBに保存しようとは思わないでしょう。

こういう場合は検索結果表示のときだけbook.averageRatingでレーティングを返せるようにし、DBには保存はしない、という設計が思いつきます。
しかし、ActiveRecordを使うとDBにも同一カラムが存在する必要がある、というわけです。

2つの問題点の原因

実は1,2とも原因は共通していて、要はActiveRecordはO/Rマッパーであるからです。

  • 検索結果はDBに保存するわけではありません(=DBを使いません)
  • ActiveRecordはView <-> DBの仲介役(O/Rマッパー)です。
  • よってActiveRecordを使う必要はありません。(少なくともそういう設計になってません)

ということになります。

その4.APIの情報は、オレオレクラス内に格納する

その3に対して、では具体的にどうするかをお伝えします。
結論から言うと、以下のようなオレオレクラスを作成しました。

オレオレクラス

app/lib/google_book.rb
class GoogleBook
  attr_reader :googlebooksapi_id, :author, :buy_link, :description, :image, :published_at, :title

  class << self
    include GoogleBooksApi

    def new_from_id(googlebooksapi_id)
      url = url_of_creating_from_id(googlebooksapi_id)
      item = get_json_from_url(url)
      new(item)
    end

    def search(keyword)
      url = url_of_searching_from_keyword(keyword)
      json = get_json_from_url(url)
      books = []
      if items = json['items']
        items.each do |item|
          books << GoogleBook.new(item)
        end
      end
      books
    end
  end

  def initialize(item)
    @item = item
    @volume_info = @item['volumeInfo']
    retrieve_attribute
  end

  def retrieve_attribute
    @googlebooksapi_id = @item['id']
    @author = @volume_info['authors'].first
    @buy_link = @item['saleInfo']['buyLink']
    @description = @volume_info['description']
    @image = @volume_info['imageLinks']['smallThumbnail']
    @published_at = @volume_info['publishedDate']
    @title = @volume_info['title']
  end
end

重要なポイントはitemを引数にinitializeができるようにすることです。
その2でも述べたように、itemに対して同じ処理をするように心がければ、共通のロジックを用いることができます。

使用例

これにより、以下のようにオブジェクト志向っぽく扱えるようになります。

book = GoogleBook.new_from_id("axicQgAACAAJ")
book.title
=> "影響力の武器"
book.author
=> "ロバート・B. チャルディーニ"

books = GoogleBook.search("影響力の武器")
=> [ book1, book2, book3, .... ]()
book = books.first
book.id
=> "axicQgAACAAJ"
book.title
=> "影響力の武器"

このクラスはインスタンス生成にかかるコストは大したことはありません。
よってその3のような、ActiveRecordのインスタンスを複数生成時に発生していたコストも解消できています。

注意点

books = GoogleBook.search
のbooksのクラスは、ただのArrayです。そのままではgem kaminariによるpaginateとか、render @booksとかが出来ません。
以下のように続けて書くことで、kaminariのpaginateを利用できます。

@books = Kaminari.paginate_array(books).page(params[:page])

その5.リソース登録時にはモデルにロジックを書く

追記: Fat Modelになってしまうので、あまりいい設計では無かったです。記事の下の方で追記いたしましたので、そちらをご参照いただければと思います。

上記で作ったGoogleBookクラスを使って、いざDBに本を保存するロジックを書こうとすると、これまたFat controllerになりがちです。

app/controllers/books_controller.rb
def create
  google_book = GoogleBook.new_from_id(取ってきたid)
  @book = Book.new(
    author: google_book.author
    title: google_book.title
    (その他 )
  )
  if @book.save
  (以下略)
end

controllerというのは、DBの情報を知りすぎない、というのが良い設計らしいです。上のような書き方は「DBにこれとこれが入るんでしょ」って言ってしまっています。

実装

app/controllers/books_controller.rb
def create
  google_book = GoogleBook.new_from_id(取ってきたid)
  @book = current_user.books.build
  @book = @book.substitute_for_googlebook(google_book)
  if @book.save
  (以下略)
end
app/models/book.rb
def substitute_for_googlebook(google_book)
  self.author = google_book.author
  self.description = google_book.description
  self.googlebooksapi_id = google_book.googlebooksapi_id
  self.published_at = google_book.published_at
  self.title = google_book.title
  self.buy_link = google_book.buy_link
  self.image = google_book.image
  self
end

割とcontrollerはスッキリできたのでは無いでしょうか。

といいながら実はその5はあんまり自信無いです笑
もうちょっと上手くできる気がします。

追記

その5に関して、新たにいいアイディアが浮かびました。結論としては、その4のGoolgeBookクラスにリソース登録のロジックを書く、というものです。

app/lib/google_book.rb
class GoogleBook

  ()

  def build_book_by_user(user)
    user.books.build(
      author: @author,
      description: @description,
      image: @image
      googlebooksapi_id: @googlebooksapi_id,
      published_at: @published_at,
      title: @title,
      buy_link: @buy_link
    )
  end
end

これにより、Bookクラス(ActiveRecord)のインスタンスが返ってきます。

app/controllers/books_controller.rb
def create
  google_book = GoogleBook.new_from_id(取ってきたid)
  @book = google_book.build_book_by_user(current_user)
  if @book.save
  (以下略)
end

createの処理も以前よりはオーソドックスかつスッキリした書き方にできると思います。

4
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?