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

この投稿は、
[DMM WEBCAMP mentor Advent Calendar 2022](https://qiita.com/advent-calendar/2022/infratop_mentor)
の投稿12日目のエントリーです。

11日目は @takumi3488 さんで
[【CSS】テキストの下線(アンダーライン)を太くしたくないですか？僕はしたいです。](https://qiita.com/takumi3488/items/074a995cb4a5313991eb)

## 環境と前提

- ruby 3.1.1
- Rails 6.1.7

https://qiita.com/tomoaki-kimura/items/635b7f136a9967b457b4

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

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

## 1.Railsでアプリを作る

### 準備

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

```bash:bash
$ rails _6.1.7_ new rakuten_api
```

でアプリを作成します。

もし、この時点で、

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

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

```bash:bash
$ gem install rails -v 6.1.7
```

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

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

```bash: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:bash
$ cd rakuten_api
```

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


```ruby:Gemfile
gem "net-smtp"
gem "net-imap"
gem "net-pop"
```

上記３点を追加して、

```bash:bash
$ bundle install
```
を実行しておきましょう。

これで準備は完了です。

### Gem

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

## 2.Apiのリクエスト

### 必要情報

ここで必要情報ですが、[前記事](https://qiita.com/tomoaki-kimura/items/635b7f136a9967b457b4)をやっている方であれば、楽天Apiの[アプリID](https://webservice.rakuten.co.jp/app/list)は取得しているハズですので、まだの方は取得しておきましょう。

![](https://gyazo.com/82ba69f50cd84899c8ff711b9091c9f6.png)

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

### ライブラリ

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

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

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

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

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

  def greet
    p "hello"
  end
end
```

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

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

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

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

続いて、
```irb: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` を開きましょう。

```ruby: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
```

この中の１行
```ruby
# config.eager_load_paths << Rails.root.join("extras")
```

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

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

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

```ruby: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
```

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

```irb: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:bash
EDITOR="vi" bin/rails credentials:edit
```

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

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

```yml: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モードに変えてから、

```yml: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: (ここめっちゃ長いので略。これは消したら駄目なので注意)
```

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

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

では確認しましょう。

```irb: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

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

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

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

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

  def greet
    p "hello"
  end
end
```

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

```ruby: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
```

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

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

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

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

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

```ruby: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`メソッドに引数を書いてますが、 `*` を付ける事で数の定まらない引数を取る事が出来ます。

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

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

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

```irb: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を作成する状態まで作ってみましょう。

```ruby: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` を入力したり、あえて外したりしてみます。

```irb: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
```

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

```ruby
  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` メソッドの、

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

この部分です。 

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

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

そして、 `request_url` メソッドで、

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

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

### リクエスト

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

```ruby
  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: "最低１つの検索条件が必要です。" }
    end
  end
```

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

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

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

```ruby: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: "最低１つの検索条件が必要です。" }
    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
```

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

```irb:rails cdonsole
irb(main):017:0> reload!
Reloading...
=> true                                                                                             
irb(main):018:0> Rakuten::BookSearch.new().result
=> {:status=>:ERR, :title=>"検索できません。", :description=>"最低１つの検索条件が必要です。"}
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` が加えられているので、前回と少し結果の構造が変わっています。

![](https://gyazo.com/467cfd2cafb09de3df4d4db5fc837d89.png)

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

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

## 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:bash
$ rails g model Book isbn title author item_price item_url image_url
```

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

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

```ruby: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` も貼っておきます。
ということで、以下のように編集します。

```ruby: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:bash
$ rails db:migrate
Running via Spring preloader in process 76286
== xxxxxxxxxxxxx CreateBooks: migrating ======================================
-- create_table(:books)
   -> 0.0019s
== xxxxxxxxxxxxx CreateBooks: migrated (0.0019s) =============================
```
マイグレーションが通ったら成功です。

### モデルの編集 

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

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

```ruby:app/models.book.rb
class Book < ApplicationRecord
end
```

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

```ruby: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
```

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

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

```ruby
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` を用いて、

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

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

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

```ruby
        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` されません。
一番のポイントは、このメソッドではデーターの登録をしないという事です。
いたずらにレコード数が増えるのを避けるため、今回はデーターを１件選ぶ時点でレコードさせようと企んでみます。

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

```irb: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のみが走っています。登録はしていません。
変数の中身を見てみましょう。

```irb: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
=> ["検索できません。 最低１つの検索条件が必要です。"]
```

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

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

## 4.コントローラー

### 登録のタイミング

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

### コントローラー

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

```
rails g controller Books index show create 
```

```bash: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 
```

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

```ruby: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
```

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

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

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

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

  def create
  end
end
```

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

```ruby: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` パラメーターがない時は空配列ですが、

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

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

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

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

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

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

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

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

```erb: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: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
```

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

![](https://gyazo.com/563be2c61fb05b2b3af8e72adc25e257.png)

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

試しに `ruby` と、入れると、


![](https://gyazo.com/0ef9f2c4463761efb03b86d67317efac.png)

リストが出てきました。

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

![](https://gyazo.com/7510bc5cd0c0f0ad206d9ca0cc9b6633.png)

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

![](https://gyazo.com/2ee09b00bd215677b2888da94895a83d.png)

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

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

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

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

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

```erb
      <%= 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 %>
```

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

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

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

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

```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

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

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

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

```erb: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 %>
```

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

では、動作確認です。

トップページでは、

![](https://gyazo.com/588c93e86b8fa8b405ba6c1ca1ab45b6.png)

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

詳細画面では、

![](https://gyazo.com/d3f382f74cbc9f7e35c10acc01b7ce11.png)

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

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

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

```ruby
  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` を書いておきます。

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

```ruby: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`のボタン部分

```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 %>
```

この条件式の

```erb
<% if book.new_record? %>
```

を

```erb
<% if true %>
```

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

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

お疲れ様でした。

## 結論

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

## 終わりに

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

ここまでを[github](https://github.com/tomoaki-kimura/rakuten_api)でも見られるようにしました。

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