LoginSignup
13
3

More than 1 year has passed since last update.

【Rails】gemを使わずに 楽天Apiを動かしてみよう:後編【ハンズオン】

Last updated at Posted at 2022-12-12

この投稿は、
DMM WEBCAMP mentor Advent Calendar 2022
の投稿12日目のエントリーです。

11日目は @takumi3488 さんで
【CSS】テキストの下線(アンダーライン)を太くしたくないですか?僕はしたいです。

環境と前提

  • ruby 3.1.1
  • Rails 6.1.7

↑こちらからの続きとなります。

Apiの理解が・・・という方は是非前編で手を動かしてみて下さい。

1.Railsでアプリを作る

準備

準備は、 rails new をやる準備が整っている前提で進めていきます。

bash
$ rails _6.1.7_ new rakuten_api

でアプリを作成します。

もし、この時点で、

can't find gem railties (= 6.1.7) with executable rails (Gem::GemNotFoundException)

といったエラーが出るようでしたら、

bash
$ gem install rails -v 6.1.7

を実行して、該当バージョンのRailsをインストールしておきましょう。

コマンドの結果は以下となります。

bash
 $ rails new rakuten_api
      create  
      create  README.md
      create  Rakefile
      create  .ruby-version
      create  config.ru
      create  .gitignore
      create  .gitattributes
      create  Gemfile
         run  git init from "."
Initialized empty Git repository in /xxxxxx/xxxxxx/xxxxxxx/xxxxxxx/rakuten_api/.git/
      create  package.json
      create  app
.
.
(省略)
.
.
├─ url-parse@1.5.10
├─ utils-merge@1.0.1
├─ uuid@3.4.0
├─ wbuf@1.7.3
├─ webpack-dev-middleware@3.7.3
├─ webpack-dev-server@3.11.3
├─ websocket-driver@0.7.4
├─ websocket-extensions@0.1.4
└─ ws@6.2.2
✨  Done in 6.53s.
Webpacker successfully installed 🎉 🍰
&

プロジェクトに移動します。

bash
$ cd rakuten_api

ここで、Rails6.1系とruby3.1系との組み合わせで必要となるGemがあります。

Gemfile
gem "net-smtp"
gem "net-imap"
gem "net-pop"

上記3点を追加して、

bash
$ bundle install

を実行しておきましょう。

これで準備は完了です。

Gem

今回はGemを使わずに・・というテーマなので、追加しません。

2.Apiのリクエスト

必要情報

ここで必要情報ですが、前記事をやっている方であれば、楽天ApiのアプリIDは取得しているハズですので、まだの方は取得しておきましょう。

この部分を控えておけばOKです。

ライブラリ

今回は、Railsの lib フォルダ内にApi呼び出しのコードを書いておきたいと思います。

lib/rakuten ディレクトリを作成し、
lib/rakuten/book_search.rb ファイルを作成します。

bash
$ mkdir lib/rakuten         
$ touch lib/rakuten/book_search.rb 

そして、このようなコードを書いておきます。

lib/rakuten/book_search.rb
class Rakuten::BookSearch
  def initialize
    p "initialized!"
  end

  def greet
    p "hello"
  end
end

これで、 Rakuten::BookSearch というクラスを追加する事が出来ます。

ただし、このままではRails側に無視されてしまいます。

試しに、コンソールを起動してみましょう。

bash
$ rails c                      
Running via Spring preloader in process 49332
Loading development environment (Rails 6.1.7)
irb(main):001:0>

続いて、

rails console
irb(main):001:0> Rakuten::BookSearch
(irb):1:in `<main>': uninitialized constant Rakuten (NameError)
Did you mean?  RakutenApi
irb(main):002:0> 

呼び出せないですね。

自動ロード

ここで、今作ったディレクトリ(lib以下)のファイルを自動で読み込む為の設定を追加しておきます。

config/applicaiotn.rb を開きましょう。

config/application.rb
require_relative "boot"

require "rails/all"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module RakutenApi
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.1

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US & Canada)"
    # config.eager_load_paths << Rails.root.join("extras")
  end
end

この中の1行

# config.eager_load_paths << Rails.root.join("extras")

このコメントを外し、追加するディレクトリ名を変更しておきます。

config.eager_load_paths << Rails.root.join("lib")

これを application.rb に反映しておきます。

config/application.rb
require_relative "boot"

require "rails/all"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module RakutenApi
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.1

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US & Canada)"
    config.eager_load_paths << Rails.root.join("lib")
  end
end

では、もう一度実験です。

rails console
 rakuten_api (main #%)% rails c                 
Running via Spring preloader in process 50039
Loading development environment (Rails 6.1.7)
irb(main):001:0> Rakuten::BookSearch
=> Rakuten::BookSearch
irb(main):002:0> rakuten = Rakuten::BookSearch.new
"initialized!"
=> #<Rakuten::BookSearch:0x000000010d6d4238>                                 
irb(main):003:0> rakuten.greet
"hello"
=> "hello"   

インスタンスを作った initialize メソッド実行時には、 "initialized!" と出力され、インスタンスメソッド greet 実行時には "hello" と出力されているようです。

実験が成功したので、一旦このファイルは置いておきます。

アプリIDの扱い

で、せっかくRailsを使いますので、この段階でアプリIDを隠蔽しておきましょう。
基本的にIDを生でGithub等に上げないように注意です。

一旦、IDを保存しておきます。

bash
EDITOR="vi" bin/rails credentials:edit

上記コマンドで、 vi を使ってファイルを編集します。

通常暗号化されている credentials.yml.enc というファイルが開きます。

credentials.yml.enc
# aws:
#   access_key_id: 123
#   secret_access_key: 345

# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: xxxxxxxxx(めっちゃ長いので略。これは消したら駄目なので注意)

このようになっていますので、楽天の アプリID を追記します。

i を押して insertモードに変えてから、

credentials.yml.enc
# aws:
#   access_key_id: 123
#   secret_access_key: 345

rakuten:
  application_id: xxxxここにIDを貼るxxxxx

# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: (ここめっちゃ長いので略。これは消したら駄目なので注意)

このように書き換えて(全角スペース混入やインデントの数がスペース2以外にならないよう注意。rakuten:の前にスペース入っても駄目です。)

esc : w q と順に押して保存します。

では確認しましょう。

rails console
 rakuten_api (main #%)% rails c
Running via Spring preloader in process 51270
Loading development environment (Rails 6.1.7)
irb(main):001:0> Rails.application.credentials.rakuten[:application_id]
=> "xxxxxxxxxxxxxxxxxx"

Rails.application.credentials.rakuten[:application_id] と記述すると、隠蔽したアプリIDの呼び出しが可能となります。

ライブラリへの記述

では、先程作ったファイルでApiの呼び出しを可能にしていきましょう。

では、早速リクエストのURLを作って、パラメーターを作成するようにします。

ちなみに、前篇の最後では、

  • isbn
  • 書籍タイトル
  • 著者名
  • 定価
  • 商品URL
  • 商品画像

この情報が取れれば良いという事でしたが、この情報を絞り込む作業はモデル側でやります。
あくまでライブラリ側は、Apiのデーターにアクセスして、結果を取って来たり、エラーが想定される場合の処理が出来れば良しとしておきます。

もし、欲しい情報が変化しても大丈夫という事になります。

ただし、検索機能として、

  • 書籍タイトル
  • isbn

での検索を想定しますので、これだけはライブラリ側でやってしまいます。

インスタンス作成時の動作

では早速書いていきます。

lib/rakuten/book_search.rb
class Rakuten::BookSearch
  def initialize
    p "initialized!"
  end

  def greet
    p "hello"
  end
end

先程のコードがこのようになっていますので、

lib/rakuten/book_search.rb
class Rakuten::BookSearch

  def initialize
    p application_id
  end

  private

  def application_id
    Rails.application.credentials.rakuten[:application_id]
  end
end

まずは、このように書き換えてみましょう。

コンソールで確認すると、

rails console
irb(main):001:0> Rakuten::BookSearch.new
xxxxxxxxxxxxxxxxxx
=> #<Rakuten::BookSearch:0x000000010b7dcb28>     

xxxの所に取得したアプリIDが出力されればOKです。

続いて、 initialize メソッドに引数を取るようにしましょう。

lib/rakuten/book_search.rb
class Rakuten::BookSearch

  def initialize(*args)
    params = args.extract_options!
    p params[:title]
    p params[:isbn]
  end

  private

  def application_id
    Rails.application.credentials.rakuten[:application_id]
  end
end

と、書き直してみましょう。

initializeメソッドに引数を書いてますが、 * を付ける事で数の定まらない引数を取る事が出来ます。

def initialize(*args)
  params = args.extract_options!
end

このように書く事で、 引数を完全にオプション扱いとし、引数なしでもエラーが出ないようになっています。

ここまでの状態を実験します。
コードを書き換えながらコンソールの実験をする場合、 reload! をするか、一旦 exit でコンソールを止めるようにしておきます。

rails conosole
irb(main):004:0> reload!
Reloading...
=> true               
irb(main):005:0> Rakuten::BookSearch.new
nil
nil
=> #<Rakuten::BookSearch:0x000000010d8aca88>
irb(main):006:0> Rakuten::BookSearch.new(title: "web")
"web"
nil
=> #<Rakuten::BookSearch:0x00000001088a1048>
irb(main):007:0> Rakuten::BookSearch.new(title: "web", isbn: 11111111)
"web"
11111111
=> #<Rakuten::BookSearch:0x0000000109ee2d20>
irb(main):008:0> 
irb(main):008:0> Rakuten::BookSearch.new(description: "hello")
nil
nil                                                                                  
=> #<Rakuten::BookSearch:0x000000010b7ef598> 

これで、特定のパラメーターのみを検索対象とする事が可能となります。

続いて、インスタンス作成時にリクエストURLを作成する状態まで作ってみましょう。

lib/rakuten/book_search.rb
class Rakuten::BookSearch

  def initialize(*args)
    params = args.extract_options!
    @request_url = request_url(params[:title], params[:isbn])
  end

  private

  def application_id
    Rails.application.credentials.rakuten[:application_id]
  end

  def request_url(title, isbn)
    if title.present? || isbn.present?
      book_search_url + paramators(title, isbn)
    end
  end

  def book_search_url
    "https://app.rakuten.co.jp/services/api/BooksBook/Search/20170404?"
  end

  def paramators(title, isbn)
    {
      title: title,
      isbn: isbn,
      format: 'json',
      formatVersion: 2,
      applicationId: application_id
    }.transform_values{|v| v == "" ? v = nil : v }.compact.to_param
  end
end

では、コンソールで動かしましょう。引数に title isbn を入力したり、あえて外したりしてみます。

rails console
irb(main):009:0> reload!
Reloading...
=> true                                                                              
irb(main):010:0> Rakuten::BookSearch.new(title: "web")
=> 
#<Rakuten::BookSearch:0x000000010d99dac8                                             
 @request_url=                                                                       
  "https://app.rakuten.co.jp/services/api/BooksBook/Search/20170404?applicationId=xxxxxxxxxxxxxxxxxxx&format=json&formatVersion=2&title=web">                                                              
irb(main):011:0> Rakuten::BookSearch.new(title: "web", isbn: 1111111)
=> 
#<Rakuten::BookSearch:0x000000010a0d5e48                                             
 @request_url=                                                                       
  "https://app.rakuten.co.jp/services/api/BooksBook/Search/20170404?applicationId=xxxxxxxxxxxxxxxxxxx&format=json&formatVersion=2&isbn=1111111&title=web">  

URLが返ってきていますが、パラメーター部分に注目してみましょう。

title=web
isbn=1111111&title=web

値を入れない場合にプロパティごと省くようにしています。
これは、

  def paramators(title, isbn)
    {
      title: title,
      isbn: isbn,
      format: 'json',
      formatVersion: 2,
      applicationId: application_id
    }.transform_values{|v| v == "" ? v = nil : v }.compact.to_patam
  end

この paramators メソッドの、

.transform_values{|v| v == "" ? v = nil : v }.compact.to_param

この部分です。

transform_values でHashの バリューのみをブロック内の処理を行い、 空文字を nil に変更した後、 compact で バリューが nil のものを排除しています。

これを、 to_param で Hashから、HTMLのリクエストパラメーターに変換します。

そして、 request_url メソッドで、

  def request_url(title, isbn)
    if title.present? || isbn.present?
      book_search_url + paramators(title, isbn)
    end
  end

title isbn の値が入力された場合のみ URLを生成するようになっています。
検索出来ない条件の時は、Apiのリクエストを避けておくように工夫です。

リクエスト

取得出来たURLでいよいよデーターを取得します。

  def result
    if @request_url
      url = URI.open(@request_url).read
      items = JSON.parse(url, { symbolize_names: true })[:Items]
      if items.any?
        {status: :OK, value: JSON.parse(url, { symbolize_names: true })[:Items]}
      else
        {status: :NG, title: "検索できません。", description: "該当するデーターはありません。" }
      end
    else
      {status: :NG, title: "検索できません。", description: "最低1つの検索条件が必要です。" }
    end
  end

このようなメソッドを追加しておきます。

忘れないように open-uriURI などの必要ライブラリも require しておきます。

完成は、下記のようになります。

lib/rakuten/book_search.rb
class Rakuten::BookSearch
  require 'open-uri'
  require 'URI'

  def initialize(*args)
    params = args.extract_options!
    @request_url = request_url(params[:title], params[:isbn])
  end

  def result
    if @request_url
      url = URI.open(@request_url).read
      items = JSON.parse(url, { symbolize_names: true })[:Items]
      if items.any?
        {status: :OK, value: JSON.parse(url, { symbolize_names: true })[:Items]}
      else
        {status: :NG, title: "検索できません。", description: "該当するデーターはありません。" }
      end
    else
      {status: :NG, title: "検索できません。", description: "最低1つの検索条件が必要です。" }
    end
  end

  private

  def application_id
    Rails.application.credentials.rakuten[:application_id]
  end 

  def request_url(title, isbn)
    if title.present? || isbn.present?
      book_search_url + paramators(title, isbn)
    end
  end

  def book_search_url
    "https://app.rakuten.co.jp/services/api/BooksBook/Search/20170404?"
  end

  def paramators(title, isbn)
    {
      title: title,
      isbn: isbn,
      format: 'json',
      formatVersion: 2,
      applicationId: application_id
    }.transform_values{|v| v == "" ? v = nil : v }.compact.to_param
  end
end

これでライブラリ実装最後のテストです。

rails cdonsole
irb(main):017:0> reload!
Reloading...
=> true                                                                                             
irb(main):018:0> Rakuten::BookSearch.new().result
=> {:status=>:ERR, :title=>"検索できません。", :description=>"最低1つの検索条件が必要です。"}
irb(main):019:0> Rakuten::BookSearch.new(isbn: 111).result
=> {:status=>:ERR, :title=>"検索できません。", :description=>"該当するデーターはありません。"}
irb(main):020:0> Rakuten::BookSearch.new(isbn: 9784800721082).result
=> 
{:status=>:OK,                                                                                      
 :value=>                                                                                           
  [{:affiliateUrl=>"",                                                                              
    :author=>"片岡亮太",                                                                            
    :authorKana=>"カタオカリョウタ",                                   
    :availability=>"5",                                                
    :booksGenreId=>"001006018003/001006021",                           
    :chirayomiUrl=>"",                                                 
    :contents=>"",                                                     
    :discountPrice=>0,                              
    (以下略)           

というように、使うかどうかはさておき、例外エラーを出さずに値がない場合も考慮された結果が出てきます。
status: :OK の場合だけ配列でApiの商品データーが取得出来るようになっています。
前篇で少し触れたのですが、今回のリクエストには、パラメーターに formatVersion=2 が加えられているので、前回と少し結果の構造が変わっています。

扱いやすそうなので、入れてみました。

ここまでで、ライブラリの記述は終了です。

3.モデル

データーの呼び出しは出来るようになりましたが、これをRails内で扱う為には、もう少し工夫が必要です。

出来れば、モデルインスタンスでデーターを扱いたいので、一度欲しいデーターをカラムに持つ Book モデルを作ってみましょう。
名称で混乱しないようにまとめておきます。

Bookモデル

名称 楽天Api カラム名
isbn isbn isbn
書籍タイトル title title
著者名 author author
定価 itemPrice item_price
商品URL itemUrl item_url
商品画像 largeImageUrl image_url

全て String型です。
Apiの場合文字としてデーターを取得しますので、特別な理由がない限りは String で良いでしょう。

では、モデルを作成します。

bash
$ rails g model Book isbn title author item_price item_url image_url

マイグレーションファイル

マイグレーションファイルを編集します。

db/migrate/xxxxxxxxxxxx_create_books.rb
class CreateBooks < ActiveRecord::Migration[6.1]
  def change
    create_table :books do |t|
      t.string :isbn
      t.string :title
      t.string :author
      t.string :item_price
      t.string :item_url
      t.string :image_url

      t.timestamps
    end
  end
end

このようになっていますので、 title isbn の項目のみ 必須入力 としておきましょう。

本来は、全部情報は欲しいので、全部必須にしたいのですが、Apiは意図しないデーター抜けなどあるので、NULL制約はあまり付けたくないのが正直な所です。
ですので、ある程度Apiの動作が把握出来てからリセット前提で編集するのも手です。

また、 扱う件数的には検索速度は、あまり気にする必要ないのですが、一応 index も貼っておきます。
ということで、以下のように編集します。

db/migrate/xxxxxxxxxxxx_create_books.rb
class CreateBooks < ActiveRecord::Migration[6.1]
  def change
    create_table :books do |t|
      t.string :isbn, null: false
      t.string :title, null: false
      t.string :author
      t.string :item_price
      t.string :item_url
      t.string :image_url

      t.timestamps
      t.index [:isbn]
      t.index [:title]
    end
  end
end

では、

bash
$ rails db:migrate
Running via Spring preloader in process 76286
== xxxxxxxxxxxxx CreateBooks: migrating ======================================
-- create_table(:books)
   -> 0.0019s
== xxxxxxxxxxxxx CreateBooks: migrated (0.0019s) =============================

マイグレーションが通ったら成功です。

モデルの編集

では、早速モデルに想定のカラムを入力する所まで作ってみましょう。

現状は、何もない状態です。

app/models.book.rb
class Book < ApplicationRecord
end

先程のライブラリを利用して、配列でデーターを取得し、その結果をBookモデルのインスタンスに変換します。
以下のように追記して下さい。

app
class Book < ApplicationRecord
  def self.set_api(*args)
    params = args.extract_options!
    api_data = Rakuten::BookSearch.new(params).result
    if api_data[:status] == :OK
      books = api_data[:value].map do |o|
        book = Book.find_or_initialize_by(isbn: o[:isbn])
        if book.new_record?
          book.assign_attributes(
            title: o[:title],
            author: o[:author],
            item_price: o[:itemPrice],
            item_url: o[:itemUrl],
            image_url: o[:largeImageUrl]
          )
        end
        book
      end
      [:OK, books]
    else
      blank_data = Book.new
      blank_data.errors.add(api_data[:title], api_data[:description])
      [:NG, blank_data]
    end
  end
end

では、少し内容を見てみましょう。

ポイントとなるコードはここです。

books = api_data[:value].map do |o|
        book = Book.find_or_initialize_by(isbn: o[:isbn])
        if book.new_record?
          book.assign_attributes(
            title: o[:title],
            author: o[:author],
            item_price: o[:itemPrice],
            item_url: o[:itemUrl],
            image_url: o[:largeImageUrl]
          )
        end
        book
      end

まず、 find_or_initialize_by を用いて、

book = Book.find_or_initialize_by(isbn: o[:isbn])

Bookモデルのインスタンスを、DBから isbn で検索し、見つからなければ、DBに登録せずに、 isbn に値を入れた状態で、インスタンスを作ります。

次に、 new_record? でインスタンスの id の有無をチェックします。
もし id が存在しなければ新規作成となりますので、他のカラムのデーターを入力する必要があります。

        if book.new_record?
          book.assign_attributes(
            title: o[:title],
            author: o[:author],
            item_price: o[:itemPrice],
            item_url: o[:itemUrl],
            image_url: o[:largeImageUrl]
          )
        end

assign_attributes を使う事により、こちらもまだ save されません。
一番のポイントは、このメソッドではデーターの登録をしないという事です。
いたずらにレコード数が増えるのを避けるため、今回はデーターを1件選ぶ時点でレコードさせようと企んでみます。

ここまでを確認しましょう。

rails console
irb(main):001:0> books = Book.set_api(title: "webdb")
   (0.9ms)  SELECT sqlite_version(*)
  Book Load (0.2ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784297132453"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784297131111"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784297122157"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784297127893"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784297128906"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784297124359"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784297130008"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784297127053"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784297122874"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784297118112"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784774196862"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784534042477"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784297125394"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784798122847"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784873118062"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784297122072"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784297119607"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784297115661"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784297114664"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784297116699"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784297113452"], ["LIMIT", 1]]
  Book Load (0.0ms)  SELECT "books".* FROM "books" WHERE "books"."isbn" = ? LIMIT ?  [["isbn", "9784798121758"], ["LIMIT", 1]]
=> 
[:OK,
...
irb(main):002:0> 

SELECTのみが走っています。登録はしていません。
変数の中身を見てみましょう。

rails conosole
irb(main):002:0> books
=> 
[:OK,                        
 [#<Book:0x0000000108751170  
   id: nil,                  
   isbn: "9784297132453",    
   title: "WEB+DB PRESS Vol.132",
   author: "WEB+DB PRESS編集部",
   item_price: "1628",          
   item_url: "https://books.rakuten.co.jp/rb/17341512/",
   image_url: "https://thumbnail.image.rakuten.co.jp/@0_mall/book/cabinet/2453/9784297132453_1_3.jpg?_ex=200x200",
   created_at: nil,
   updated_at: nil>,
  #<Book:0x0000000108fc2a20
   id: nil,
   isbn: "9784297131111",
   title: "WEB+DB PRESS Vol.131",
   author: "WEB+DB PRESS編集部",
   item_price: "1628",
   item_url: "https://books.rakuten.co.jp/rb/17263689/",
   image_url: "https://thumbnail.image.rakuten.co.jp/@0_mall/book/cabinet/1111/9784297131111_1_4.jpg?_ex=200x200",
   created_at: nil,
   updated_at: nil>
   (以下略)

インスタンスになっていますね。

検索エラーの場合も見ておきます。

irb(main):007:0> books = Book.set_api(title: "")
=> 
[:NG,                                                                                  
...                                                                                    
irb(main):008:0> books.last.errors.full_messages
=> ["検索できません。 最低1つの検索条件が必要です。"]

エラーの時は、モデルのエラー機能を使ってメッセージを吐かせます。

ここまでで、モデルは終了とします。

4.コントローラー

登録のタイミング

登録のタイミングですが、 show のページに入る前に create を通過するというパターンにしてみます。
ユースケース的に、ブックマークと同時とかの方が良いかも知れませんが、今回は簡単に済ませます。

コントローラー

index show create の 3つのアクションを作ります。

rails g controller Books index show create 
bash
$ rails g controller Books index show create 
Running via Spring preloader in process 80007
      create  app/controllers/books_controller.rb
       route  get 'books/index'
              get 'books/show'
              get 'books/create'
      invoke  erb
      create    app/views/books
      create    app/views/books/index.html.erb
      create    app/views/books/show.html.erb
      create    app/views/books/create.html.erb
      invoke  test_unit
      create    test/controllers/books_controller_test.rb
      invoke  helper
      create    app/helpers/books_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/books.scss 

次に、 ルーティングも触っておきます。

config/routes.rb
Rails.application.routes.draw do
  get 'books/index'
  get 'books/show'
  get 'books/create'
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end

このようになっていますので、以下のように書き換えましょう。

config/routes.rb
Rails.application.routes.draw do
  root 'books#index'
  resources :books, only: [:index, :create, :show]
end

では、コントローラーを書いていきます。
現状は、このようになっていますので、

app/controllers/books_controller.rb
class BooksController < ApplicationController
  def index
  end
  
  def show
  end

  def create
  end
end

下記のように変更しましょう。

app/controllers/books_controller.rb
class BooksController < ApplicationController
  def index
    @books = []
    if params[:title].present? || params[:isbn].present?
      rakuten_result = Book.set_api(title: params[:title], isbn: params[:isbn])
    end
    if rakuten_result.first == :OK
      @books = rakuten_result.last
    elsif rakuten_result.first == :NG
      @errors = rakuten_result.last
    end
  end

  def show
    @book = Book.find(params[:id])
  end

  def create
    @book = Book.new(book_params)
    if @book.save
      flash[:success] = "DBに登録しました。"
      redirect_to @book
    else
      @books = []
      flash.now[:danger]
      render :index
    end
  end

  private

  def book_params
    params.require(:book).permit(:isbn, :title, :author, :item_price, :item_url, :image_url)
  end
end

index

index では、
@booksに配列データーを入れておきます。
title isbn パラメーターがない時は空配列ですが、

    @books = []
    if params[:title].present? || params[:isbn].present?
      rakuten_result = Book.set_api(title: params[:title], isbn: params[:isbn])
    end

パラメーターを取得すると、 set_api を実行し、

    if rakuten_result.first == :OK
      @books = rakuten_result.last
    elsif rakuten_result.first == :NG
      @errors = rakuten_result.last
    end

結果がきちんと返ってくればOKです。

一旦ここまでの機能の View も書いておきましょう。

現状このようになっていますので、

app/views/books/index.html
<h1>Books#index</h1>
<p>Find me in app/views/books/index.html.erb</p>

下記のように追記しておきましょう。

app/views/books/index.html
<h1>Books#index</h1>
<p>Find me in app/views/books/index.html.erb</p>

<% if @errors.present? %>
  <% @errors.errors.full_messages.each do |message| %>
    <%= message %><br>
  <% end %>
<% end %>

<%= form_with(url: root_path, method: :get, local: true) do |f| %>
  <%= f.search_field :title, placeholder: "title", value: params[:title] %><br>
  <%= f.search_field :isbn, placeholder: "isbn", value: params[:isbn] %><br>
  <%= f.submit "検索" %>
<% end %>

<% @books.each do |book| %>
  <div>
    <ul>
      <li><%= image_tag book.image_url %></li>
      <li><%= book.isbn %></li>
      <li><%= book.title %></li>
      <li><%= book.author %></li>
      <li><%= book.item_price %></li>
      <li><%= link_to "商品リンク", book.item_url %></li>
    </ul>
  </div>
  <hr>
<% end %>

ざっくり検索フォームに入れた値で検索すると、結果が一覧表示されるというものですので、入力して試してみましょう。

bash
$ rails s                
=> Booting Puma
=> Rails 6.1.7 application starting in development 
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 5.6.5 (ruby 3.1.1-p18) ("Birdie's Version")
*  Min threads: 5
*  Max threads: 5
*  Environment: development
*          PID: 87181
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop

トップページに接続します。

このようなフォームが出てきますので、titleに適当に入れてみましょう。

試しに ruby と、入れると、

リストが出てきました。

続いて、タイトルとしてありえないものを入れてみると、

エラー文が出てきました。良さそうです。

isbn でも検索出来ているようです。

では、ここから、 create => show の流れを作っていきましょう。

登録する時に再度Apiにリクエストしたくないので、現在インスタンスに保持しているデーターをフォームから送ります。

id を書いているのは rails が作るフォームのHTMLの id はモデル名に依存するので、同一画面に同じフォームを作る場合は、 id の重複を避けるようにしています。

下記のようなフォームになり、hidden_field を使っていますので、見た目にはボタンしかありません。

      <%= form_with(model: book, url: books_path, method: :post, local: true, id: "book_#{book.isbn}") do |f| %>
        <%= f.hidden_field :isbn %>
        <%= f.hidden_field :title %>
        <%= f.hidden_field :author %>
        <%= f.hidden_field :item_price %>
        <%= f.hidden_field :item_url %>
        <%= f.hidden_field :image_url %>
        <%= f.submit "DBに登録" %>
      <% end %>

あとは、一度登録した後は、不要な処理となりますので、

<%= button_to "詳細ページへ", book, method: :get %>

このようなボタンを作って条件式で出し分けます。

app/views/books/index.html.erb は最終的にこうなります。

app/views/books/index.html.erb
<h1>Books#index</h1>
<p>Find me in app/views/books/index.html.erb</p>
<% if @errors.present? %>
  <% @errors.errors.full_messages.each do |message| %>
    <%= message %><br>
  <% end %>
<% end %>

<%= form_with(url: root_path, method: :get, local: true) do |f| %>
  <%= f.search_field :title, placeholder: "title", value: params[:title] %><br>
  <%= f.search_field :isbn, placeholder: "isbn", value: params[:isbn] %><br>
  <%= f.submit "検索" %>
<% end %>

<% @books.each do |book| %>
  <div>
    <ul>
      <li><%= image_tag book.image_url %></li>
      <li><%= book.isbn %></li>
      <li><%= book.title %></li>
      <li><%= book.author %></li>
      <li><%= book.item_price %></li>
      <li><%= link_to "商品リンク", book.item_url %></li>
    </ul>
  </div>
  <div>
    <% if book.new_record? %>
      <%= form_with(model: book, url: books_path, method: :post, local: true, id: "book_#{book.isbn}") do |f| %>
        <%= f.hidden_field :isbn %>
        <%= f.hidden_field :title %>
        <%= f.hidden_field :author %>
        <%= f.hidden_field :item_price %>
        <%= f.hidden_field :item_url %>
        <%= f.hidden_field :image_url %>
        <%= f.submit "DBに登録" %>
      <% end %>
    <% else %>
      <%= button_to "詳細ページへ", book, method: :get %>
    <% end %>
  </div>
  <hr>
<% end %>

show

詳細画面も作っておきましょう。

app/views/books/show.html.erb
<h1>Books#show</h1>
<p>Find me in app/views/books/show.html.erb</p>

現状こうなっていますので、

app/views/books/show.html.erb
<h1>Books#show</h1>
<p>Find me in app/views/books/show.html.erb</p>
<ul>
  <li><%= image_tag book.image_url %></li>
  <li><%= book.isbn %></li>
  <li><%= book.title %></li>
  <li><%= book.author %></li>
  <li><%= book.item_price %></li>
  <li><%= link_to "商品リンク", book.item_url %></li>
</ul>

<%= link_to "戻る", root_path %>

このように書き換えておきましょう。

では、動作確認です。

トップページでは、

登録済みのボタンと、 未登録のボタンが変化しています。

詳細画面では、

このようになっています。 url も DBに登録されたものを見にいきますので、

books/1

のようなパスになっている事を確認しておきましょう。

後は多少の仕上げですが、ボタン自体は隠していますが、 create の処理が重複する事を考えて、 before_action を書いておきます。

  before_action :isbn_check, only: :create
    ()
  def isbn_check
    @book = Book.find_by(isbn: params.dig('book', 'isbn'))
    redirect_to @book if @book
  end

もしisbn でレコードが見つかったら、その詳細ページに直接飛ぶように before_action を書いておきます。

最終的にコントローラーは、こうなります。

app/controllers/books_controller.rb
class BooksController < ApplicationController
  before_action :isbn_check, only: :create
  def index
    @books = []
    if params[:title].present? || params[:isbn].present?
      rakuten_result = Book.set_api(title: params[:title], isbn: params[:isbn])
    end
    if rakuten_result&.first == :OK
      @books = rakuten_result.last
    elsif rakuten_result&.first == :NG
      @errors = rakuten_result.last
    end
  end

  def show
    @book = Book.find(params[:id])
  end

  def create
    @book = Book.new(book_params)
    if @book.save
      flash[:success] = "DBに登録しました。"
      redirect_to @book
    else
      @books = []
      flash.now[:danger]
      render :index
    end
  end

  private

  def isbn_check
    @book = Book.find_by(isbn: params.dig('book', 'isbn'))
    redirect_to @book if @book
  end

  def book_params
    params.require(:book).permit(:isbn, :title, :author, :item_price, :item_url, :image_url)
  end
end

もし、動作を試したいのであれば、 app/views/books/index.html.erbのボタン部分

  <div>
    <% if book.new_record? %>
      <%= form_with(model: book, url: books_path, method: :post, local: true, id: "book_#{book.isbn}") do |f| %>
        <%= f.hidden_field :isbn %>

この条件式の

<% if book.new_record? %>

<% if true %>

にすると、常時未登録側のボタンが出現しますので、登録済みのレコードで試してみて下さい。
成功すれば、レコードが登録されず、詳細画面に遷移します。

一通り遷移がつながったら完成です。

お疲れ様でした。

結論

Apiはインスタンスに変換して扱うととてもラクに処理が出来る。

終わりに

少し長くなりましたが、楽天Apiをコンソールから扱ってアプリに組み込んでみました。

ここまでをgithubでも見られるようにしました。

ここまで見ていただきありがとうございます。

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