この投稿は、
DMM WEBCAMP mentor Advent Calendar 2022
の投稿12日目のエントリーです。
11日目は @takumi3488 さんで
【CSS】テキストの下線(アンダーライン)を太くしたくないですか?僕はしたいです。
環境と前提
- ruby 3.1.1
- Rails 6.1.7
↑こちらからの続きとなります。
Apiの理解が・・・という方は是非前編で手を動かしてみて下さい。
1.Railsでアプリを作る
準備
準備は、 rails new
をやる準備が整っている前提で進めていきます。
$ rails _6.1.7_ new rakuten_api
でアプリを作成します。
もし、この時点で、
can't find gem railties (= 6.1.7) with executable rails (Gem::GemNotFoundException)
といったエラーが出るようでしたら、
$ gem install rails -v 6.1.7
を実行して、該当バージョンのRailsをインストールしておきましょう。
コマンドの結果は以下となります。
$ 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 🎉 🍰
&
プロジェクトに移動します。
$ cd rakuten_api
ここで、Rails6.1系とruby3.1系との組み合わせで必要となるGemがあります。
gem "net-smtp"
gem "net-imap"
gem "net-pop"
上記3点を追加して、
$ bundle install
を実行しておきましょう。
これで準備は完了です。
Gem
今回はGemを使わずに・・というテーマなので、追加しません。
2.Apiのリクエスト
必要情報
ここで必要情報ですが、前記事をやっている方であれば、楽天ApiのアプリIDは取得しているハズですので、まだの方は取得しておきましょう。
この部分を控えておけばOKです。
ライブラリ
今回は、Railsの lib フォルダ内にApi呼び出しのコードを書いておきたいと思います。
lib/rakuten
ディレクトリを作成し、
lib/rakuten/book_search.rb
ファイルを作成します。
$ mkdir lib/rakuten
$ touch lib/rakuten/book_search.rb
そして、このようなコードを書いておきます。
class Rakuten::BookSearch
def initialize
p "initialized!"
end
def greet
p "hello"
end
end
これで、 Rakuten::BookSearch
というクラスを追加する事が出来ます。
ただし、このままではRails側に無視されてしまいます。
試しに、コンソールを起動してみましょう。
$ rails c
Running via Spring preloader in process 49332
Loading development environment (Rails 6.1.7)
irb(main):001:0>
続いて、
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
を開きましょう。
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
に反映しておきます。
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
では、もう一度実験です。
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を保存しておきます。
EDITOR="vi" bin/rails credentials:edit
上記コマンドで、 vi
を使ってファイルを編集します。
通常暗号化されている 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モードに変えてから、
# 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
と順に押して保存します。
では確認しましょう。
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
での検索を想定しますので、これだけはライブラリ側でやってしまいます。
インスタンス作成時の動作
では早速書いていきます。
class Rakuten::BookSearch
def initialize
p "initialized!"
end
def greet
p "hello"
end
end
先程のコードがこのようになっていますので、
class Rakuten::BookSearch
def initialize
p application_id
end
private
def application_id
Rails.application.credentials.rakuten[:application_id]
end
end
まずは、このように書き換えてみましょう。
コンソールで確認すると、
irb(main):001:0> Rakuten::BookSearch.new
xxxxxxxxxxxxxxxxxx
=> #<Rakuten::BookSearch:0x000000010b7dcb28>
xxxの所に取得したアプリIDが出力されればOKです。
続いて、 initialize
メソッドに引数を取るようにしましょう。
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
でコンソールを止めるようにしておきます。
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を作成する状態まで作ってみましょう。
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(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-uri
や URI
などの必要ライブラリも require
しておきます。
完成は、下記のようになります。
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
これでライブラリ実装最後のテストです。
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 で良いでしょう。
では、モデルを作成します。
$ rails g model Book isbn title author item_price item_url image_url
マイグレーションファイル
マイグレーションファイルを編集します。
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
も貼っておきます。
ということで、以下のように編集します。
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
では、
$ rails db:migrate
Running via Spring preloader in process 76286
== xxxxxxxxxxxxx CreateBooks: migrating ======================================
-- create_table(:books)
-> 0.0019s
== xxxxxxxxxxxxx CreateBooks: migrated (0.0019s) =============================
マイグレーションが通ったら成功です。
モデルの編集
では、早速モデルに想定のカラムを入力する所まで作ってみましょう。
現状は、何もない状態です。
class Book < ApplicationRecord
end
先程のライブラリを利用して、配列でデーターを取得し、その結果をBookモデルのインスタンスに変換します。
以下のように追記して下さい。
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件選ぶ時点でレコードさせようと企んでみます。
ここまでを確認しましょう。
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(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
$ 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
次に、 ルーティングも触っておきます。
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
このようになっていますので、以下のように書き換えましょう。
Rails.application.routes.draw do
root 'books#index'
resources :books, only: [:index, :create, :show]
end
では、コントローラーを書いていきます。
現状は、このようになっていますので、
class BooksController < ApplicationController
def index
end
def show
end
def create
end
end
下記のように変更しましょう。
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
も書いておきましょう。
現状このようになっていますので、
<h1>Books#index</h1>
<p>Find me in app/views/books/index.html.erb</p>
下記のように追記しておきましょう。
<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 %>
ざっくり検索フォームに入れた値で検索すると、結果が一覧表示されるというものですので、入力して試してみましょう。
$ 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
は最終的にこうなります。
<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
詳細画面も作っておきましょう。
<h1>Books#show</h1>
<p>Find me in app/views/books/show.html.erb</p>
現状こうなっていますので、
<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
を書いておきます。
最終的にコントローラーは、こうなります。
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でも見られるようにしました。
ここまで見ていただきありがとうございます。