3
2

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.

GoogleBooksAPIだけで本リソースの取得をする設計を行い、失敗した話

Posted at

対象読者

  • 本に関するRailsのポートフォリオを作っている方々
  • Google Books APIを用いたポートフォリオを作っている方々

外部APIによるリソースを使ったサービスのシステム設計に関する話です。

元々の設計

  • users, books, likesの3つのテーブルがありました。
  • booksの情報はGoogle Books APIから取得し、いちいちbooksテーブルに保存する設計にしていました。

book_db.png

likesテーブル
id user_id book_id
1 1 1
2 1 5
3 2 1
4 2 4
5 6 1
6 1 4

割と標準的な設計かと思います。

何を考えたか

  • Google Books APIの情報をbooksテーブルにいちいち登録するのは冗長じゃないか?と考えました。
  • likesテーブルに登録するbook_idを、Google Books APIのIDに変えれば解決!と思いました。

book_db2.png

likesテーブル
id user_id book_id
1 1 W_6FAwAAQBAJ
2 1 o0JuxK5_1esC
3 2 W_6FAwAAQBAJ
4 2 CrzcDwAAQBAJ
5 6 W_6FAwAAQBAJ
6 2 CrzcDwAAQBAJ

CrzcDwAAQBAJみたいなのは、Google Books APIにおけるIDです。
全部の本にユニークについています。

考えられうるメリット

  • いちいち本の情報をDBに登録する必要がない
  • APIから常に新しい情報を取得できるようになる
  • 後々の、本に関しての機能追加の際に、新しいテーブルを作らなくて済む

やったこと(結論だけを見たい方は飛ばしてください)

1. books/:id:idにGoogle Books APIのIDを入れれば、books#showにてAPIを叩く

app/controllers/books_controller.rb
class BooksController < ApplicationController
  def show
    @book = GoogleBook.new(googlebooksapi_id: params[:id])
  end
end

自分の過去の記事で似たようなことしてるので、興味があればご参照ください。
【Ruby on Rails】Google Books APIを叩く際の5つのTips

2. likesテーブルのinteger型→string型に変える

db/migrate/20202020202020_change_type_of_book_id.rb
class ChangeTypeOfBookId < ActiveRecord::Migration[5.2]
  def change
    remove_foreign_key :likes, :books
    remove_index :likes, :book_id
    change_column :likes, :book_id, :string
  end
end

実際に行いたいのはchange_column :likes, :book_id, :stringでしたが、外部キー制約がついたbook_idの型は変えられないようでした。
なので一度book_idの外部キー制約を外し、型をinteger→stringに変えることに成功しました。

3. 既存のlikesテーブルのbook_idを、Google Books APIのIDに変換するrake taskを作成

lib/tasks/update_book_id_to_googlebooksapi_id.rake
namespace :update_book_id_to_googlebooksapi_id do
  desc 'likesテーブルのbook_idカラムをGoogle Books APIのIDに変更する'
  task update: :environment do
    Like.transaction do
      Like.all.each do |like|
        new_id = like.book.googlebooksapi_id
        like.update!(book_id: new_id)
      end
    end
  end
end

デプロイ時にこれを実行すれば良いだろう、と画策していました。

4. Likeモデルのbelongs_toにオプションを付ける

Likeのモデルテストを走らせてみましたが、以下の失敗が発生。

ActiveRecord::RecordInvalid: バリデーションに失敗しました: Bookを入力してください
app/models/like.rb
class Like < ApplicationRecord
  belongs_to :book
  belongs_to :user
end

どうやらLikeモデルのbelongs_to :bookは外部キー制約におけるヴァリデーションと同じような働きをしてくれるみたいです。
以下のように修正し、テストの失敗を解消させました。

app/models/like.rb
class Like < ApplicationRecord
  belongs_to :book, optional: true
  belongs_to :user
  validates :book_id, presence: true
end

5. has_many :like_books, through: likes, source: :bookが使えないので、自力で実装

app/models/user.rb
class User < ApplicationRecord
  has_many :likes, dependent: :destroy
  has_many :like_books, through: :likes, source: :book

  def like(book)
    like_books << book
  end

  def unlike(book)
    like_books.destroy(book)
  end

  def like?(book)
    like_books.include?(book)
  end
end

source: :bookのbooksテーブルなんてものは使わないというのが今回の方針のため、この便利なhas_manyメソッドは使えません。
ここは仕方なく、似たようなメソッドを自力で実装しました。

app/models/user.rb
class User < ApplicationRecord
  has_many :likes, dependent: :destroy

  def like(book)
    likes.create(book_id: book.googlebooksapi_id)
  end

  def unlike(book)
    like = likes.find_by(book_id: book.googlebooksapi_id)
    like.destroy
  end

  def like?(book)
    likes.all.to_a.map(&:book_id).include?(book.googlebooksapi_id)
  end
end

あとは各所を新設計通りに直して、一応完成はしました。
これでbooksテーブルは不要になる!と思いきや・・・

問題点

1. APIを複数回叩くことになり、コストがやばくなった

例えば、「お気に入り(like)した本の一覧」を表示する画面を想像してみてください。
その本の数だけ、Google Books APIを叩く必要があります。
これにより、ページのロード時間が許容できないレベルとなってしまいました。

Google Books APIが、一度叩けば複数のリソースにアクセスできる、という仕様であればこの問題は解消します。しかし、現状ではそういう仕様ではないため、複数回叩く必要性が生じ、この問題は避けられません。

2. Google Books APIに依存しすぎている

例えば、Google Books APIに大きな仕様変更があり、IDに破壊的な変更が加えられた場合。
その場合、likesテーブルにあったbook_idは全て意味のないデータと化します。
本を主題にしたサービスであれば、サービス終了レベルの被害を被ります。
そうならないためにも、本のAPIは他にも種類がありますし、他のAPIからも情報が取得できる方が、安心できそうです。
となると、やはり本の情報は自前のデータベースである程度持っておいた方が良いということになりそうです。

3. APIの回数制限にひっかかる

また、Google Books APIは1日1000回までという制限があるため、この仕様だとすぐに制限がかかってしまうと考えられます。
というか、サービスとしてスケールしたときを考えると、Google Books APIは回数が少なめですね(今更)

4. 実装を通じ、あまり標準的な設計と思えなかった

これはそこまで大きい問題ではないのですが、Railsのレールに乗っかれてないとは感じました。
特にhas_many :like_booksが使えないと知った時には、レールから外れてる感を強く感じました。
ある程度標準的な設計の方が、機能実装もリファクタリングも楽に実現できるだろうと思います。

以上4つの問題点から、この新設計は取り止めにし、元の設計に戻しました。

教訓

外部APIによる情報は必ずしも自分のDBで情報を持つべきだ、というわけではないではありません。
しかし、少なくとも今回のようなGoogle Books API + 本に関するサービスにおいては、

  • 複数のリソースをまとめて、安いコストで取る方法がない
  • サービスの根幹を支える情報であるため、外部APIを使うには依存度が高くなりすぎる
  • APIの回数制限が厳しい

という性質から、自前データベースでもテーブルとして情報を持っておくべきだと考えました。

3
2
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
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?