26
13

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 1 year has passed since last update.

【Rails】Google Books APIsで本を検索して、DBに保存する方法

Last updated at Posted at 2022-12-09

概要

Google Books APIsを使用してユーザーが本を検索して、検索結果から「よみたいに追加」ボタンを押すと検索結果をDBに保存するアプリを作成しました。試行錯誤しながら実装した実装方法を記録します。

完成版のイメージ

実行環境

この記事は以下環境で動作を確認しました。
Ruby 3.1.2
Rails 6.1.7

Google Books APIsとは?

名前の通りGoogleブックスにある情報を取得できるAPIです。
開発者向けサイト

Google Books APIsの使い方

Google Books APIsの使い方はシンプルです。

https://www.googleapis.com/books/v1/volumes?q=検索条件

「検索条件」に検索するための条件を入れてGETリクエストすると、JSON形式で情報が入手できます。

検索の最大件数(区切り)を設定したい場合はmaxResultsを追加します。
※「はらぺこあおむし」で最大10件とする場合。

https://www.googleapis.com/books/v1/volumes?q=はらぺこあおむし&maxResults=10

上記を実際にgetリクエストして取得した1件目のレスポンスが以下となります。

レスポンス
{
  "kind": "books#volumes",
  "totalItems": 654,
  "items": [
    {
      "kind": "books#volume",
      "id": "BhXmAQAACAAJ",
      "etag": "FY76Xe3RDuE",
      "selfLink": "https://www.googleapis.com/books/v1/volumes/BhXmAQAACAAJ",
      "volumeInfo": {
        "title": "はらぺこあおむし",
        "authors": [
          "Eric Carle"
        ],
        "publisher": "Kaiseisha/Tsai Fong Books",
        "publishedDate": "1976",
        "description": "Japanese edition of \"A Very Hungry Caterpillar.\" Distributed by Tsai Fong Books, Inc.",
        "industryIdentifiers": [
          {
            "type": "ISBN_10",
            "identifier": "4033280103"
          },
          {
            "type": "ISBN_13",
            "identifier": "9784033280103"
          }
        ],
        "readingModes": {
          "text": false,
          "image": false
        },
        "pageCount": 25,
        "printType": "BOOK",
        "categories": [
          "Butterflies"
        ],
        "averageRating": 4,
        "ratingsCount": 6,
        "maturityRating": "NOT_MATURE",
        "allowAnonLogging": false,
        "contentVersion": "preview-1.0.0",
        "panelizationSummary": {
          "containsEpubBubbles": false,
          "containsImageBubbles": false
        },
        "imageLinks": {
          "smallThumbnail": "http://books.google.com/books/content?id=BhXmAQAACAAJ&printsec=frontcover&img=1&zoom=5&source=gbs_api",
          "thumbnail": "http://books.google.com/books/content?id=BhXmAQAACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api"
        },
        "language": "ja",
        "previewLink": "http://books.google.co.jp/books?id=BhXmAQAACAAJ&dq=%E3%81%AF%E3%82%89%E3%81%BA%E3%81%93%E3%81%82%E3%81%8A%E3%82%80%E3%81%97&hl=&cd=1&source=gbs_api",
        "infoLink": "http://books.google.co.jp/books?id=BhXmAQAACAAJ&dq=%E3%81%AF%E3%82%89%E3%81%BA%E3%81%93%E3%81%82%E3%81%8A%E3%82%80%E3%81%97&hl=&source=gbs_api",
        "canonicalVolumeLink": "https://books.google.com/books/about/%E3%81%AF%E3%82%89%E3%81%BA%E3%81%93%E3%81%82%E3%81%8A%E3%82%80%E3%81%97.html?hl=&id=BhXmAQAACAAJ"
      },
      "saleInfo": {
        "country": "JP",
        "saleability": "NOT_FOR_SALE",
        "isEbook": false
      },
      "accessInfo": {
        "country": "JP",
        "viewability": "NO_PAGES",
        "embeddable": false,
        "publicDomain": false,
        "textToSpeechPermission": "ALLOWED",
        "epub": {
          "isAvailable": false
        },
        "pdf": {
          "isAvailable": false
        },
        "webReaderLink": "http://play.google.com/books/reader?id=BhXmAQAACAAJ&hl=&source=gbs_api",
        "accessViewStatus": "NONE",
        "quoteSharingAllowed": false
      },
      "searchInfo": {
        "textSnippet": "Japanese edition of "A Very Hungry Caterpillar." Distributed by Tsai Fong Books, Inc."
      }
    },
    {
      "kind": "books#volume",
      "id": "4-BgYgEACAAJ",
      "etag": "AO67oWMOtDk",
      "selfLink": "https://www.googleapis.com/books/v1/volumes/4-BgYgEACAAJ",
      "volumeInfo": {
        "title": "はらぺこあおむし",
        "subtitle": "ビッグブック",
        "authors": [
          "カール,E.(エリック)"
        ],
        "publishedDate": "1994",
        "industryIdentifiers": [
          {
            "type": "ISBN_10",
            "identifier": "4033211004"
          },
          {
            "type": "ISBN_13",
            "identifier": "9784033211008"
          }
        ],
        "readingModes": {
          "text": false,
          "image": false
        },
        "pageCount": 23,
        "printType": "BOOK",
        "categories": [
          "Butterflies"
        ],
        "maturityRating": "NOT_MATURE",
        "allowAnonLogging": false,
        "contentVersion": "preview-1.0.0",
        "language": "ja",
        "previewLink": "http://books.google.co.jp/books?id=4-BgYgEACAAJ&dq=%E3%81%AF%E3%82%89%E3%81%BA%E3%81%93%E3%81%82%E3%81%8A%E3%82%80%E3%81%97&hl=&cd=2&source=gbs_api",
        "infoLink": "http://books.google.co.jp/books?id=4-BgYgEACAAJ&dq=%E3%81%AF%E3%82%89%E3%81%BA%E3%81%93%E3%81%82%E3%81%8A%E3%82%80%E3%81%97&hl=&source=gbs_api",
        "canonicalVolumeLink": "https://books.google.com/books/about/%E3%81%AF%E3%82%89%E3%81%BA%E3%81%93%E3%81%82%E3%81%8A%E3%82%80%E3%81%97.html?hl=&id=4-BgYgEACAAJ"
      },
      "saleInfo": {
        "country": "JP",
        "saleability": "NOT_FOR_SALE",
        "isEbook": false
      },
      "accessInfo": {
        "country": "JP",
        "viewability": "NO_PAGES",
        "embeddable": false,
        "publicDomain": false,
        "textToSpeechPermission": "ALLOWED",
        "epub": {
          "isAvailable": false
        },
        "pdf": {
          "isAvailable": false
        },
        "webReaderLink": "http://play.google.com/books/reader?id=4-BgYgEACAAJ&hl=&source=gbs_api",
        "accessViewStatus": "NONE",
        "quoteSharingAllowed": false
      }
    },
    {

必要なJSONデータの確認

今回は取得できたJSONの内、書籍タイトル・著者・出版日・サムネ画像のURL・詳細リンクのURL・ISBNの6個を使用してDBにデータを保存します。必要なデータとJSONの格納場所が次の通りであることをJSONを見て確認しました。

必要な情報 格納場所
書籍タイトル items->volumeInfo->title
著者※1 items->volumeInfo->authors
出版日 items->volumeInfo->publishedDate
サムネ画像のURL items->volumeInfo->imageLinks->thumbnail
詳細リンクのURL items->volumeInfo->canonicalVolumeLink
ISBN※2 items->volumeInfo->industryIdentifiers

※1 著者は複数の場合があるので以下のような形で配列で格納されています。

    "authors": [
      "Eric Carle"
    ],

※2 ISBNは10桁と13桁の2つがあるので以下のように格納されています。ISBNは図書館API(カーリル)を使用して図書館情報を表示するために使用します。こちらの実装については後日、別記事で記載します。

    "industryIdentifiers": [
      {
        "type": "ISBN_10",
        "identifier": "4033280103"
      },
      {
        "type": "ISBN_13",
        "identifier": "9784033280103"
      }

アプリの作成

このようなER図となるようモデル、コントローラーを作成します。(作成方法は割愛します)

検索結果の内、著者は複数存在する場合があるので、authoursテーブルに保存して
bookテーブルは中間テーブルのbook_authorsauthoursを通して著者を参照します。
著者以外の検索結果は全てbooksテーブルに保存する方針としました。

格納するデータ Google Books APIsの格納場所 保存場所
書籍タイトル items->volumeInfo->title books->title
著者 items->volumeInfo->authors authors->name
出版日 items->volumeInfo->publishedDate books->publishd_date
サムネ画像のURL items->volumeInfo->imageLinks->thumbnail books->image_link
詳細リンクのURL items->volumeInfo->canonicalVolumeLink books->info_link
ISBN items->volumeInfo->industryIdentifiers books->systemid

gem Faradayの導入

Net::HTTPを使用してコードが煩雑になることを防ぐため、より簡潔にコードが記載できるgemFaradayを使用してAPI通信をする方針としたため、Faradayを導入します。

Gemfileに以下を追加して

Gemfile
gem 'faraday'

インストールします。

bundle install

API通信の作成

ユーザーが入力した検索条件をprams[:seach]で受け取りnilまたはblankでなかった場合にリクエストを送るようにしています。
レスポンスはインスタンス変更の@google_booksに代入してviewで使用できるようにします。
※検索フォームは後ほど作成します。

app/controllers/books_controller.rb
def search 
    if params[:search].nil?
      return
    elsif params[:search].blank?
      flash.now[:danger] = '検索キーワードが入力されていません'
      return
    else
      url = "https://www.googleapis.com/books/v1/volumes"
      text = params[:search]
      res = Faraday.get(url, q: text, langRestrict: 'ja', maxResults: 20)
      @google_books = JSON.parse(res.body)
    end
  end

検索フォームの作成

ユーザーが検索条件を入力する検索フォームを作成します。

app/views/books/search.html.erb
 <%= form_with url: search_books_path, local: true, method: :get do |f| %>
  <%= f.search_field :search, class: "form-control", value: params[:search] %>
  <%= f.submit %>
<% end %>

ルーティングの追加

routes.rb
  resources :books do
    collection { get :search }#このルーティングを追加
  end

これで検索をすることはできるようになったので、取得した値を加工してVIEWに表示させ、ユーザーが登録ボタンを押した場合にDBに登録するよう実装していきます。

application_helperで取得した値を加工する

インスタンス変数の@google_booksにはGoogle Books APIsから取得した値がそのまま入っているため、アプリの形式に合わせたデータとなるようapplication_helperにメソッドを作り加工します。

app/helpers/application_helper.rb
  def google_book_thumbnail(google_book)
    google_book['volumeInfo']['imageLinks'].nil? ? 'sample.jpg' : google_book['volumeInfo']['imageLinks']['thumbnail']
  end

  #thumbnailはネストしている配置となっているのでdigを使って取り出す
  #また画像のリンクがhttpとなっているためgsubを使いhttpsに変更する。変更した値をbookImageに代入する
  def set_google_book_params(google_book)
    google_book['volumeInfo']['bookImage'] = google_book.dig('volumeInfo', 'imageLinks', 'thumbnail')&.gsub("http", "https")

    #ISBNは13桁と10桁があり、どちら1つを取得できればよいので、最初に検索した値をsystemidに代入する
    if google_book['volumeInfo']['industryIdentifiers']&.select { |h| h["type"].include?("ISBN") }.present?
      google_book['volumeInfo']['systemid'] = google_book['volumeInfo']['industryIdentifiers']&.select { |h| h["type"].include?("ISBN") }.first["identifier"]
    end
     #volumeInfoの中が必要な項目のみになるようsliceを使って絞りこむ
    google_book['volumeInfo'].slice('title', 'authors', 'publishedDate', 'infoLink', 'bookImage', 'systemid', 'canonicalVolumeLink')
  end
  • google_book_thumbnailメソッド
    取得した値にサムネイル画像がなかった場合はsample.jpgを付与しています。
  • set_google_book_paramsメソッド
    必要な形式となるようデータを加工します。後ほどこのデータを使ってユーザーが登録ボタンを押したらコントローラーのcreateメソッドに値を送信するように実装します。

投稿フォームの作成

検索した本の情報を表示させ、ユーザーが「よみたいに追加」ボタンを押した場合に、先ほど作成したset_google_book_paramsの値を使用してコントローラーのcreateメソッドに値を送信するように実装します。

app/views/books/search.html.erb

<% if @google_books.present? %>
    #検索した本の情報を表示する
    <% @google_books['items']&.each do |google_book| %>
        <%= image_tag google_book_thumbnail(google_book) %>
        <%= google_book['volumeInfo']['title'] %>
        <%= google_book['volumeInfo']['authors'] %>
        <%= google_book['volumeInfo']['publishedDate'] %>

        #よみたいに追加ボタンを押したらhidden_fieldを使用してbooksコントローラーのcreateに値が送信される
        <%= form_with model: @book, local: true, id: "new_book" do |f| %>
            <%= f.hidden_field :title, value: set_google_book_params(google_book)["title"] %>
            <%= f.hidden_field :published_date, value: set_google_book_params(google_book)["publishedDate"] %>
            <%= f.hidden_field :info_link, value: set_google_book_params(google_book)["canonicalVolumeLink"] %>
            <%= f.hidden_field :image_link, value: set_google_book_params(google_book)["bookImage"] %>
            <%= f.hidden_field :systemid, value: set_google_book_params(google_book)["systemid"] %>
            <% set_google_book_params(google_book)["authors"]&.each do |author| %>
                <%= hidden_field_tag 'book[authors][]', author %>
            <% end %>
            <%= button_tag type: "f.submit" do %>
               <i class="fa-regular fa-square-plus"></i>よみたいに追加
            <% end %>
        <% end %>
    <% end %>
<% end %>

bookモデルに著者が保存されるメソッドを作成する

著者は配列で渡ってくるので、booksコントローラーのcreate時に合わせて保存されるようメソッドを作ります。このメソッドは次に作成するbooks_controllerのcreateメソッドで使用します。

app/models/book.rb
  def save_with_author(authors)
    ActiveRecord::Base.transaction do
      self.save!
      self.authors = authors.uniq.reject(&:blank?).map { |name| Author.find_or_initialize_by(name: name.strip) } unless authors.nil?
    end
    true
    rescue StandardError
      false
  end
  • save_with_authorメソッド
    POSTコントローラーのceateメソッドを実行する時に
    同時に実行してauthorsを保存するためのメソッド。

  • uniq.reject(&:blank?)
    一覧から重複削除してnilを除去。

  • find_or_initialize_by
    条件を指定して初めの1件を取得し1件もなければ作成する。
    Railsドキュメント

  • name.strip
    文字列先頭と末尾の空白文字を全て取り除いた文字列を生成して返す。

booksコントローラーの作成

ユーザーが「よみたいリストに追加」をした本の情報を受け取り保存します。

app/controllers/books_controller.rb
  def create
    @book = current_user.books.build(book_params)
    #モデルに書いたsave_with_authorメソッドを実行する
    if @book.save_with_author(authors_params[:authors])
      redirect_to books_path, success: t('.success')
    else
      flash.now[:danger] = t('.fail')
    end
  end

  private  

  def book_params
    params.require(:book).permit(:title, :image_link, :info_link, :published_date, :systemid)
  end
  
  #authorsは配列で渡ってくるので、配受け取れるように記載
  def authors_params
    params.require(:book).permit(authors: [])
  end

これでDBに保存されるようになります。
保存した一覧を表示する場合はコントローラーにindexメソッドを作成して、VIEWで表示するようにします。

最後に

この記事の内容で実装した、絵本を検索して図書館情報を表示するサービス「Kariteyomu」を以下で公開しています。
よろしければこちらのアプリにもアクセスしてください!

26
13
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
26
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?