20200310のRubyに関する記事は21件です。

CSS、SCSSファイルの読み込み

CSSファイルの読み込み

rails newによってアプリケーションを作成した際にはapplication.cssに

*= require_tree .

の記述がある。この記述によって同じディレクトリにあるcssファイルは読み込まれる。
またこのrequireはアセットパイプラインの仕組みによってファイルを読み込む。
アセットパイプラインはcssファイルやjavascriptファイルを一つにまとめて圧縮し処理速度をあげる仕組み。
アセットパイプラインはsprocketsと言うgemによって実装されている。

SCSSファイルの読み込み

scssファイルはapplication.scssに@importを記述することでscssファイルをインポートする。
この@importはscssのメソッドであり拡張子をscss(application.scss)にしないと使えない。
application.scssからscssファイルをインポートするために使用するメソッドである。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アプリケーションを作ってみます

自分用メモ

new コマンド実行
rails 5.0.7.2 new Put.Memo -d mysql
データベースを作成
rails db:create

作るのは単語帳アプリ
最初にデータベース設計をしてみて自分のしてみたいことを整理してみた
機能として
---アカウント機能
---投稿機能
---いいね機能
---検索機能
---意味の部分にurl,画像を描けるようにする
---ランキング機能

82486AC1-7B32-4DAC-8A93-1D76A94AED99_1_105_c.jpeg

次にフロント実装

topページには
:スクロール機能
:ランキング機能

↑とりあえずこれだけ実装したい
できればタグごとのランキング,ページネーション機能をつけたい

ビューはこんな感じ
単語、意味とurlと画像
で一つの投稿としてこれをランキングで複数並べる形にしたい
Put.Memoのところをクリックするとでtopページに戻るようにした
ヘッダーだけだけどまぁ機能としては上だけで十分だと思う
https://gyazo.com/429eb62d3c07605ff058e91be4f43712

後で
タイトルのところに作った人のアカウント名を付けようと思う

とりあえず今日はここまで

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

can't find gem bundler (>= 0.a) with executable bundle (Gem::GemNotFoundException)が起きた時に試したこと

can't find gem bundler (>= 0.a) with executable bundle (Gem::GemNotFoundException)が起きた時に試したこと

rspec勉強のためにgit cloneしてテストコードを書いていこうと思った矢先に上記のエラー文を吐き出しbundle updateができなくて、ちょっと詰まったので同じところで躓かないようにするためにメモしておく。

原因

Gemfile.lockで記述されているbundleのバージョンと実際にインストールされているバージョンが違ったため上記のエラーが発生していた。

問題解決のために行ったこと

1.Gemfile.lockのbundleのバージョンを確認
 ⇨cd [railsのプロジェクト] で対象のrailsプロジェクトに移動する
 ⇨vi Gemfile.lockとターミナルに記述する

BUNDLED WITH
   1.14.6

⇨上記のような記述があるためこのバージョンにあったbundleをインストールしなければならない

2.既存のbundleをアンインストールして新規にインストール

gem uninstall bundler -v 2.0.1
gem install bundler -v 1.14.6

⇨その後bundle installしたら無事動いた。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

prefixを正確に調べた

prefixの認識レベル

ミニブログを作っている最中、ふとprefixについて考えた時に
「パスを代入する変数」というレベルでしか認識していなかったので
詳しく理解するために調べてみた。

【詳しく調べたかったポイント】

①prefixを使用するメリット(必要性。パス記入の方が分かりやすくない?)
②普通にパスを記入する事との使い分け

調べてわかった事

まず第一に、prefixはRailsが推奨している記述方法らしい。

   tweet_path

でパスの指定完成。

①prefixを使用するメリット(必要性。パス記入の方が分かりやすくない?)について

→結論あまりないらしい。
 1,Railsが推奨している
 2,シンプルで直感的に分かりやすくなる(?)

というのが使用している理由らしい。

②普通にパスを記入する事との使い分け

→開発現場による。「こんな時はprefix!!」みたいなのはないっぽい。

結論

僕は普通にパスを書く方が好き。

「私はprefix派!」「prefixにはこんなメリットがあるぞ!」
というご意見があれば教えて下さい...!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Sass::SyntaxError in Devise::Registrations#new 解決方法 

前提条件

・gemfileに使いたいパッケージを記述している

Gemfile
 gem 'devise'

・ターミナルでbundle installが済んでいる

エラー内容

Screenshot from Gyazo

どうやら@import "modules/user"でうまく'_user.scss'が読み込めてないみたいです

_user.scssを動かしてみます

Screenshot from Gyazo

ここから

Screenshot from Gyazo

modulesディレクトリの中へ入れてみました

Screenshot from Gyazo

無事に動くようになりました

ここで考えたこと

@import "modules/user"と書いてあったけど、
これをよく読むと、
modulesディレクトリの中にある、_user.scssを読み込んでね(@import
ってことだから、

試しに
@import "user"に変えて
_user.scssのファイルの位置を元に戻したら、
ちゃんと動きましたとさ

めでたしめでたし

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsからデータベースを参照(集計)するときの書き方

前置き?

ビューを作成する前に、railsコントローラにおいて、データーベースの値を取ってくる場合にどのように記述するのか。今回はgroup by してからのカウントした値を取得する場合のRailsの書き方を調べるのに随分苦労したので、残しておく。

前提条件

JavaScriptで作成したタイピングゲームをRuby on Railsにのせようとしている。
集計が必要か?ってなったのは、タイピングした結果をランキング表示させようと思い立った時に、問題別のランキング表示をする前に、問題別にどれだけチャレンジャーがいたのか、っていうのを先に一覧表示した後で、ランキング表示をしようか、と思ったから。

テーブル

Qfiles
id
title
category
results_count
その他
Results
id
user_id
qfile_id
その他

出そうとした値

  1. 「Qfile辺りのResultを残したuser_id数」
  2. 「Qfile辺りのResultを残した数」

まぁ、単純だしすぐに終わると思いつつ、先にクエリを作成。

Select
    results.`qfile_id`,
    qfiles.`title`,
    count(distinct results.`user_id`),
    count(*)
from
    results
    inner join
        qfiles
    on  results.`qfile_id` = qfiles.`id`
group by
    results.`qfile_id`,
    qfiles.`title`
;

パンケーキ(Sequel Pro)で実行させて望み通りの値が表示されてることを確認。さあRailsに直すぞ、と意気込む。

調べる

ポイントは3点。
1. テーブルの結合はどう書くんだ?
2. group by はどう書くんだ?
3. countはどう書くんだ?(ついでにdistinctも)

とにかくググって調べたコードをrails cで片っ端から実行。うまく実行できず、よく分からない時は「こう書けば動くんじゃね?」という勘に頼ったテストもした!笑

とりあえずページ保存

Rails controllerに書く記述をRails cでテスト

  • テーブル結合はjoinsで行う。この時selectはいらないが、書かなかった場合、主にしたモデルのカラム(Qfiles.*)しか参照できない。
Qfile.joins(:results).select("results.*, qfiles.*").first.id

ここでもう一つ、先頭に記述するモデルは、親子関係の親の方のモデルでないといけない。これは先頭に記述するモデルを実際に変えてみて、サーバーログに表示されるクエリを見てたらわかる。

  • 単純カウントは、最後に.count入れるだけ
  • distinctカウントは、selectした後に.countを入れて、さらにcountオプション内でdistinctを記述する
Qfile.joins(:results).group("results.qfile_id").select("results.qfile_id").count("distinct results.user_id")

どうやって一文で実行するのか?

色々調べてテストして分かったのは、Countを2ついっぺんには無理じゃね!?ってこと。ひょっとしたら何か方法があるのかもしれないが、もうギブアップした。

カウントの片一方だけなら合わせることができた。

Qfile.joins(:results).group("results.qfile_id").group("qfiles.category").select("results.qfile_id").order("qfiles.id ASC").count("distinct results.user_id")

もう一個のカウントも一緒にやりたいんだよ・・・。

もう面倒い。生クエリ実行できるんじゃね?

find_by_sql で Rails から生 SQL クエリを直接実行する

ここの通りに実行できた。でもあれ?rails cで実行したらカウント要素が入ってなくね?どうゆうこと???

Railsのfind_by_sqlで取得できるモデルからは、select句で指定した名前で値が取れる

取得してるのに表示されないんか〜〜〜い!

これが分かればなんとか値を参照できるから、あとはビューに渡すだけ!!!

でもやっぱり生クエリは邪道な気がする

一生懸命探しました。ふとしたきっかけで新しいキーワードを用いてググったところ、ありました。

Railsで関連レコード数の集計(カウンターキャッシュ)

ふむ。「Qfile辺りのResultを残した数」はこれで保存しときゃいいじゃん。
gem 'counter_culture'を採用。これで作成したカラムが、Qfiles.results_count

ここまでで作成した実行文

Qfile.joins(:results).group("results.qfile_id").group("qfiles.category").group("results_count").select("results.qfile_id").order("qfiles.id ASC").count("distinct results.user_id")

まぁ、なんとか値が出たから使えるんだけども。なんか参照しづらいハッシュ値で取得してしまう。group by の副作用っぽい。

=> {[1, "英語-単語", 4]=>1, [2, "英語-単語", 1]=>1, [4, "英語-単語", 1]=>1, [7, "英語-文章", 4]=>1}

group by 使わなかった時みたいに、@モデル.カラムで参照させて欲しい!!

Rails ActiveRecordでgroup_by countによる集計結果をrelationとして取得する

なるほど、selectにまとめて入力できるのか!!!最終的に以下になった。

Qfile.joins(:results).select("qfiles.id, qfiles.title, qfiles.results_count, COUNT(distinct results.user_id) AS count_distinct_results_user_id").group("results.qfile_id, qfiles.category, results_count").order("qfiles.id ASC")
=> [#<Qfile:0x00007f88586419b8 id: 1, title: "abide - certification 200語", results_count: 4>,
 #<Qfile:0x00007f8858641878 id: 2, title: "certify - drill 200語", results_count: 1>,
 #<Qfile:0x00007f8858641738 id: 4, title: "induction - painter 200語", results_count: 1>,
 #<Qfile:0x00007f88586415d0 id: 7, title: "英語例文 200件 1", results_count: 4>]

上記にdistinct countが含まれてないが、select文中に指定した名前で参照できる。
Qfile.joins・・・・.count_distinct_results_user_id

selectの記述が長くなってしまうのでなんかイヤな書き方ではあるけども、参照時にインデックス番号で指定しなければなくなるよりはマシか?と思った。

外部結合のメモ

Qfile.left_outer_joins(:results).where(user_id: 1).select("qfiles.id, qfiles.title, qfiles.results_count, COUNT(distinct results.user_id) AS count_distinct_results_user_id").group("qfiles.id, qfiles.category, results_count").order("qfiles.id ASC")
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

褒められて伸びる自分のためにポジティブなツイートしか表示されないエゴサーチツールを作った話

はじめに

世の中にアウトプットを出したら他人の反応は気になるもの。長文ブログを書いたときだったり、頑張って開発したサービスをリリース出したときだったり。みな、ツイッターでエゴサーチをしていますよね。

他人からの反応はポジティブなものからネガティブなものまで様々あるかと思いますが、人間誰しも褒められたらやっぱり嬉しいものです。逆に、ネガティブなツイートも貴重な意見と分かっているものの、見るのは勇気がいりますよね。

その気持ち、とっても分かります。

ということで、ポジティブなツイートしか表示されないエゴサツールを作ってみました?

なお、ドMの方はポジティブをネガティブに置き換えれば「ネガティブなツイートしか表示されない」ことも可能なので、安心してこのままお読みください。

まずは動かしてみよう

Webアプリケーションとして作ったのですが本記事では簡単のためrubyプログラムのみご紹介します。試しに私の大好きな「サウナ」で検索した結果がこちらです。

サウナを検索した例
$ ruby twi_posi_search.rb "サウナ"
ポジティブ度数:43.2%
バイトした後、銭湯行ってサウナして牛乳飲んでパンク聴いて帰る!最高!

ポジティブ度数:57.3%
気持ちかったぁ〜ガラガラだったし、やっぱ北欧サウナ最高‥眠い

サウナ万歳 ?

刻々とツイートがなされているため通常の検索結果との違いがお伝えするのが難しいのですが、ポジティブなツイートのみが表示されかつそのツイートのポジティブ度数も可視化しております。

動作環境、利用したアセット

  • 言語:ruby
  • Twitter API
  • COTOHA API(感情分析API)

COTOHA APIについて

テキストの感情を分析するためにCOTOHA APIの感情分析を利用しています。そもそもCOTOHA APIとは、

NTTコミュニケーションズが開発した日本最大級の日本語辞書を活用した自然言語処理、音声認識APIプラットフォーム

です。感情分析を含む、14の自然言語処理、音声処理をAPIで提供してくれています。ありがたや。APIを使うために必要な準備はスタートガイド(COTOHA公式)をご覧ください。

また、COTOHA APIをrubyで簡単に利用するためのgemを@tanaken0515さんが作ってくれていたので、今回はそちらを利用しています。詳細はコチラの記事をご覧あれ?

感情分析APIについて

感情分析APIは、入力として日本語で記述されたテキストを受け取り、そのテキストの書き手の感情(ネガティブ・ポジティブ)を判定します。返却結果には以下が含まれます。

  1. 感情ラベル:ポジティブ/ネガティブ/ニュートラル
  2. 感情スコア:1の信頼度(0.0〜1.0)
  3. 感情フレーズ:1の判定結果の元になったフレーズとその情報

サンプルとして、「人生の春を謳歌しています」という文章の返却結果はこちらです。

{
  "result":
    {
    "sentiment":"Positive",
    "score":0.20766788536018937,
    "emotional_phrase":[
      {
        "form":"謳歌",
        "emotion":"喜ぶ,安心"
      }
    ]
  },
  "status":0,
  "message":"OK"
}

詳細は公式リファレンスをご覧ください。

実装の流れ

  • Twitter APIを使ってキーワード検索の結果を取得
    • 簡単のため、今回は最新10件を取得
  • 取得したツイートを1件づつ感情分析 by COTOHA API
  • ポジティブなツイートのみを表示
    • ポジティブ度数も併せて

サンプルコード

技術的に新しいことはしていないため紹介のみ

twi_posi_search.rb
require 'twitter'
require 'cotoha'

keyword= ARGV[0]

twitter_client = Twitter::REST::Client.new do |config|
  config.consumer_key         = "ホゲホゲ"
  config.consumer_secret      = "ホゲホゲ"
end

client_id = 'ホゲホゲ'
client_secret = 'ホゲホゲ'
cotoha_client = Cotoha::Client.new(client_id: client_id, client_secret: client_secret)
cotoha_client.create_access_token

since_id = nil
tweets = twitter_client.search(keyword, count: 10, result_type: "recent", exclude: "retweets", since_id: since_id)

tweets.take(10).each do |tw|
  result = cotoha_client.sentiment(sentence: tw.full_text)
  if result['result']['sentiment'] == "Positive"
    score = result['result']['score'].round(3)*100
    puts "ポジティブ度数:#{score}%"
    puts tw.full_text
    puts ""
  end
end

最後に

エゴサは手軽にお客様の反応を調べることができる便利な手段です。今回はポジティブなツイートのみを表示しましたが、ネガティブなツイートのみを表示するエゴササービスも面白いそうです。「あなたに対する辛辣な意見こそ、実は貴重なアドバイス」と言いますしね。はたまた、エゴサした結果を感情分析するサービスという方向性もありかなぁと思っています。

エゴサーチと感情分析

この組み合わせで良いアイデアあればぜひコメントください!ちょっとでも面白いなと思ったらイイねをしてくれるともっと喜びます。

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者】長めのモデル名(スネークケース)からfindとかをする

コンソールでモデルから値を取得しようとしたとき

知らずにスネークケースでモデルを指定してエラーになっていたので調べました。
おそらく初心者や始めたばかりの頃はUserモデルとかPostモデルなどしか扱わないので悩まないと思います。。
そもそもスネークケース、キャメルケースとは?という方へ。

UserとかPostとかの短いモデル名のとき

User.find(1)

簡単に取得できますね。

長めのモデル名のとき

キャメルケースとスネークケースとは

CustomerOrder

キャメルケース(大文字部分がラクダの背中っぽい)
CとOが大文字

customer_order

スネークケース(ヘビっぽい)
単語と単語のつなぎ目に_が使われている

モデルから値を取得

CustomerOrder.find(1)

で取れます。(キャメルケース)

Customer_order.find(1)

スネークケースでは不可。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsのViewからVue.jsのModalを表示させてみた

画像のように、一覧画面で詳細を開くボタンで詳細画面を開くときに、Railsのviewは使用せずVue.jsのModalで表示することを試みた。
何とか実装出来たが、JSはjQueryから入門したこともあり結構大変だったのでメモとして残しておく。
スクリーンショット 2020-03-10 19.41.40.png

環境

Ruby 2.6.5
Ruby on Rails 6.0.0
Vue.js 2.6.11

ここではRailsやVueの設定は省略し、Railsのwebpackerでvueのインストール、セットアップが完了していることを前提として書いていく。

やりたい事

冒頭の通り、詳細を開くボタンをクリックしたときに詳細画面をVue.jsのModalで開きたい。
しかし、今までRailsの中でVueを使った事はあっても、Vueインスタンスがどのタイミングで生成されるかなど無知であったため、最初はどうやるのかイメージがつかなかった。
jQueryからJavascriptに入門したので、なかなか生のJSを書く機会がなかった。

Rails側

Modalを開く部分はBootstrapを使うことにした。
参照: https://getbootstrap.com/docs/4.0/components/modal/

上記のURLを参考にして、BootstrapだけでModalを開けるようにしていく。

view/users/index.html.slim

thead
  tr
    th id
    th 名前
    th email
    th tell
    th
tbody
  - @users.each do |user|
    tr
      th = user.id
      td = user.name
      td = user.email
      td = user.tell
      td 
        / Vue側にuser_idを渡すために、data属性にuser_idを持たせている
        button.btn.btn-info.btn-modal data-target="#full-width-modal" data-toggle="modal" data-user_id="#{user.id}" 詳細を開く

/ Modalのコンポーネント部分。ここにVueのtemplateをマウントさせる。
#full-width-modal.modal.fade tabindex="-1" role="dialog" aria-hidden="true" aria-labelledby="full-width-modalLabel" data-keyboard="false" data-backdrop="static"
  #showUser
    = javascript_pack_tag 'users'

Vue.js側

次にvueインスタンスを生成させていく。
javascript/packs配下にusers.jsを作成し、コードを記述

javascript/packs/users.js

import Vue from 'vue'
// packs配下にcomponentsディレクトリを作成し、その中にcomponentを格納している
import ShowUser from './components/ShowUser.vue'

// ボタン要素をgetElementsByClassNameで配列として取得
const btnModals = document.getElementsByClassName('btn-modal')

// for文で各ボタン要素を取り出し
for(let btnModal of btnModals) {
  // 取り出したボタン要素に対し、addEventListenerでボタンクリックされた時にvueインスタンスを生成させる
  btnModal.addEventListener('click', () => {
    new Vue({
      el: '#showUser',
      render: h => h(
        ShowUser, {
          // componentにpropsでuser_idを渡している
          props: {
            userId: Number(btnModal.getAttribute('data-user_id'))
          } 
        }
      )
    })
  })
}

ここまで出来たら、あとはShowUser.vueを書いていけば終わり。
Modalを閉じるときはどうすればいいの?となりそうだが、単純に閉じるボタンのアクションにthis.$destroy()を仕込んであげれば大丈夫。
modalを閉じる処理をbootstrapのjQueryではなく、Vue側で書いてあげないといけないので、そこも含めて次回の記事で頑張って書いていく。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails 〜コメント投稿の削除・編集機能について〜

 概要

Railsで作成したコメントの削除・編集機能についてまとめる。

 投稿の編集

投稿の編集については
① 編集したい投稿の取得
② その投稿のcontentの値を上書き
③ データベースに保存
この3つの処理が必要である。
post=Post.find_by(id:1)
post.content="コメント"
post.save

これらの処理が必要

ビュー

HTMLに記載するコード
<%=link_to("/posts/#{@post.id}/edit")>

link_to + editアクションのURLを指定

ルーティング

get "posts/:id/edit"=>"posts#edit"

編集したい投稿のidをURLに含む

updateアクションについて
フォームの値を受けとるのでルーティングはgetではなくpostにする必要あり。また特定のidの投稿を更新するためURLにidを含む。投稿編集から投稿一覧ページにリダイレクトさせるのでビューは不要。

編集したものを上げる処理のルーティング

post "posts/:id/update"=>"posts#update"

コントローラ

def update
redirect_to("/posts/index")
end

入力したコメントの送信先を指定

フォームで入力した内容をデータベースに保存するためには、フォームのデータをupdateアクションに送信する必要あり。htmlには
<%=form_tag("/posts/#{@post.id}/update")do%>
~html~
<%end%>

とここまで記載。form_tagメソッドを用いて送信先を指定する。

updateアクションの中身

投稿の内容を更新する手順
① URLに含まれたidを用いてデータベースから投稿データを取得
② フォームの編集内容を受け取り、投稿データを更新する
この2つの処理が必要
ルーティングにはidを含めたURL先を記載しつなぐ 
コントローラには

def update
@post=Post.find_by(id:params[:id])
@post.content=params[:content]
@post.save

と記載。何かを見つけるときはfind_byメソッドを使用。

 投稿の削除

投稿の削除については
① データベースから削除したい投稿を取得
② destroyメソッドを用いて、投稿を削除する

post=Post.find_by(id:2)
post.destroy

これらの処理が必要

今日はここまで

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

#Ruby で自前クラスのインスタンス同士で同値性チェックを実装するには == ===イコールメソッドを定義すれば良いかね?

  • 強制的に同値の評価を真にしてしまう場合。別のクラスのインスタンスだろうとなんだろうと常に「同値だ!」と言い張るクラスを作ってみる。実用的には特に意味なし。
  • Aのインスタンスが同値評価をしようとすると、常に true を返す。(特に意味はありません)
  • 有意な同値評価を実装してみてください。
class A
  def ==(instance)
    true
  end

  def ===(instance)
    true
  end
end

class B
end



A.new == B.new
# => true

A.new === B.new
# => true

B.new == A.new
# => false

B.new === A.new
# => false

== と === だけ再定義可能みたいだ

Rubyにおける==,===,eql?,equal?の違い - ぬいぐるみライフ?

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/3014

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails, jQ]インクリメンタルサーチ

インクリメンタルサーチ

本記事では前回作成したユーザーの名前検索機能を使用して実装していきます。
以下を使用しています。

  • ruby 2.5.1
  • rails 5.2.4.1
  • gem 'jquery-rails'
  • gem 'devise'

なお、使用するビューは以下を使用します。

index.html.erb
<%= form_with(url: users_searches_path, local: true, method: :get, class: "search_form") do |f| %>
  <%= f.text_field :keyword, placeholder: "Name", class: "search_input" %>
  <%= f.submit "Search", class: "search_btn" %>

<div class="contents">
  <% @users.each do |user| %>
    <div class="user_content">
      <p class="user_name">
        user.name
      </p>
    </div>
  <% end %>
</div>

準備

以下が記入されていなければ記入します。
app/assets/javascripts/application.js
//= require jquery

フォーマット毎に処理を分ける

フォーマット毎に処理を分けるためコントローラーのindexアクションを編集します。

controllers/users/searches_controller.rb
  def index
    @users = User.search(params[:keyword])
    respond_to do |format|
      format.html
      format.json
    end
  end

respond_to

アクションの中でHTMLとJSONなどのフォーマット毎にhtmlかjsonかを条件分岐することができます。

jbuilderファイルの作成、編集

index.json.jbuilderを新規作成し内容を編集します。

app/views/tweets/searches/index.json.jbuilder
json.array! @users do |user|
  json.id user.id
  json.name name.name
end

jbuilderという拡張子を持つテンプレートでは、JSONという名前のJbuilderオブジェクトが自動的に利用できるようになります。
arrayメソッドはその内の一つでJavaScript側に配列で値を送ることができます。

search.jsの作成、編集

検索フォームの値を取得

app/assets/javascripts/search.js
$(function() {
  $(".search_input").on("keyup", function() {
    var input = $(".search_input").val();
  });
});

keyupイベントを使用して文字が入力される度に発火するようにします。

JSON形式で値を返す

app/assets/javascripts/search.js
$(function() {
  $(".search_input").on("keyup", function() {
    var input = $(".search_input").val();
//---以下を追記---
    $.ajax({
      type: 'GET',
      url: '/users/searches',
      data: { keyword: input },
      dataType: 'json'
    })
//---以上を追記---
  });
});

Ajax通信を実現するためには、上記のように$.ajaxメソッドを使用します。
また。上記のコードは
HTTPメソッドはGETで、/users/searchのURLに{ keyword: input }を送信。サーバーから値を返す際は、JSON。
という意味を持ちます。JSON形式の場合は、app/views/users/searches/index.json.jbuilderが読まれ,該当する投稿情報はjbuilderによってJSONに変換されてJavaScriptのファイルに返されます。

レスポンス結果によって処理を分ける

app/assets/javascripts/search.js
$(function() {
  $(".search_input").on("keyup", function() {
    var input = $(".search_input").val();
    $.ajax({
      type: 'GET',
      url: '/users/searches',
      data: { keyword: input },
      dataType: 'json'
    })
//---以下を追記---
    .done(function(users) {
      $(".contents").empty();
      if (users.length !== 0) {
        users.forEach(function(user){
          appendUser(user);
        });
      } else {
        appendErrMsgToHTML("一致するユーザーはいません");
      }
    })
    .fail(function() {
      alert('error');
    });
//---以上を追記---
  });
});

レスポンスが成功した場合は、ユーザーが表示される親要素の中身を都度空っぽにします。そしてusersが空ではない場合usersの中身の数だけappendUser関数を呼び出します。
該当ユーザーがいない場合は”一致するツイートがありません”という引数を与え、appendErrMsgToHTML関数を呼び出します。
また、レスポンスに失敗した場合はアラートを表示させます。

empty()メソッド

指定したDOM要素の子要素のみを削除するメソッドです。
指定したDOM要素自体を削除するremoveメソッドとは異なります。

forEachメソッド

forEachは、与えられた関数を配列に含まれる各要素に対して一度ずつ呼び出します。

検索に該当ユーザーいた場合、いない場合の関数を定義

app/assets/javascripts/search.js
$(function() {
//---以下を追記---
  var search_list = $(".contents");

  function appendUser(user) {
    var html = `
               <div class="user_content">
                 <p class="user_name">
                   #{user.name}
                 </p>
               </div>
               `
    search_list.append(html);
   }

  function appendErrMsgToHTML(msg) {
    var html = `
          <div class="user_content">
                  <p class="user_name">
                    ${ msg }
                  </p>
                 </div>
         `
    search_list.append(html);
  }
//---以上を追記---
  $(".search_input").on("keyup", function() {
    var input = $(".search_input").val();
    $.ajax({
      type: 'GET',
      url: '/users/search',
      data: { keyword: input },
      dataType: 'json'
    })
    .done(function(users) {
      search_list.empty();
      if (users.length !== 0) {
        users.forEach(function(user){
          appendUser(user);
        });
      } else {
        appendErrMsgToHTML("一致するユーザーはいません");
      }
    })
    .fail(function() {
      alert('error');
    });
  });
});

検索に該当ユーザーがいた場合
変数htmlにユーザー情報を表示する要素を代入し、appendメソッドで親要素の一番下に追加します。
検索に該当ユーザがいない場合
変数htmlに"一致するユーザーはいません"を表示する要素を代入し、appendメソッドで親要素の一番下に追加します。

おわり

これでインクルメンタルサーチが実装できました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyの通信処理におけるopensslのエラー

■ 背景

Slack通知が来なくなったので気づいたのですが、Ruby使って実行しているBatch処理における通信部分がコケていたのでその原因調査したメモです。

■ 目次

  1. 環境
  2. 事象
  3. やったこと
  4. まとめ
  5. 参考サイト

■ 内容

1. 環境

$ rbenv -v
rbenv 1.1.2-26-gc6324ff
$ rbenv versions
  system
* 2.5.1 (set by /Users/y-agatsuma/.rbenv/version)
$ bundler -v
Bundler version 1.16.2
$ ruby -v
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin18]
$ bundle exec gem list | grep slack
slack-incoming-webhooks (0.3.0)

2. 事象

SSL_connect returned=1 errno=0 state=error: tlsv1 alert protocol version
Traceback (most recent call last):
    23: from app/controllers/scout_actions/execute_bot_periodically.rb:84:in `<main>'
    22: from app/controllers/scout_actions/execute_bot_periodically.rb:41:in `start'
    21: from /Users/y-agatsuma/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/parallel-1.12.1/lib/parallel.rb:264:in `y-agatsuma'
    20: from /Users/y-agatsuma/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/parallel-1.12.1/lib/parallel.rb:358:in `work_in_processes'
    19: from /Users/y-agatsuma/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/parallel-1.12.1/lib/parallel.rb:418:in `create_workers'
    18: from /Users/y-agatsuma/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/parallel-1.12.1/lib/parallel.rb:418:in `each_with_index'
    17: from /Users/y-agatsuma/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/parallel-1.12.1/lib/parallel.rb:418:in `each'
    16: from /Users/y-agatsuma/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/parallel-1.12.1/lib/parallel.rb:419:in `block in create_workers'
    15: from /Users/y-agatsuma/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/parallel-1.12.1/lib/parallel.rb:428:in `worker'
    14: from /Users/y-agatsuma/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/parallel-1.12.1/lib/parallel.rb:428:in `fork'
    13: from /Users/y-agatsuma/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/parallel-1.12.1/lib/parallel.rb:437:in `block in worker'
    12: from /Users/y-agatsuma/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/parallel-1.12.1/lib/parallel.rb:455:in `process_incoming_jobs'
    11: from /Users/y-agatsuma/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/parallel-1.12.1/lib/parallel.rb:484:in `call_with_index'

  # <----- 何かしら処理 ----->

     6: from /Users/y-agatsuma/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/slack-incoming-webhooks-0.2.0/lib/slack/incoming/webhooks/request.rb:11:in `post'
     5: from /Users/y-agatsuma/.rbenv/versions/2.5.1/lib/ruby/2.5.0/net/http.rb:1455:in `request'
     4: from /Users/y-agatsuma/.rbenv/versions/2.5.1/lib/ruby/2.5.0/net/http.rb:909:in `start'
     3: from /Users/y-agatsuma/.rbenv/versions/2.5.1/lib/ruby/2.5.0/net/http.rb:920:in `do_start'
     2: from /Users/y-agatsuma/.rbenv/versions/2.5.1/lib/ruby/2.5.0/net/http.rb:981:in `connect'
     1: from /Users/y-agatsuma/.rbenv/versions/2.5.1/lib/ruby/2.5.0/net/protocol.rb:44:in `ssl_socket_connect'
/Users/y-agatsuma/.rbenv/versions/2.5.1/lib/ruby/2.5.0/net/protocol.rb:44:in `connect_nonblock': SSL_connect returned=1 errno=0 state=error: tlsv1 alert protocol version (OpenSSL::SSL::SSLError)

上記は、並列で何かしらの処理を実行した最後に、slack-incoming-webhooksというgemを使って
Slackに通知を送信する部分の処理で発生してる模様。

他にも調べてみると通信全般でエラーが発生していました。

3. やったこと

■ rbenv/ruby-buildを最新化したら治るんじゃね? → ダメ

$ brew upgrade ruby-build
$ 処理実行 → ダメ

■ rubyをバージョンアップしたらいけるんじゃね?

1. ruby2.7.0にして、古いbunlderのままで実行してみる → ダメ
$ rbenv install 2.7.0
$ rbenv global 2.7.0
$ rbenv rehash
$ ruby -v
ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin19]
$ rbenv exec gem install bundler -v 1.16.2
$ 処理実行 → ダメ
2. ruby2.7.0にして、bundlerをdefalut(=2.1.2)に変更して実行してみる → イケた
$ rbenv exec gem uninstall bundler
→ 古いやつ(1.16.2)をアンインストール
$ rbenv exec gem list | grep bundler
bundler (default: 2.1.2)
# Gemfile.lockを削除してから、再度 $ bundle installしてあげて
$ 処理実行 → イケた

4. まとめ

  • Rubyのバージョンというよりも、bundlerに付随するエラーだった模様
  • bundlerをアップデートして、Gemfile/Gemfile.lockを更新したら直ったった

5. 参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Cancancanを使っているときのtest

Cancancanとは?

この記事を見て下さっているということは既にご存知だと思いますが、 権限管理のgem です。

Cancancanを使っているときのテストはどうしたらいいのか?

権限があるときは、権限を持つテストユーザーでログインして

test "test#indexにmanager権限を持つユーザーがアクセスして、正常レスポンスが返されること" do
  # manager権限を持つユーザーでログイン
  login(:yama_p)

  get tests_path
  assert_response :success
end

正常に処理ができることを確認すればまぁいいかも知れませんが、 アクセスできない(権限がない)ことを確認する にはどうしたらよいでしょうか?
というのも、上記で assert_response :errors などとしてもcancancanで弾かれたときにエラーが返ってくるわけではなくそもそもアクセスできないので

CanCan::AccessDenied: You are not authorized to access this page.

とテストが落ちてしまいます。

権限を持つかどうかをテストする

Cancancanのwikiにテストについての記述があります。

test "test#indexにmember権限を持つユーザーはアクセス権限を持たないこと" do
  # member権限を持つユーザー
  user = users('fan_bingbing')
  ability = Ability.new(user)
  assert ability.cannot?(:manage, Test.new)
end

上記のような感じで、ユーザーが権限を持たないことをテストすればOK!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

シンボルとは

シンボルとは

Symbol とは、Ruby が内部でメソッド名などの識別に使っている数値で、任意の文字列に対して異なった値が割り当てらる。

なるほど、よく分かりませんね。

つまり、文字列だけど数値。みたいなものです。ハッシュのキーや文字列自体がデータでは無い物に使うことが吉です。

前コロンと後コロンの違い

シンボルとは主に文字列にコロン記号「:」を前置して定義したものです。

それにより、文字列を””で囲む必要がなくなります。

コロン記号「:」が、文字列記号「””」の代わりに、「これはシンボルだよ」とRubyに知らせています。

上記のように、コロン記号「:」を文字列に前置するとシンボルになります。

たとえば、ハッシュのキーとしてシンボルを使う際や、キーワード引数を使う際に、コロン記号「:」を後置します。

シンボルはオブジェクトの一つ

メソッドなどの名前を識別するためのラベルをオブジェクト化

samurai   /*文字列
:samurai /*シンボル

ハッシュのキーとして利用する

よく使われるのが、ハッシュのキーだと思います。

hash_symbol = { tsuma: "sazae", otto: "masuo", kodomo: "tarao" } 

取り出しは

puts hash_string[:tsuma]  # "sazae" と表示
puts hash_string[:otto]   # "masuo" と表示
puts hash_string[:kodomo] # "tarao" と表示

なり、キーをシンボルで定義しています。

ハッシュはデータを持たない

最初に記述した通り、ハッシュは「文字列だけど数値。」なのでデータを持ちません。
そのため、文字列そのもののデータを必要としない場合にシンボルが使われています。

シンボルのメリット

シンボルを使うことでコードが短くなり可読性が上がったり、処理が早くなったり、メモリ消費が少なくなったりと、いいことばかりらしいので、一緒に勉強しましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

備忘録(TX50)シンボル型の説明

Rubyでは:(コロン)で始まるシンボルと呼ばれるものがありますが、これはどういったものか、またシンボルを使うことのメリットは何か解説してください。

A
Rubyの内部では整数として管理されているが、文字列のように呼び出せるオブジェクト。同じシンボルであれば同一のオブジェクトを参照するので、いくつ作成しても必要なメモリ容量は変わらない。また、文字列よりも高速に処理することができる。

Q
tweet = Tweet.new(tweet_params)
if tweet.save
some_method(tweet)
end

Railsアプリケーションではバリデーションに引っかかると保存がされない仕組みがあります。以下のコードで、保存されるべきtweetがなぜか保存されずロールバックされてしまう場合、どうすればその理由を確認できるか説明してください。

①1行目と2行の間にbinding.pryを記載し止める。
②pryの中で、tweet.saveを実行する。
③tweet.errorsを実行すると保存の際に出たエラーの内容が表示される。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Sinatraを使ってHeroku drainログをTDへ送るAppを作った時にハマった記録

はじめに

Heroku drainからhttp経由でログを取得し、TDへ送るアプリをsinatraで作っていた時ハマった時のことを記録として残す。
Heroku drainからログをpostで受け取る形式のアプリを作った。(Sinatraは初めて)

最初はクラシック形式で作成。

app_main.rb
require 'sinatra'
require 'sinatra/reloader' if development?
require 'td'
require 'date'
TreasureData::Logger.open('DB名',
    :apikey=>ENV['TD_API_KEY'],
    :auto_create_table=>true)
post '/' do
・・・ requestの内容をパースする処理 ・・・
 TD.event.post('テーブル名',{送るデータ})
end

ここで一つ注意点。
・TreasureData::Logger.openはpostのなかに記述してはいけない
理由は簡単。 postされるたびTreasureData::Logger.openが実行されるため
最終的にはThread errorでログが受け取れない状態に陥る。

この記述にすると大体5分間隔でbufferに蓄積したデータをTDへpostする。

モジュールに書き直し

後々のメンテナンス性や拡張性、可読性を考えモジュール化させる。

app_main2.rb
require 'sinatra/base'
require 'sinatra/reloader' 
require 'td'
require 'date'
require 'ltsv'
require "concurrent/map"
require './object_ext' #.present?が使いたかったために拡張

class MyApp < Sinatra::Base
    configure :development do
        register Sinatra::Reloader
    end

    before do

     #TD初期設定
     TreasureData::Logger.open('DB名',
        :apikey=>ENV['TD_API_KEY'],
        :auto_create_table=>true)

        class Lib
            include ObjectExtension

            def log_parser(log)
              ・・・ 受け取ったログをパースしてhashで返す ・・・
       end
       end
       paser = Lib.new

  end  
      post '/' do
        ・・・ logを受け取りlog_parser(log)でパースする ・・・
        TD.event.post('テーブル名',{送るデータ})
      end
end

・ここでのミス
 beforeを使ったこと
実際のところ前処理をする必要は今回のアプリには無かったが、
beforeで事前に色々と設定や処理などをするものと勘違いしていたためまたもやThread errorの連発。
何が起きていたかと言うと、herokuからドレインされたログを受ける度にbeforeが呼ばれTreasureData::Logger.openを
呼び出すと処理を行っていた。そのため、今度はbufferに溜まるのを待たずに即TDにpostする現象に見舞われblockされてしまう状況に陥った。

app_main2.rb
require 'sinatra/base'
require 'sinatra/reloader' 
require 'td'
require 'date'
require 'ltsv'
require "concurrent/map"
require './object_ext' #.present?が使いたかったために拡張

class MyApp < Sinatra::Base
    configure :development do
        register Sinatra::Reloader
    end

     #TD初期設定
     TreasureData::Logger.open('DB名',
        :apikey=>ENV['TD_API_KEY'],
        :auto_create_table=>true)

        class Lib
            include ObjectExtension

            def log_parser(log)
              ・・・ 受け取ったログをパースしてhashで返す ・・・
       end
        end
            paser = Lib.new
  
      post '/' do
        ・・・ logを受け取りlog_parser(log)でパースする ・・・
        TD.event.post('テーブル名',{送るデータ})
      end
end
MyApp.run!

結果から言うとbeforeは要らなかった。
これで、約5分間隔でTDへpostする様に安定した。

結論

TreasureData::Logger.openは一回呼び出せばそれで良い代物であって、
何度も呼び出される様な場所に記述をしてはいけない。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DB接続がない状態でassets:precompileを行う

本番とは違う環境だったり、Dockerfile内でassets:precompileを行ったりするときにDB接続でエラーになるときがある。これを回避する方法ってあるのかなと思ったので調べてみた

activerecord-nulldb-adapterを使う

github
https://github.com/nulldb/nulldb

gem 'activerecord-nulldb-adapter'
config/database.yml
default: &default
  adapter: <%= ENV['DB_ADAPTER'] ||= "mysql2" %>

database.ymlに環境変数でDB_ADAPTERを指定する。

$ DB_ADAPTER=nulldb bundle exec rake assets:precompile

上記を実行すればDB接続なしでprecompileできる

参考

Rails × ECS でオートスケーリング&検証環境の自動構築
https://tech.medpeer.co.jp/entry/2018/06/20/080000

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]rails sの時のエラー

rails sを行なった時のエラーの解消方法を書いて行きたいと思います。
私はこれで直りましたが皆が当てはまるかは分かりません。よろしくお願いします!

環境

・macOS Catalina
・Rails 5.2.0
・Ruby 2.5.0

エラー文

terminal
$ rails s
Traceback (most recent call last):
    4: from bin/rails:3:in `<main>'
    3: from bin/rails:3:in `load'
    2: from /Users/name/App/Parttime-job/bin/spring:8:in `<top (required)>'
    1: from /Users/name/.rbenv/versions/2.5.0/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'
/Users/name/.rbenv/versions/2.5.0/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require': cannot load such file -- bundler (LoadError)

試したこと

パソコンに入っているbundlerとRailsプロジェクトのGemfile.lockに書いてあるbundlerのバージョンが違っていたのでとりあえずそこのバージョンを合わせました。

バージョン確認

terminal
$ bundler -v 

bundlerバージョン変更
ご自身のbundlerのバージョンに合わせてバージョンを変更して下さい。

terminal
$ gem install bundler -v 1.3.0
$ gem uninstall bundler -v 2.1.4

そして

terminal
$ bundle update --bundler

試したこと(2)

これは私だけかもしれませんがterminalで確認したRubyのバージョンとRailsのGemfileにあるRubyのバージョンが違っていたのでそこのバージョンを合わせました。

インストールするバージョンを確認

terminal
$ rbenv install -list

バージョンを指定してインストール

terminal
$ rbenv install 2.5.0

バージョンを確認してインストールできたか確認

terminal
$ rbenv versions

バージョンを変更

terminal
$  rbenv local 2.6.3

バージョンを変更したらrehash

terminal
$  rbenv rehash

以上で私はrails sができました。

最後に

今回、以上の方法でエラーが直りましたがもしかしたら正しい方法ではないかもしれませんが何か役にたったら嬉しいです!
読んで頂きありがとうございました!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ubuntuで使う言語のインストール方法とか環境構築とか

最近はバックエンド言語毎にVMで環境用意して勉強したりしてて、その環境構築方法の管理を最近はGistでしてるのですが、何となくQittaに。※※但し、Gistは英語で書いてるので。

environment

  • host OS: Windows
  • VM: Virtual Box with Vagrant
    • Ubuntu 18.0

CUIまたはGUIの仮想環境をUbuntuを使って構築するのはこっち。

Ruby on Rails

Install latest version

terminal
# install in one time
sudo apt install autoconf bison build-essential libssl-dev libreadline-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm-dev

# install rbenv
# rbenv is tool to manage a few of ruby versions and enable to change ruby ver. project by project.
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
source ~/.bashrc
git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build

# Install ruby
rbenv install --list
rbenv install 2.〇.〇
rbenv global 2.〇.〇

# Instal yarn
# Rails6 needs webpacker, and Webpacker needs yarn to install
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt update
sudo apt install yarn

# Install Rails
gem install rails --no-document

# install webpacker
# inner App
rails webpacker:install

Install RubyonRails by "apt install"

terminal
sudo apt install -y ruby ruby-dev build-essential
sudo apt install yarn

sudo gem install rails
  • "-y" means "All Yes"
  • build-essential contain information about package to build Debian pack.
    • If do not build Debian, build~ is not needed
    • Reference

Nodejs

rails6 uses webpacker, which needs nodejs

terminal
# first, install nodejs and npm
sudo apt install -y nodejs npm

# install n-package
sudo npm install n -g

# by n-package, install node
sudo n stable

# uninstal old nodejs and npm, and re-login
sudo apt purge -y nodejs npm
exec $SHELL -l

# confirm
node -v

Rust

when discord changed golang to Rust, I just tried this and coded a little.

terminal
sudo apt install build-essential

# install rust
curl https://sh.rustup.rs -sSf | sh

# add the pass
source $HOME/.cargo/env

Java

terminal
sudo apt update
sudo apt install git
sudo apt install openjdk-11-jdk

# confirmation
java --version

PHP

Python

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby on Rails】Google Books APIを叩く際の5つのTips

想定読者

  • Railsで読書系ポートフォリオを作っている方
$ ruby -v                     
ruby 2.6.5
$ rails -v
Rails 5.2.4.1

その1.APIを叩くロジックはcontrollerから切り分ける

まず以下記事(私の前記事です)のようにAPIを叩くわけですが、これをcontrollerに書いたらあっという間にFat controllerになりました。
Ruby on RailsでGoogle Books APIを叩く

APIを叩くロジックは、以下を参考にmodlueとしてapp/libに置きました。
Rails 5 で自作のモジュールを読み込む方法

APIを叩く自作module

app/lib/google_books_api.rb
module GoogleBooksApi
  def get_json_from_url(url)
    JSON.parse(Net::HTTP.get(URI.parse(Addressable::URI.encode(url))))
  end

  # ①検索するAPIを叩く
  def url_from_keyword(keyword)
    "https://www.googleapis.com/books/v1/volumes?q=#{keyword}&country=JP&maxResults=20"
  end

  # ②IDから本の情報を取得するAPIを叩く
  def url_from_id(googlebooksapi_id)
    "https://www.googleapis.com/books/v1/volumes/#{googlebooksapi_id}"
  end
end

controllerにincludeする

app/controllers/books_controller.rb
class BooksController < ApplicationController
  include GoogleBooksApi
 ()
end

books_controller内で、GoogleBooksAPIというmoduleをincludeしたので、そのmoduleの関数が使えます。

app/libというディレクトリが自作なので、
「そもそもgoogle_books_api.rbを読み込んでくれるの?」
と不安に思うかもしれません。

しかし、実はRailsでは自動的にapp/〇〇(ディレクトリ)/〇〇.rbを読み込んでくれるという仕様があるようです。
※ さらに階層が深くなるとNGみたいです。

その2.APIの構造のうち、itemを理解する

Google Books APIからは下記のように色々な情報が入ってるので、APIに慣れていないと混乱するかもしれません。
https://www.googleapis.com/books/v1/volumes?q=Rails

先に結論を書きます。
https://www.googleapis.com/books/v1/volumes?q=#{keyword}では以下が取り出せます。
{ (他のハッシュ),
"items" => [{ item1 }, { item2 }, { item3 }, ....] }

https://www.googleapis.com/books/v1/volumes/#{googlebooksapi_id}
では以下が取り出せます。
{ item }

①本を検索するAPIも、②IDから本情報を取得するAPIも、結局itemを取り出せるわけです。
(このitemの中に、本1つの情報が入っています)
このitemに対してのロジックを書くだけで、①本を検索するAPIに対しても、②IDから本情報を取得するAPIに対しても、共通のロジックを使うことができます。
よって①本を検索するAPIに対しては、以下のように処理するのが良いと思われます。

(検索するAPIから得たjson文字列)["items"].each do |item|
  (itemに対するロジック)
end

その3.検索時にはActiveRecordのオブジェクトを作らないようにする

これは私がやってしまったアンチパターンです。

最終的には検索した本をActionViewで使う際に、
render @books
あるいは
@books.each do |book|
のような繰り返し処理を書きたいかと思います。

その際に、以下のようにActiveRecordのオブジェクトを大量生成するロジックを作ってしまいました。

ActiveRecordから@booksを作る(アンチパターン)

books_controller
def search
  @books = []
  (items).each do |item|
    (中略)
    @books << Book.new(
      author: (itemから引っ張ってきた著者)
      title: (itemから引っ張ってきたタイトル)
      (その他 )
    )
  end
end

Fat controllerになる以外にも、これの問題点は2つあります。

  1. ActiveRecordのインスタンス生成(Book.new)はコストが高いのに、それを繰り返し処理させている
  2. モデルと同じattirbuteを使わなくてはいけない

1の解説

実際にやってみるとわかります。
検索し始めてから結果が表示されるまで5〜10秒くらいかかってて、UXが悪かったです。

参考(理解はできてません笑):
ActiveRecordのパフォーマンス・チューニング

2の解説

例えば検索結果としては詳細な情報が表示できるようにしたいが、アプリのDBに保存させるつもりは無い、というような場合があります。
パッと思いつくのは、
averageRating: 4.0
amount: 3960.0
みたいな情報でしょうか。レーティングや値段はその時々で変わるので、DBに保存しようとは思わないでしょう。

こういう場合は検索結果表示のときだけbook.averageRatingでレーティングを返せるようにし、DBには保存はしない、という設計が思いつきます。
しかし、ActiveRecordを使うとDBにも同一カラムが存在する必要がある、というわけです。

2つの問題点の原因

実は1,2とも原因は共通していて、要はActiveRecordはO/Rマッパーであるからです。

  • 検索結果はDBに保存するわけではありません(=DBを使いません)
  • ActiveRecordはView <-> DBの仲介役(O/Rマッパー)です。
  • よってActiveRecordを使う必要はありません。(少なくともそういう設計になってません)

ということになります。

その4.APIの情報は、オレオレクラス内に格納する

その3に対して、では具体的にどうするかをお伝えします。
結論から言うと、以下のようなオレオレクラスを作成しました。

オレオレクラス

app/lib/google_book.rb
class GoogleBook
  attr_reader :googlebooksapi_id, :author, :buy_link, :description, :image, :published_at, :title

  class << self
    include GoogleBooksApi

    def new_from_id(googlebooksapi_id)
      url = url_of_creating_from_id(googlebooksapi_id)
      item = get_json_from_url(url)
      new(item)
    end

    def search(keyword)
      url = url_of_searching_from_keyword(keyword)
      json = get_json_from_url(url)
      books = []
      if items = json['items']
        items.each do |item|
          books << GoogleBook.new(item)
        end
      end
      books
    end
  end

  def initialize(item)
    @item = item
    @volume_info = @item['volumeInfo']
    retrieve_attribute
  end

  def retrieve_attribute
    @googlebooksapi_id = @item['id']
    @author = @volume_info['authors'].first
    @buy_link = @item['saleInfo']['buyLink']
    @description = @volume_info['description']
    @image = @volume_info['imageLinks']['smallThumbnail']
    @published_at = @volume_info['publishedDate']
    @title = @volume_info['title']
  end
end

重要なポイントはitemを引数にinitializeができるようにすることです。
その2でも述べたように、itemに対して同じ処理をするように心がければ、共通のロジックを用いることができます。

使用例

これにより、以下のようにオブジェクト志向っぽく扱えるようになります。

book = GoogleBook.new_from_id("axicQgAACAAJ")
book.title
=> "影響力の武器"
book.author
=> "ロバート・B. チャルディーニ"

books = GoogleBook.search("影響力の武器")
=> [ book1, book2, book3, .... ]()
book = books.first
book.id
=> "axicQgAACAAJ"
book.title
=> "影響力の武器"

このクラスはインスタンス生成にかかるコストは大したことはありません。
よってその3のような、ActiveRecordのインスタンスを複数生成時に発生していたコストも解消できています。

注意点

books = GoogleBook.search
のbooksのクラスは、ただのArrayです。そのままではgem kaminariによるpaginateとか、render @booksとかが出来ません。
以下のように続けて書くことで、kaminariのpaginateを利用できます。

@books = Kaminari.paginate_array(books).page(params[:page])

その5.リソース登録時にはモデルにロジックを書く

上記で作ったGoogleBookクラスを使って、いざDBに本を保存するロジックを書こうとすると、これまたFat controllerになりがちです。

app/controllers/books_controller.rb
def create
  google_book = GoogleBook.new_from_id(取ってきたid)
  @book = Book.new(
    author: google_book.author
    title: google_book.title
    (その他 )
  )
  if @book.save
  (以下略)
end

controllerというのは、DBの情報を知りすぎない、というのが良い設計らしいです。上のような書き方は「DBにこれとこれが入るんでしょ」って言ってしまっています。

実装

app/controllers/books_controller.rb
def create
  google_book = GoogleBook.new_from_id(取ってきたid)
  @book = current_user.books.build
  @book = @book.substitute_for_googlebook(google_book)
  if @book.save
  (以下略)
end
app/models/book.rb
def substitute_for_googlebook(google_book)
  self.author = google_book.author
  self.description = google_book.description
  self.googlebooksapi_id = google_book.googlebooksapi_id
  self.published_at = google_book.published_at
  self.title = google_book.title
  self.buy_link = google_book.buy_link
  self.image = google_book.image
  self
end

割とcontrollerはスッキリできたのでは無いでしょうか。

といいながら実はその5はあんまり自信無いです笑
もうちょっと上手くできる気がします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む