- 投稿日:2020-07-26T23:58:22+09:00
Gitに管理されたファイルを削除したい
.gitignoreに書き忘れてcommitしてしまった時に、
「.gitignoreに追記すれば大丈夫...」
と誤解していませんか?
僕のように..汗後から.gitignoreに追記するだけではすでにGitに管理されたファイルは除去されないので、そんな時の対処方法を書いておきます。
Gitに管理されたファイルを残して除外したい場合
$ git rm --cached [除外したいファイル名]その後は必ず
.gitignore
に除外したいファイルを追記する。
注意箇所:
--cached
を必ずつける!!
つけないとファイルごと削除してしまいます。ファイルごと削除したい場合
$ git rm [削除したいファイル名]ディレクトリごと削除したい場合
$ git rm -r [削除したいディレクトリ]最後に
Gitを使い始めた時に何回か忘れてcommitして、都度調べていたので備忘録として残しておきます。
同じ境遇になった方のお役に立てば嬉しいです。
- 投稿日:2020-07-26T22:09:26+09:00
Active Storageで複数画像の投稿・削除
環境
Ruby 2.5.1
Rails 5.2.4.3やりたい事
Rails標準のファイル管理機能Active Storageを使い、画像を複数を投稿・削除できる機能を実装したい。
まずアプリを作成
$ rails new sample_app $ cd sample_app $ bin/rails db:create別ターミナルで
$ bin/rails shttp://localhost:3000 にアクセスして馴染みの画像が出ていればOK。
modelとcontrollerを作成
今回はpostという名前で。
$ bin/rails g model post name:string $ bin/rails db:migrate $ bin/rails g controller postsルーティング
config/routes.rbRails.application.routes.draw do resources :posts endActive Storageをインストール
$ bin/rails active_storage:install Copied migration 20200726095142_create_active_storage_tables.active_storage.rb from active_storage以下の2つのテーブルを作成するマイグレーションファイルが作成される。
・active_storage_attachments
・active_storage_blobs
migrateしてテーブルを作成。$ bin/rails db:migrate == 20200726095142 CreateActiveStorageTables: migrating ======================== -- create_table(:active_storage_blobs) -> 0.0020s -- create_table(:active_storage_attachments) -> 0.0019s == 20200726095142 CreateActiveStorageTables: migrated (0.0041s) ===============モデルと関連付けて使えるようにする
models/posts.rbclass Post < ApplicationRecord has_many_attached :images endcontrollerを記述
controllers/posts_controller.rbclass PostsController < ApplicationController def index @posts = Post.all end def new @post = Post.new end def create @post = Post.new(post_params) if @post.save flash[:success] = "作成しました" redirect_to posts_path else render :new end end def destroy @post = Post.find(params[:id]) @post.destroy flash[:success] = "作成しました" redirect_to posts_path end private def post_params params.require(:post).permit(:name, images: []) end endviewを作成
投稿一覧ページ
posts/index.html.erb<h1>投稿一覧</h1> <%= link_to '新規投稿', new_post_path %> <%= render @posts %>今回はpostのpartialを作って表示させる。
posts/_post.html.erb<div class="post-partial"> <li id="post-<%= post.id %>"> <%= post.name %> <% post.images.each do |image| %> <%= image_tag(image, width:100) %> <% end %> <%= link_to '削除', post, method: :delete, data: { confirm: '削除してよろしいですか?' } %> </li> </div>新規作成ページ
画像を複数投稿する場合はmultiple: trueが必要
posts/new.html.erb<%= form_with(model: @post, local: true) do |f| %> <div> <%= f.label :name, '名前' %> <%= f.text_field :title %> </div> <div> <%= f.label :images, '画像' %> <%= f.file_field :images, multiple: true %> </div> <div> <%= f.submit '投稿する' %> </div> <% end %>ブラウザから http://localhost:3000/posts にアクセス
新規投稿から画像を複数投稿する事ができた。
任意の画像を削除する
controllerにメソッド追記
編集に必要なedit、updateメソッドを追記します。
controllers/posts_controllers.rbdef edit @post = Post.find(params[:id]) end def update post = Post.find(params[:id]) if params[:post][:image_ids] params[:post][:image_ids].each do |image_id| image = post.images.find(image_id) image.purge end end if post.update_attributes(post_params) flash[:success] = "編集しました" redirect_to posts_url else render :edit end end投稿編集ページの作成
新規投稿ページとほぼ一緒だが、登録されている画像を表示しチェックを入れる部分を追記。
posts/edit.html.erb<%= form_with(model: @post, local: true) do |f| %> <div> <%= f.label :name, '名前' %> <%= f.text_field :name %> </div> <div> <%= f.label :images, '画像' %> <%= f.file_field :images, multiple: true %> </div> <% if @post.images.present? %> <p>現在登録されている画像(削除するものはチェックしてください)</p> <% @post.images.each do |image| %> <%= f.check_box :image_ids, {multiple: true}, image.id, false %> <%= image_tag image, size:"100x100" %> <br> <% end %> <% end %> <div> <%= f.submit '編集する' %> </div> <% end %>indexに編集リンクを追記
posts/index.html.erb<div class="post-partial"> <li id="post-<%= post.id %>"> <%= post.name %> <% post.images.each do |image| %> <%= image_tag(image, width:100) %> <% end %> <%= link_to '編集', edit_post_path(post.id) %> #追加 <%= link_to '削除', post, method: :delete, data: { confirm: '削除してよろしいですか?' } %> </li> </div>確認
ブラウザから編集のリンクをクリックし、削除したい画像にだけチェックを入れる。
任意の画像だけ削除できました。
参考
- 投稿日:2020-07-26T20:17:44+09:00
【スクレイピングまとめ】| Python Node.js PHP Ruby Go VBA | 6種類の言語でヤフートップをスクレイピング
Python
動画
リポジトリ
https://github.com/yuzuru-program/scraping-python-yahoo
ソース
index.pyimport urllib.request as request from bs4 import BeautifulSoup req = request.Request( "https://www.yahoo.co.jp", None, {} ) instance = request.urlopen(req) soup = BeautifulSoup(instance, "html.parser") li = soup.select('main article section ul')[0].select('li') for m in li: print(m.text) print(m.select("a")[0].get("href")) print()Node.js
動画
リポジトリ
https://github.com/yuzuru-program/scraping-node-yahoo
ソース
package.json{ "dependencies": { "cheerio": "^1.0.0-rc.3", "node-fetch": "^2.6.0" } }index.jsconst fetch = require('node-fetch'); const cheerio = require('cheerio'); const main = async () => { // https://www.yahoo.co.jp/にリクエスト投げる const _ret = await fetch('https://www.yahoo.co.jp/', { method: 'get', headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36', }, referrer: '', }).catch((err) => { console.log(err); }); if (_ret.status !== 200) { console.log(`error status:${_ret.status}`); return; } // jqueryチックに使えるように変換 const $ = cheerio.load(await _ret.text()); const _li = $('main article section ul').eq(0).find('li'); // ヤフートップニュースを表示 _li.map(function (i) { console.log(_li.eq(i).text()); console.log(_li.eq(i).find('a').attr()['href']); console.log(); }); }; main();PHP
動画
リポジトリ
https://github.com/yuzuru-program/scraping-php-yahoo
ソース
index.php<?php require_once './phpQuery-onefile.php'; function my_curl($url) { $cp = curl_init(); /*オプション:リダイレクトされたらリダイレクト先のページを取得する*/ curl_setopt($cp, CURLOPT_RETURNTRANSFER, 1); /*オプション:URLを指定する*/ curl_setopt($cp, CURLOPT_URL, $url); /*オプション:タイムアウト時間を指定する*/ curl_setopt($cp, CURLOPT_TIMEOUT, 30); /*オプション:ユーザーエージェントを指定する*/ curl_setopt($cp, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36'); $data = curl_exec($cp); curl_close($cp); return $data; } $url = 'https://www.yahoo.co.jp'; $doc = phpQuery::newDocument(my_curl($url)); $ul = $doc->find('main article section')->find("ul:eq(0)"); for ($i = 0; $i < count($ul->find("li")); ++$i) { $li = $ul->find("li:eq($i)"); echo $li[0]->text(); echo "\n"; echo $li[0]->find("a")->attr('href').PHP_EOL; echo "\n"; } ?>phpQuery-onefile.php
https://github.com/yuzuru-program/scraping-php-yahoo/blob/master/phpQuery-onefile.php
Ruby
動画
リポジトリ
https://github.com/yuzuru-program/scraping-ruby-yahoo
ソース
index.rbrequire "nokogiri" require "open-uri" doc = Nokogiri::HTML(open("https://www.yahoo.co.jp")) test = doc.css("main article section ul")[0].css("li") test.each do |li| puts li.content puts li.css("a")[0][:href] puts endGo
動画
リポジトリ
https://github.com/yuzuru-program/scraping-go-yahoo
ソース
index.gopackage main import ( "fmt" "log" "net/http" "github.com/PuerkitoBio/goquery" ) func main() { req, _ := http.NewRequest("GET", "http://yahoo.co.jp", nil) req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36") res, _ := new(http.Client).Do(req) if res.StatusCode != 200 { log.Fatalf("status code error: %d %s\n", res.StatusCode, res.Status) } doc, err := goquery.NewDocumentFromReader(res.Body) if err != nil { log.Println(err) } li := doc.Find("main article section ul").Eq(0).Find("li") li.Each(func(index int, s *goquery.Selection) { fmt.Println(s.Text()) tmp, err := s.Find("a").Attr("href") if err != true { log.Fatal(err) } fmt.Println(tmp + "\n") }) }VBA
動画
ソース
'Microsoft HTML Object Library 'Microsoft Internet Controls ' IEのプロセスを削除する関数 Function IeProcessKill() CreateObject("WScript.Shell").Exec ("taskkill.exe /F /IM iexplore.exe") Application.Wait Now + TimeValue("0:00:2") End Function 'ヤフートップスクレイピング Sub main() Dim ie As InternetExplorer ' IEプロセスを削除' Call IeProcessKill 'IE起動 Set ie = New InternetExplorer 'サイトを非表示 ie.Visible = False Debug.Print "読み込み中..." Debug.Print 'ヤフー ie.Navigate "https://www.yahoo.co.jp/" Do While ie.Busy = True Or ie.readyState < READYSTATE_COMPLETE Loop For Each tmp In ie.document.querySelector("main article section ul").getElementsByTagName("li") Debug.Print Trim(tmp.textContent) Debug.Print tmp.getElementsByTagName("a")(0).href Debug.Print Next tmp ' ブラウザ閉じる ie.Quit Set ie = Nothing End Sub
- 投稿日:2020-07-26T18:42:05+09:00
【ポートフォリオ を作成される方へ】Dockerでbinding.pryを使う方法
ポートフォリオを作成中、dockerとCircleCIを使ってHerokuにデプロイするためにこちらの記事を参考に作っていました。
そこにbinding.pryを使いたいと思った時の導入の方法をお伝えしようと思います。docker-compose.ymlversion: '3' services: db: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: root ports: - "4306:3306" web: build: . command: rails s -p 3000 -b '0.0.0.0' environment: RAILS_ENV: development volumes: - .:/sample_app #自身のアプリディレクトリ名を設定 ports: - "3000:3000" links: - db上記の状態から
①command: rails s -p 3000 -b '0.0.0.0'を削除
②tty: true最終的な形はこちら
docker-compose.ymlversion: '3' services: db: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: root ports: - "4306:3306" web: build: . environment: RAILS_ENV: development volumes: - .:/sample_app ports: - "3000:3000" links: - db tty: true③docker-compose up
④docker-compose exec web bash
⑤rails s -p 3000 -b '0.0.0.0'
⑥好きなとことに binding.pryを差し込む参考
https://qiita.com/gakinchoy7/items/ae31107ef56efb16fe7e
https://stackoverflow.com/questions/35211638/how-to-debug-a-rails-app-in-docker-with-pry
- 投稿日:2020-07-26T18:16:22+09:00
post to Qiita patching version
すでにqiitaにあげた記事を更新するversion
に最初に投稿するversionの作成記録があります.さらに,すでにqiitaにあげた記事を更新するversionです.下のcodeを
ruby post_final.rb post_final.orgなんかでできます.最初のpostでは新しいのを作り,そのあとitemsのidをorgに記します.それがあるとそのidにpatchします.
さらに,'open', 'teams'を選べます.defaultは'private'.
ruby post_final.rb post_final.org teamsなんてね.
keyは
code
1 require "net/https" 2 require "json" 3 4 def get_title_tags(src) 5 $conts = File.read(src) 6 title = $conts.match(/\#\+(TITLE|title|Title): (.+)/)[2] || "テスト" 7 m = [] 8 tags = if m = $conts.match(/\#\+(TAG|tag|Tag|tags|TAGS|Tags): (.+)/) 9 m[2].split(',').inject([]) do |l, c| 10 l << {name: c.strip} #, versions: []} 11 end 12 else 13 [{ name: "hoge"}] #, versions: [] }] 14 end 15 p tags 16 return title,tags 17 end 18 19 src = ARGV[0] || 'README.org' 20 title, tags = get_title_tags(src) 21 p title 22 p tags 23 24 system "emacs #{src} --batch -l ~/.emacs.d/site_lisp/ox-qmd -f org-qmd-export-to-markdown --kill" 25 26 lines = File.readlines(src.gsub('org','md')) 27 28 m = [] 29 patch = false 30 if m = $conts.match(/\#\+qiita_id: (.+)/) 31 qiita_id = m[1] 32 patch = true 33 else 34 qiita_id = '' 35 end 36 37 38 case ARGV[1] 39 when 'teams' 40 qiita = 'https://nishitani.qiita.com/' 41 p access_token = ENV['QIITA_TEAM_WRITE_TOKEN'] 42 private = false 43 when 'open' 44 qiita = 'https://qiita.com/' 45 p access_token = ENV['QIITA_WRITE_TOKEN'] 46 private = false 47 else 48 qiita = 'https://qiita.com/' 49 p access_token = ENV['QIITA_WRITE_TOKEN'] 50 private = true 51 end 52 53 params = { 54 "body": lines.join, #"# テスト", 55 "private": private, 56 "title": title, 57 "tags": tags 58 } 59 60 if patch 61 path = "api/v2/items/#{qiita_id}" 62 else 63 path = 'api/v2/items' 64 end 65 p qiita+path 66 uri = URI.parse(qiita+path) 67 68 http_req = Net::HTTP.new(uri.host, uri.port) 69 http_req.use_ssl = uri.scheme === "https" 70 71 headers = {"Authorization" => "Bearer #{access_token}", 72 "Content-Type" => "application/json"} 73 if patch 74 res = http_req.patch(uri.path, params.to_json, headers) 75 else 76 res = http_req.post(uri.path, params.to_json, headers) 77 end 78 79 p res.message 80 81 res_body = JSON.parse(res.body) 82 res_body.each do |key, cont| 83 if key == 'rendered_body' or key == 'body' 84 puts "%20s brabrabra..." % key 85 next 86 end 87 print "%20s %s\n" % [key, cont] 88 end 89 system "open #{res_body["url"]}" 90 qiita_id =res_body["id"] 91 unless patch 92 File.write(src,"#+qiita_id: #{qiita_id}\n"+$conts) 93 endえっと,refactoringしてください.
課題
openとteamsで二箇所に出したいときは,どっちがどっちかわからなくなりますね.その辺り,自動で更新してくれるようにできんかな...
- 投稿日:2020-07-26T17:17:49+09:00
タスク管理 アプリケーション作成手順
1. アプリケーション作成準備
1-1. 作成するアプリケーションの内容を考える
機能 機能内容 一覧表示 全てのタスクを一覧表示する 詳細表示 一つのタスクの詳細内容を表示する 新規登録 新しいタスクをdbに登録する 編集 登録済みのタスクを編集し、dbを更新する 削除 登録済みのタスクをdbから削除 1-2. アプリケーション名を決め、雛形を作成
rails new アプリケーション名 [オプション] #例) rails new taskmanage -d postgresql1-3. dbを作成する
アプリケーションを作成したディレクトリに移動しdbを作成する
rails db:create1-4. サーバーを起動してみる
以下のコマンドを叩き、
「http://localhost:3000」
にアクセスし、railsのデフォルトページが表示されるか確認rails s2. モデルを作成
モデルの構成要素
モデルに対応するRubyのクラス
キャメルケースモデルに対応するデータベースのテーブル
モデルのクラス名を複数名にしたもの、スネークケース2-1. モデルの属性を設計する
属性 属性名 データ型 nullを許可するか デフォルト値 名称 name string 許可しない なし 詳しい説明 description text 許可する なし 2-2. モデルの雛形を作成
rails g model [モデル名] [属性名:データ型 属性名:データ型 ...] [オプション] #例) rails g model Task name:string description:text2-3. マイグレーションをし、dbにテーブルを追加
作成されたマイグレーションファイルを確認し、rails db:migrateを実行
3. コントローラーとビューの作成
rails g controller コントローラー名 [アクション名 アクション名...] [オプション] #例) rails g controller tasks index show new editRails.application.routes.draw do root to: 'tasks#index' resources :tasks end3. 新規登録機能の実装
3-1. 一覧画面に新規登録ページへ遷移するリンクを追加
= link_to '新規登録', new_task_path3-2. 新規登録画面のためのアクションを実装
tasks_controller.rbdef new @task = Task.new end3-3. 新規登録ページのビューを実装
new.html.slim= form_with model: @task, local: true do |f| .form-group = f.label :name #label = 入力欄の名前の表示とラベル部分クリックで入力欄をフォーカスする = f.text_field :name .form-group = f.label :description = f.text_area :description = f.submit nil3-4. 登録アクションの実装
createアクション = 「登録フォームから送られてきたデータをdbに保存し、一覧画面に遷移」
tasks_controller.rbdef create @task= Task.new(task_params) task.save redirect_to tasks_url, notice:"タスク「#{task.name}」を登録しました。" end private def task_params #Strong parameters = フォームからリクエストパラメータとして送られてきた情報が想定どおり{task: {...}}の形であるかチェックし、必要な情報だけを抜き取るという役割 params.require(:task).permit(:name, :description) endrenderとRedirect_toの違い
render Redirect_to アクションに続けてビューを表示させる アクションを処理した直後にビューを表示せず、別のURLに案内する flashメッセージ
リダイレクト時に、次のリクエストに対してちょっとしたデータを伝える
redirect_to tasks_url, notice:"タスク「#{task.name}」を登録しました。" #タスク登録完了メッセージをしてからリダイレクトするという意味 flash.now[:alert] = "すぐにアラート" #同じリクエスト内の場合は、flash.now4. 一覧表示機能の実装
4-1. 新規登録画面のためのアクションを実装
tasks_controller.rbdef index @tasks = Task.all end4-2. 一覧ページで全てのタスクを表示
@tasks.each do |task| task.name task.created_at5. 詳細表示機能の実装
5-1. 一覧ページから詳細ページへのリンクを追加
index.html.slimlink_to task.name, task_path(task)5-2. 選択されたタスクを詳細表示アクションで取得
tasks_controller.rbdef show @task = Task.find(params[:id]) end5-3. 選択されたタスクを詳細表示アクションで取得
show.html.slim@task.id @task.name @task.created_at @task.updated_at6. 編集機能の実装
6-1. 一覧ページと詳細ページに編集ページへのリンクを追加
index.html.slimlink_to '編集', edit_task_path(task)show.html.slimlink_to '編集', edit_task_path6-2. editアクションとupdateアクションを定義づける
tasks_controller.rbdef edit @task = Task.find(params[:id]) end def update task = Task.find(params[:id]) task.update!(task_params) redirect_to tasks_url, notice: "タスク「#{task.name}」を更新しました。" end6-3. 編集ページのビューを実装
new.html.slim= form_with model: @task, local: true do |f| .form-group = f.label :name #label = 入力欄の名前の表示とラベル部分クリックで入力欄をフォーカスする = f.text_field :name .form-group = f.label :description = f.text_area :description = f.submit nilパーシャルテンプレートを使った共通化
パーシャルテンプレートを使い、新規登録ページと編集ページの共通部分をまとめる
_form.html.slim= form_with model: task, local: true do |f| .form-group = f.label :name = f.text_field :name .form-group = f.label :description = f.text_area :description = f.submit nilパーシャルテンプレートの呼び出し
new.html.slim= render partial: 'form'. locals: {task: @task} #「インスタンス変数@taskを、パーシャル内のローカル変数taskとして渡す」edit.html.slim= render partial: 'form'. locals: {task: @task}7. 削除機能の実装
7-1. 一覧ページと詳細ページに削ボタンを追加
index.html.slim= link_to '削除', task, method: :delete, date: { confirm: "タスク「#{task.name}」を削除します。よろしいですか?"}show.html.slim= link_to '削除', @task, method: :delete, date: { confirm: "タスク「#{task.name}」を削除します。よろしいですか?"}7-2. destroyアクションを定義づける
tasks_controller.rbdef destroy task = Task.find(params[:id]) task.destroy redirect_to tasks_url, notice: "タスク「#{task.name}」を削除しました。" end
- 投稿日:2020-07-26T17:07:37+09:00
【Mac】「Docker」で「Ruby on Rails 6」「React」と「MySQL 8」で環境構築(CRUDのサンプル付)
■ はじめに
SESエンジニアとして、PHPをメインに参画し、現在はJava案件に参画しているTatsuyaです。
そしてこの度転職が決まりまして、9月から新しい職場に!!!!
楽しみもある反面、実は不安要素が。。。それは、、、
次の職場は!!Ruby on Rails!!やったことない!!僕「学習しないと転職した瞬間からお荷物確定ダァー」
と焦り散らかし始めて、先日からRuby on Railsの学習に励み始めたわけなのですが。。。
僕「Rubyの公式チュートリアルめちゃくちゃ大変すぎないか!?さらにHerokuとかも使うの!?結構大掛かり!?」
というので、実は挫折しちゃいました。。。
挫折理由としては、チュートリアル自体が骨太ということもありますが、 Dockerを使った環境構築 も重なって折れちゃいました。(最弱)僕は、直接自分のMacにRuby環境は作りたくなかったんで、なんとしてでもDockerでローカル環境を整えたかったのです。
(次の職場でもDocker使うみたいですしお寿司)ということで、先人の方々が丁寧に書き上げていただいた貴重な文献を元に、実際に動かしながらRubyの動きがわかるような良い感じの環境を作らせていただいて、そこで学習を進めようという方向に舵を切ることになりました。
その過程で、とてもわかりやすく、実際に手を動かしながら理解しやすいと感じた「環境」「サンプルコード」を有り難くミックスさせていただいたので、そちらの結果の方をこの記事でご紹介できればなと思います。
◇ 大変お世話になった貴重な参考文献(無許可です。すいません。)
以下に記載した方々のお力をお借りして、良い感じのサンプルサイトの構築ができました。ありがとうございます。
これから環境構築作業に入っていきますが、丁寧に進めたい!という方はぜひ僕の記事と一緒に偉大なる先人方の記事を見ていただければかなり深まるのではないかと思います。◉ 環境
Docker + Ruby on Rails + MySQLで開発環境を新規構築する
- 丁寧な記事で、詳しくソース毎の解説をしてくれています。またDockerのインストールから記載していただいているので、初めてDockerに触るという方にもオススメです。とてもありがたかったです。
◉ サンプルコード
Rails + React + AjaxでCRUDのサンプルプロジェクト
Rails + React + AjaxでCRUDのサンプルプロジェクト(ソース一式)
- チュートリアル方式で解説を進めてくださっております。大変わかりやすいです。私はまるっとクローンして動かしたのですが、書きながら学びたい方は順に進めていくのも良いと思います。
◉ その他細かい部分でお世話になった記事
Rails on Dockerでcredentialsをeditしたい
◇ 感謝と謝罪
- ※基本的に参考記事様の情報をコピペで使用しております。私が今回取り組んだのは、 環境とサンプルコードのミックス なので、変にアレンジさせずにほぼ原型のままで使用させていただいております。貴重なお時間を割いて書き上げていただいた記事を丸々コピーするような形になりまして、大変申し訳ないという気持ちと、Rubyの学習コストを下げていただいて誠に有り難うございますという感謝と謝罪を先にさせていただきたいと思います。
■ 実際に環境を構築していきましょう
◇ 前提条件
- Mac
- Docker インストール済みであること
- まだの方はこちらの記事から
- git インストール済みであること
◇ 最終的なディレクトリ構成
ディレクトリ構成mpp_react_crud /mysql /Dockerfile /my.cnf /rails /app /bin /config /db /log /node_modules /public /tmp .browserslistrc .gitignore .ruby-version .babel.config.js .config.ru Dockerfile Gemfile Gemfile.lock LICENSE package.json pastcss.config.js Rakefile README.md yarn.lock docker-compose.yml◇ サンプルプロジェクトをクローン
terminal# homeへ $ cd # mpp_react_crud ディレクトリを作成 $ mkdir mpp_react_crud # mpp_react_crud ディレクトリに移動 $ cd mpp_react_crud # サンプルプロジェクトをclone $ git clone https://github.com/TakeshiOkamoto/mpp_react_crud.git # mpp_react_crudプロジェクト の名前を railsに変更 $ mv mpp_react_crud rails◇ docker-compose.yml の作成
terminal# 『◇ サンプルプロジェクトをクローン』の続きから # docker-compose.yml を作成する $ vi docker-compose.yml # vim画面が開いたら、「iキー」でINSERTモードにし、以下のymlをコピペしてください。 # コピペが完了したタイミングで、「:wq」で保存をして終了してください。docker-compose.ymlversion: "3.7" services: db: build: mysql image: mpp_react_crud_db container_name: mpp_react_crud_db ports: - 3306:3306 app: build: rails command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" image: mpp_react_crud_app container_name: mpp_react_crud_app ports: - 3000:3000 volumes: - ./rails:/rails depends_on: - db◇ MySQL用のDockerfileとmy.cnfを作成
▼ Dockerfile の作成
terminal# 『◇ docker-compose.yml の作成』の続きから # mysql ディレクトリを作成 $ mkdir mysql # mysql ディレクトリに移動 $ cd mysql # Dockerfile を作成する $ vi Dockerfile # vim画面が開いたら、「iキー」でINSERTモードにし、以下のymlをコピペしてください。 # コピペが完了したタイミングで、「:wq」で保存をして終了してください。DockerfileFROM mysql:8.0.18 ENV MYSQL_ROOT_PASSWORD root_pass COPY ./my.cnf /etc/mysql/conf.d/my.cnf RUN mkdir /var/log/mysql RUN chown mysql:mysql /var/log/mysql RUN mkdir /var/run/mysql RUN chown mysql:mysql /var/run/mysql▼ my.cnf の作成
terminal# my.cnf を作成する $ vi my.cnf # vim画面が開いたら、「iキー」でINSERTモードにし、以下のymlをコピペしてください。 # コピペが完了したタイミングで、「:wq」で保存をして終了してください。my.cnf[mysql] default-character-set=utf8mb4 [mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_bin datadir=/var/lib/mysql socket=/var/run/mysql/mysql.sock log-error=/var/log/mysql/mysqld.log pid-file=/var/run/mysql/mysqld.pid port=3306 default_authentication_plugin= mysql_native_password [client] default-character-set=utf8mb4terminal# 最後に mpp_react_crud ディレクトリに戻る $ cd ..◇ Ruby on Rails用のDockerfileとGemfile.lockを作成
▼ Dockerfile の作成
terminal# 『◇ MySQL用のDockerfileとmy.cnfを作成』の続きから # rails ディレクトリに移動 $ cd rails # Dockerfile を作成する $ vi Dockerfile # vim画面が開いたら、「iキー」でINSERTモードにし、以下のymlをコピペしてください。 # コピペが完了したタイミングで、「:wq」で保存をして終了してください。DockerfileFROM ruby:2.6.5 ENV LANG C.UTF-8 ENV APP_HOME /rails RUN apt-get update -qq && apt-get install -y build-essential nodejs # もしyarnでエラーが発生した場合 RUN apt-get update -qq && apt-get install -y curl && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && apt-get update && apt-get install -y yarn && apt-get install -y vim RUN rm -rf /var/lib/apt/lists/* RUN mkdir $APP_HOME WORKDIR $APP_HOME COPY ./Gemfile $APP_HOME/Gemfile COPY ./Gemfile.lock $APP_HOME/Gemfile.lock RUN bundle install COPY . $APP_HOME EXPOSE 3000▼ Gemfile.lock の作成
terminal# Gemfile.lock を作成 # 空ファイルで良いので、touch コマンドで作成します。 $ touch Gemfile.lock◇ Rails用データベース設定ファイル"database.yml"を編集
terminal# 『◇ Ruby on Rails用のDockerfileとGemfile.lockを作成』の続きから # database.yml を編集 $ vi config/database.yml # vim画面が開いたら、「iキー」でINSERTモードにし、以下のymlをコピペしてください。 # コピペが完了したタイミングで、「:wq」で保存をして終了してください。database.ymldefault: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: root_pass host: db development: <<: *default database: mpp_react_crud_development test: <<: *default database: mpp_react_crud_test production: <<: *default database: username: password:terminal# 最後に mpp_react_crud ディレクトリに戻る $ cd ..◇ Dockerイメージをビルド
terminal# 『◇ Rails用データベース設定ファイル"database.yml"を編集』の続きから # Docker イメージをビルド $ docker-compose build◇ docker-composeでアプリを起動〜プロジェクト設定
terminal# 『◇ Dockerイメージをビルド』の続きから # サービスの立ち上げ $ docker-compose up -d # -d: バックグラウンドで起動 # bundle インストール $ docker-compose run app bundle install # 各種パッケージのインストール $ docker-compose run app yarn install # マスターキーの生成 # ファイル生成後、credentials.yml.encの編集画面が表示されるので:q!で終了します。 $ docker-compose run -e EDITOR=vim app rails credentials:edit # フォルダの生成 $ mkdir rails/app/assets/images◇ データベース準備
terminal# 『◇ docker-composeでアプリを起動〜プロジェクト設定』の続きから # データベースの作成 $ docker-compose run app rails db:create # 各テーブルの作成 $ docker-compose run app rails db:migrate # 各テーブルの初期データの作成 $ docker-compose run app rails db:seed◇ 完成!
最後にコンテナを再起動しましょう。
terminal$ docker-compose restart
再起動終了後に、http://localhost:3000 にアクセスしてみてください!
すると、以下のようなページが開かれます。以上でサンプルサイトの環境構築終了です!
■ 終わりに
初心者の方だけでなく、経験者の方でもあっても、初めて触る技術の学習にはかなり苦戦すると思います。
そのため、先に「どういうサイトがどのように動いているか」ということを知るだけでも学習効率は上がるんじゃないかなーと思い、今回の作業に取り掛かりました。
まだ私も環境構築をしてすぐにこの記事を書いているので、この環境を使った学習は始めていませんが、実際にログを仕込んでみたりしながら体感的に学ぶ方が飲み込みは早いと考えるので、ガンガンこの環境とこのサイトを元に学習に取り組んでいこうと思います。この記事は解説部分を完全に端折って、ただただ完成させることだけを目標に書き上げました!
そのため、しっかりと解説を確認したい方は、参考文献様の記事をご確認いただいて、照らし合わせながら進めてほしいと思います。僕「本当に先人の方々はすげえや。」
- 投稿日:2020-07-26T16:38:41+09:00
【ポートフォリオ を作成される方へ】ransackで作る検索機能
ransackとは
検索機能を少ないコードで簡単に実装できるgemです。
設定も簡単でできることもたくさんあります。導入方法
Gemfilegem 'ransack'$ bundle install使い方
①検索パラメーターは ":q"
②Ransack版form_forは"search_form_for"
③検索結果は"resultメソッド"で取得できるcontroller.rbclass ProductsController < ApplicationController def index @q = Product.ransack(params[:q]) @products = @q.result(distinct: true) end endview.html.erb<%= search_form_for @q do |f| %> # nameカラムに対して部分一致検索ができる <%= f.label :name_cont, "商品名を含む" %> <%= f.search_field :name_cont %> <div class="actions"><%= f.submit "検索" %></div> <% end %>f.search_field :name_contのnameのところを変えるだけで違う検索を変更することができます。
検索方法 意味 *_eq 等しい *_cont 部分一致 *_lteq 以下 *_gteq 以上 *_start で始まる *_end で終わる
- 投稿日:2020-07-26T15:23:50+09:00
[Ruby] 標準入力を受け付ける際に ^H などの ASCII 制御文字を意図した通りに認識させる
結論
STDIN.gets
ではなくReadline.readline
を使おうSTDIN.gets
Ruby で標準入力を受け取る方法を調べると、多くの場合、以下の実装方法が出てきます。
stdin.rbprint '> ' text = STDIN.gets.strip puts "You said #{text}!"$ ruby stdin.rb > hello You said hello!
gets
の他にもread
やreadline
、readlines
があるようです。
Ruby 標準入力から複数行読み取りたい欠点
しかし、上記の方法だと ASCII 制御文字を意図した挙動で認識させることができません。
$ ruby stdin.rb > hello^H^H^H You said he!lo上記の例では、
hello
と入力したあとに、ASCII 制御文字の backspace (ctrl
+H
で入力可能) を 3 回入力しています。本当は
hello
と入力したあとに 3 回 backspace の制御文字を入力しているのでhe
となってほしいのですが、hello^H^H^H
となってしまい、標準入力の受け付けを終了したあとに backspace が適用されています。解決方法
これを意図した通りにするためには以下のように実装します。
readline.rbrequire 'readline' print '> ' text = Readline.readline puts "You said #{text}!"$ ruby readline.rb > he # "hello" と入力したあとに ctrl + H を 3 回押した You said he!その他
Readline.readline
は、他にもヒストリを使うことができたりして便利です。カーソルの上キーやctrl
+P
を押すと、前に入力した文字列を表示することができます。詳しくは module Readline のリファレンスマニュアル を参照ください。ASCII 制御文字については Wikipedia をご覧ください。
- 投稿日:2020-07-26T14:56:56+09:00
ランキング機能のつくりかた【Railsの内部結合と外部結合を理解する】
概要
この記事は、実際に私がBooQsで運用している「ランキング機能」を例にして、Railsのテーブル結合(内部結合と外部結合)を解説する記事です。
前提条件
Rails 5.1
【?♂️Before】テーブル結合を使わないランキング機能
私が開発しているBooQsという英単語学習サービスでは、ゲーミフィケーションを用いてユーザーの学習意欲を高めるために、英単語の解答数(userのもつanswer_historiesの数)によるユーザーのランキング機能を実装しています。
しかし、以下のNewrelicのダッシュボードを見ていただくとわかるように、自分の書いたランキング機能の処理速度はお世辞にも良いものとは言えませんでした。
それではまず、自分が組んだ(下手くそな)コードから見ていきましょう。
恥ずかしながら、自分はテーブル結合をよく理解していなかったため、次のようなまどろっこしいコードでランキング機能を実装していました。home_controller.rbdef user_ranking ## ここから!! rankers = [] user_ids = AnswerHistory.where(created_at: Time.now.all_month).group(:user_id).order('count(user_id) desc').pluck(:user_id) user_ids.each do |id| #user_idがnilである非ログインユーザーの解答履歴を除外する。 if id.present? user = User.find(id) rankers << user end end @users = rankers.paginate(page: params[:page], per_page: 10) ## ここまでがわいの書いたうんこーど!!!??? #ページネーションの位置に応じて、最初に表示する順位を調整するための処理 if params[:page].present? @base_of_ranking = params[:page].to_i*10+1-10 else @base_of_ranking = 1 end end以下に、modelとviewも記載します。
model
models/user.rbclass User < ApplicationRecord has_many :answer_histories, dependent: :destroy endmodels/answer_history.rbclass AnswerHistory < ApplicationRecord # 非ログインユーザーの解答記録は、user_idをnilで記録する。 belongs_to :user, optional: true endView
views/home/user_ranking.html.erb<% provide(:title, "月間ランキング") %> <%= render "home/navbar" %> <div class="box users_index"> <div class="wrapper"> <div class="headline-green"> <h1 class="center green"><i class="fas fa-user-crown"></i>解答数ランキング</h1> </div> <p> </p> <div class="quiz_tab"> <%= link_to home_user_ranking_path, class: "left btn green" do %> 月間 <% end %> <%= link_to home_user_ranking_weekly_path, class: "right btn" do %> 週間 <% end %> </div> <ul class="users"> <% @users&.each_with_index do |user, i| %> <% i += @base_of_ranking %> <div class="whole_link"> <li class="user-feed"> <%= link_to user_path(user) do %> <%= icon_for user, size: 50 %> <% end %> <div class="right-side"> <div class="name"> <%= link_to user do %> <% if user.premium_member? %> <i class="fas fa-crown non-margin"></i> <% end %> <%= user.name %> <% end %> </div> <p>ランク:<b><%= i %>位</b></p> <p>月間解答数:<b><%= user.answer_histories.where(created_at: Time.now.all_month).count %></b></p> <p>継続日数:<b><%= running_days_count(user) %></b></p> <p>レベル:<b>Lv.<%= user.current_level.floor %></b></p> <div class="follow_btn"><%= render 'users/follow_form', user: user %></div> </div> </li> <a href="<%= user_path(user) %>" class="link"></a> </div> <% end %> </ul> <div class="center"> <%= will_paginate @users, previous_label: '← 前へ', next_label: '次へ →', page_links: false %> </div> </div> </div>実際のランキングページは以下になります。
ランキングページ: https://www.booqs.net/home/user_ranking
【?♂️After】内部結合を使ったランキング機能
Beforeのテーブル結合を利用しない方法だと、user_idsを取得するためにクエリを発行し、さらにユーザーを取得するたびに、
user = User.find(id)
でクエリが発行されるため、処理が遅くなります。そのため、知り合いのベテランの開発者の方に、下のような方法をオススメいただきました。
home_controller.rbdef user_ranking ## ここから!!! @users = User.joins(:answer_histories).where(answer_histories: {created_at: Time.now.all_month}) .group(:id).order('count(answer_histories.user_id) desc') .paginate(page: params[:page], per_page: 10) ## ここまでが修正いただいたコード!!!??? #ページネーションの位置に応じて、最初の順位を調整するための処理 if params[:page].present? @base_of_ranking = params[:page].to_i*10+1-10 else @base_of_ranking = 1 end endめちゃくちゃスッキリ!!
さらにクエリも一発でとても効率的...!!ただこのコードをコピペするだけでは自分の勉強にはならないので、
ここからは、これらの処理がどんなことをしているのかを1つずつ丁寧に解説していきます。
内部結合【
.joins(:answer_histories)
】
.joins(:answer_histories)
を利用することで、usersテーブルとanswer_historiesテーブルを結合して、データベースを検索できるようになります。つまり、2つのテーブル同士を合体させて、1つのテーブルをつくれるのですね!
BooQsの例で、解説しましょう。
まず、BooQsには以下のように、
会員登録したユーザーのデータを格納するusersテーブル
と、そのユーザーが解いた問題のデータを格納するanswer_historiesテーブル
があります。usersテーブル
id name 1 清水さん 2 小林さん 3 長谷川さん 4 山田さん anwser_historiesテーブル
id quiz_id user_id 1 104 4 2 89 nil 3 95 2 4 184 4 5 43 1 6 205 1 7 21 nil 8 76 1 9 164 1 (補足:user_idがnilであるanswer_historiesは、ログインしていないユーザーの解答履歴です。)
そして、
User.joins(:answer_histories)
を利用することによって、answer_historiesテーブルの外部キー(user_id)に基づいて、2つのテーブルを結合することができます。では、実際に見てみましょう。
usersテーブルにanswer_historiesテーブルを内部結合したテーブル
id name id quiz_id user_id 4 山田さん 1 104 4 2 小林さん 3 95 2 4 山田さん 4 184 4 1 清水さん 5 43 1 1 清水さん 6 205 1 1 清水さん 8 76 1 1 清水さん 9 164 1 右端のanswer_historisの外部キー(user_id)に基づいて、
きちんと2つのテーブルがつながりましたね!結合結果のテーブルの中ですぐに目につく変化としては、次の3つがありますね!
- answer_historiesのuser_id(外部キー)の数だけ山田さんと清水さんが増殖した。
- answer_historiesのuser_idに存在しない長谷川さんは排除された。
- user_idがnilであるanswer_historiesのidが2と7のレコードが排除された。
(注意:例ではanswer_historiesの主キー(id)の昇順でレコードを並べていますが、私の調べた限りでは、結合結果のレコードの順序については、特定のidを基準にした昇順・降順などはないようでした。もし私が間違っていたら誰かぜひ教えてください。)
このようなjoinsメソッドを利用したテーブル同士の結合方法は、【内部結合】と呼ばれます。
内部結合では、結合条件に合致しないレコードは排除されます。
内部結合の結合条件とは、
結合先の外部キー = 結合元の主キー
です。BooQsの例で紹介しましょう。
たとえば、今回のUser.joins(:answer_histories)
によって内部結合を行うと、次のようなSQLが発行されます。「内部結合」で発行されるSQLSELECT "users".* FROM "users" INNER JOIN "answer_histories" ON "answer_histories"."user_id" = "users"."id"上記SQLの
ON "answer_histories"."user_id" = "users"."id"
の部分が、テーブルの結合条件となります。つまり、answer_historiesレコードの外部キー(user_id)とusersレコードの主キー(id)が一致するかどうかが、レコード同士を結合する条件となるのですね。
そして内部結合においては、この結合条件に合致しないusersテーブルとanswer_historiesテーブルのレコードは、結合結果のテーブルから排除されています。具体的に見ていきましょう。
上の結合されたテーブルをじっくりと眺めてください。
きっと次の2種類のレコードが排除されていることがわかるはずです。
- answer_historiesの外部キー(user_id)に存在しないusersレコード(例:長谷川さんのレコードが排除されている)
- 外部キーがnilであるanswer_historiesレコード(例:idが2と7のレコードが排除されている)
具体例があると、きっとわかりやすいと思います。
別の表現で説明すると、内部結合では、以下に示すようなテーブル結合結果にはならない ということです。
(以下で紹介する例は、テーブル結合のうち「外部結合」の解説でもあります。
ランキング機能では「内部結合」を利用するので、読み飛ばしていただいても構いませんが、読んでおくと、ランキングで利用する「内部結合」についても理解が深まるはずです。)
【こうはならない!!】answer_historiesの外部キーに存在しないusersレコードまで結合される【左外部結合】
id name id quiz_id user_id 4 山田さん 1 104 4 2 小林さん 3 95 2 4 山田さん 4 184 4 1 清水さん 5 43 1 1 清水さん 6 205 1 1 清水さん 8 76 1 1 清水さん 9 164 1 3 長谷川さん nil nil nil 上記のテーブルでは、answer_historiesレコードのuser_idに存在しないはずの「長谷川さん」までテーブルに結合されています。
長谷川さんは人生でただ一回もBooQsで問題を解いてくれなかったので、長谷川さんに結合されたanswer_historiesレコードのデータは、なんと、すべてnilです!!(解いてよ!!!?)これは内部結合ではありません。
これは【外部結合】と呼ばれています。
さらに言えば、外部結合のうち、【左外部結合】と呼ばれるテーブル結合方法です。
内部結合と外部結合の区別は簡単です。
外部結合では、内部結合では排除されていた「結合条件に合致しないレコード」まで結合することができます。外部結合には、【左外部結合】と 【右外部結合】の2種類がありますが、こちらもいかめしい文字面よりは、区別も簡単です。
左外部結合は、結合条件に合致しない結合『元』のレコードを結合します。
右外部結合は、結合条件に合致しない結合『先』のレコードを結合します。今回の例でいえば、長谷川さんという、結合条件に合致しない「usersテーブル(結合元)のレコード」を結合しています。
だから、【左外部結合】なのですね。Railsによる左外部結合は、次のように行えます。
Railsにおける「左外部結合」のやり方.rbUser.joins("LEFT OUTER JOIN answer_histories ON users.id = answer_histories.user_id") #Rails5.0以降は、次のメソッドで右外部結合を行うこともできます。 User.left_outer_joins(:answer_histories)【こうはならない!!】外部キーがnilであるanswer_historiesレコードまで結合される【右外部結合】
id name id quiz_id user_id 4 山田さん 1 104 4 nil nil 2 89 nil 2 小林さん 3 95 2 4 山田さん 4 184 4 1 清水さん 5 43 1 1 清水さん 6 205 1 nil nil 7 21 nil 1 清水さん 8 76 1 1 清水さん 9 164 1 上記のテーブルでは、user_idがnilであるanswer_historiesレコードも結合されてしまっています。
user_idがnilということは、これは「会員登録していないユーザーの解答記録」ということなので、当然、結合されたusersレコードのデータもすべてnilです(会員登録してよ!!!?)。このテーブルを生成する結合方法は、「外部結合」のうち、「右外部結合」と呼ばれています。
先ほど説明したように、結合条件に合致しない、結合『先』のレコードを結合するので、右外部結合なのですね。
answer_historiesテーブルは、結合先のテーブルです。右外部結合は、Railsでは次のコードで行えます。
Railsにおける「右外部結合」のやり方.rbUser.joins("RIGHT OUTER JOIN answer_histories ON users.id = answer_histories.user_id")(Railsの右外部結合については、なぜか1つも解説記事が見当たりませんでした。
この記事がRailsによる右外部結合について触れた最初の記事かもしれません。
外部結合(左外部結合&右外部結合)をきちんと理解されたいなら、以下の記事を参考にされると良いでしょう。
SQL素人でも分かるテーブル結合(inner joinとouter join))
ランキング機能をつくる場合、『内部結合によるレコードの排除』はとても都合の良い処理です。
なぜなら、排除される
answer_historiesの外部キーに存在しないusersレコード
とは、すなわち、「1問も問題を解いていないランキング圏外のユーザーのデータ」であり、
外部キーがnilであるanswer_historiesレコード
とは、すなわち、「ログインしていないユーザーの解答記録」であるため、ランキングをつくる上で考慮する必要のないデータだからです。
結合先のレコードの絞り込み【
.where(answer_histories: {created_at: Time.now.all_month})
】Where句を使って、結合先のカラムの値を条件にして、レコードを絞り込むことも可能です。
その場合、
where(結合先のテーブル: {結合先のテーブルのカラム: 値})
という形で行います。BooQsの例で言えば、『月間の』解答数ランキングを表示したかったので、次のように結合先のデータを絞り込んでいます。
「月間」の解答数で絞り込む.rb@users = User.joins(:answer_histories).where(answer_histories: {created_at: Time.now.all_month})ランキングの順位順にusersレコードを並べ替える
【.group(:id).order("count(answer_histories.user_id) DESC")】
さて、ランキング圏内のユーザーのレコードはすべて揃えたので、最後にこれを順位順に並べ替えましょう。
ここまで内部結合を使って作成したテーブルは次のようになっていますよね。
id name id quiz_id user_id 4 山田さん 1 104 4 2 小林さん 3 95 2 4 山田さん 4 184 4 1 清水さん 5 43 1 1 清水さん 6 205 1 1 清水さん 8 76 1 1 清水さん 9 164 1 明かに、清水さんが1位、山田さんが2位、小林さんが3位という風にわかりますが、
これをプログラムで集計するためには、groupメソッド
でユーザーごとにレコードをまとめる必要があります。BooQsの例でいえば、
.group(:id)
でそれぞれのユーザーのレコードをまとめらていますね。さて、groupメソッドでまとめたら、それぞれのグループがもつレコードの数で順位をつければ、ランキングを完成させることができます。
ここで使われるのが、
orderメソッド
です。
orderメソッド自体は、馴染みのある方ばかりでしょうが、実はこのorderメソッド、中身でSQLも利用できます。そのため、groupメソッドによる
GROUP BY
、orderメソッドによるORDER BY
の2つを組み合わせて次のようにすれば、「レコードの多い順(降順)にユーザーを並べ替え」ができ、結果としてランキングの順位でユーザーを並べることができます。ランキング順位でユーザーを並べ替える.rb@users = User.joins(:answer_histories).where(answer_histories: {created_at: Time.now.all_month}) .group("id").order("count(answer_histories.user_id) DESC")orderのなかで、
.order("count(answer_histories.user_id) DESC")
という風に条件を指定していることに注目してください。
この"count([数えたい要素]) 並び順"
という指定方法は、RubyでもRailsでもなく、SQLによる指定方法です。発行されるSQLのクエリは、次のようになっています。
ランキング順位で並べ替え時に発行されるSQLSELECT "users".* FROM "users" INNER JOIN "answer_histories" ON "answer_histories"."user_id" = "users"."id" GROUP BY "users"."id" ORDER BY count(answer_histories.user_id) DESCこのようにgroupメソッドとorderメソッドをうまく使うことで、ランキングの順位でユーザーを並べ替えることができます。
id name 1 清水さん 4 山田さん 2 小林さん あとはこれを上記のviewで
each_with_index
などで取り出してあげれば、ランキング機能の完成です。お疲れさまでした!!
ランキングページ: https://www.booqs.net/home/user_ranking
これだけ教えてください?♂️
これだけ本当にわからなかったので、わかる方がいらしたらぜひ教えてください。。。
内部結合したあとの、users.idとanswer_histories.user_idは同じはずだと思います。
なので私は、下のコードは、
@users = User.joins(:answer_histories)
と全く同じだと思っていました。
.group("id").order("count(answer_histories.user_id) DESC")ランキング順位でユーザーを並べ替える.rb@users = User.joins(:answer_histories) .group("answer_histories.user_id").order("count(answer_histories.user_id) DESC")しかし、このコードを実行すると
ActiveRecord::StatementInvalid (PG::GroupingError: ERROR: column "users.id" must appear in the GROUP BY clause or be used in an aggregate function
というエラーが表示されてしまいます。
つまり、group(GROUP BY)のなかに「users.id」を利用しろと言われてしまいます。
なぜ、groupのなかは、"answer_histories.user_id"
ではダメで、"id"
でなくてはならないのでしょうか??何卒ご教示いただければ幸いです...!?♂️
- 投稿日:2020-07-26T14:19:09+09:00
メソッドや定数を使った出力 学習メモ
- 投稿日:2020-07-26T13:27:12+09:00
gem 'kaminari' でページネーション実装
- 投稿日:2020-07-26T13:04:45+09:00
Ruby入門2
initialize
newメソッドでオブジェクトを生成するとき、そのオブジェクトのinitializeメソッドが実行される
class User attr_reader :name, :address, :email def initialize(name, address, email) @name = name @address = address @email = email end endprivate
privateと書かれた行より後に定義されたメソッドはprivateメソッドとなり、オブジェクトの内部からは利用できるが、外部からは利用不可
class Person def initialize(money) @money = money end def billionaire? money >= 10000000 end private def money @money end end継承
既存のクラスが持っている機能を基本的に全部引き継いだ上で、一部を変えたいときに使用
親クラスが持つメソッドの処理を、子クラスの書かれた処理で上書きすることを「オーバーライド」という。
※親クラスを呼びたいときはsuperを使うclass PricedObject #親クラス、スーパークラス def total_price price * Tax.rate end def price raise NotImplementedError end end class product < PricedObject #子クラス、サブクラス attr_accessor :price end class OrderedItem < PricedObject #子クラス、サブクラス attr_accessor :unit_price, :volume def price unit_price * volume end endモジュール
Rubyの基本単位はオブジェクトであり、オブジェクトの設計図としてクラスがある。
この他、Rubyにはある一連の振る舞いの設計図を一箇所にまとめた存在として「モジュール」という概念があるモジュールとクラスの違いは、モジュールはオブジェクトを生成することができない。
モジュールは一連の振る舞いを表しており、それを「include」を使い、まとめてクラスに取り込んでもらうことができる。
module Talking def talk "ワンワン" end module Walking def walk "テケテケ" end end class Dog include Talking include Walking end mugi = Dog.new mugi.talk mugi.walk#継承を使った書き方 class PricedObject def total_price price * Tax.rate end def price raise NotImplementedError end end class product < PricedObject attr_accessor :price end class OrderedItem < PricedObject attr_accessor :unit_price, :volume def price unit_price * volume end end #モジュールを使った書き方 module PricedHolder def total_price price * Tax.rate end end class product include PriceHolder attr_accessor :price end class OrderedItem include PriceHolder attr_accessor :unit_price, :volume def price unit_price * volume end end例外捕捉
- 例外が発生するかもしれないコードをbeginのなかに記述
- その中で発生した例外への対応の仕方をrescueの中に記述
- さらに、例外が出た場合も出なかった場合も必ず行いたい後処理をensureののなかに記述
begin #例外が発生するかもしれないコード rescue #例外に対応するコード ensure #例外が発生してもしなくても必ず実行したいコード endnilガード
もし変数がnilだった場合に値をいれる構文
#例1 a = nil a ||= 3 puts a # ==> 3 #例2 def people @people ||= [] end # ==>@peopleがnilである場合も、このメソッドが呼び出された時は空の配列が代入されて返される。ぼっち演算子(safe navigation operator)
&.を使ってメソッドを呼び出すと、レシーバがnilであった場合でもエラーが発生しない。
#ifを使った場合 name = if object object.name else nil end #ぼっち演算子を使った場合 name = object&.name%記法
配列の全ての要素が文字列の場合、「%w」を使って配列を記述可能
array1 = ['apple', 'banana', 'orange'] #==>["apple", "banana", "orange"] #↓同じ array1 = %w(apple banana orange) #==>["apple", "banana", "orange"]全ての要素がシンボルでる配列は、「%i」を使って配列を記述可能
array1 = [:apple, :banana, :orange] #==>[:apple, :banana, :orange] #↓同じ array1 = %i(apple banana orange) #==>[:apple, :banana, :orange]
- 投稿日:2020-07-26T12:52:39+09:00
【Rails】JavaScriptが絡むテスト
はじめに
Capybaraでのテストで、JavaScriptが作動する処理をする場合には少し設定を変える必要があります。
今回は簡単な例としてクリックするとテキストが変わる処理についてテストしてみます。設定
まずはCapybaraを導入する必要がありますが、わからない方は過去に書いたこちらの記事を参照してください。
続いてhelperファイルに追記します。
spec_helper.rb# 最後の行に追加 ENV["RAILS_ENV"] ||= 'test' require File.expand_path("../../config/environment", __FILE__) Capybara.javascript_driver = :selenium_chrome_headless※ なぜかjavascript_driverにwebkitを使うとうまくいきませんでした…
詳しい方いたらコメントいただけると助かります。テストファイル
spec/features
の下にファイルを用意します。sample_spec.rbrequire 'rails_helper' feature 'post', type: :feature do scenario 'jsが動作すること', js: true do # クリックの表示が存在する visit root_path expect(page).to have_content('クリック') # クリックの文字をクリックするとOKになる find('.js-class').click expect(page).to have_text("OK") end endjsを動作させたい場合は上述のようにscenarioの行に
js: true
を追記する必要があります。念のためviewファイルとjsファイルも載せておきます。
(HamlとjQueryを使ってます)index.html.haml.header = link_to "トップページ", root_path .js-class クリックsample.js$(function(){ $(".js-class").on("click", function() { $(this).text("OK"); }) })テスト実行
あとはターミナルから実行するだけです。
ターミナル$ bundle exec rspec spec/features/sample_spec.rb
- 投稿日:2020-07-26T12:43:02+09:00
メソッドや定数を使って出力 学習メモ
- 投稿日:2020-07-26T11:59:53+09:00
同時に同一のレコードにアクセスして編集しがちなテーブルにはlock_versionが使えるかも
はじめに
Rails Guide読んでて気になったので実際に触ってみました。そのメモです。
アプリにてAさんとBさんが同じレコードを編集する際、全く同じタイミングで同じレコードにアクセスした場合、Aさんがレコードをupdateしてその後にBさんがupdateするとBさんはAさんの編集内容を確認せずに上書きしてしまいます。
これが同じ属性のupdateならまだしも別の属性である場合は編集が巻き戻されるので良くない状況です。この問題に対しActive Recordにlock_versionが用意されています。
lock_versionはテーブルにそのインスタンスの編集履歴カウントの役割をするカラムを追加し、その値を参照することで
lock_versionを利用すると上記の様なケースに置いてエラーを発生させることができます。使用方法
利用するにはテーブルにlock_versionというカラムを作成すればいい。
t.integer :lock_version, default: 0動作確認
p1 = Memo.find(1) p2 = Memo.find(1) # => #<Memo id: 1, text: "hello", lock_version: 0>当然ですがまだ呼び出した時点ではlock_versionは0
次にp1をupdateします。p1.update(text:'good bye') # => #<Memo id: 1, text: "good bye", lock_version: 1>SQL(0.3ms) BEGIN Memo Update (13.4ms) UPDATE `memos` SET `text` = 'good bye', `lock_version` = 1 WHERE `memos`.`id` = 1 AND `memos`.`lock_version` = 0 (3.9ms) COMMITlook_versionが1に書き換わりました。
またupdateのSQLの条件にlock_version = 0
があることも確認できます。
次はp2をupdateしてみましょう。reloadしていないのでp2のインスタンス のlock_versionは0のままです。p2.update(text:'say hello') # => ActiveRecord::StaleObjectError (Attempted to update a stale object: Memo.)SQL(0.7ms) BEGIN Memo Update (1.0ms) UPDATE `memos` SET `text` = 'say hello', `lock_version` = 1 WHERE `memos`.`id` = 1 AND `memos`.`lock_version` = 0 (2.4ms) ROLLBACKちゃんとエラーでていますね。
SQL自体はエラー実行しても出ないと思うのでrails側で影響与えた行がないことをトリガーとしてエラーを返している感じかな?
今回ソースまで読まないので読んだ方は共有していただけると助かります!動作確認(Web)
RailsApi通り、formにhiddenパラメータとしてlock_versionを入れましょう。
これがないとバックエンドでlock_versionの比較ができないのでエラーを発生しません。<input type="hidden" value="2" name="memo[lock_version]" id="memo_lock_version">上記を追記し、同じレコードを編集するタブを2つ開いてそれぞれupdateすると後からupdateした方に無事エラーが出ました。
あとはこれまたRailsApiにある通りエラーハンドリングして対応しましょう。
対応の仕方としては色々考えられますね。
- 2人が別々の属性を編集したのであればそのまま順番にupdateして同一の属性を編集した時は画面に表示してユーザーに確認
- 自分よりも前の人がupdateした他の属性には影響が及ばない様にした上でupdateさせる
とか。この辺はデータがアプリ上でどの様に使用されているかによりけりと言ったところでしょうか。
最後に
僕は実務で利用したことないので「ウチではこうやってるよ!」的な共有がいただけると嬉しいです!
参考記事
- 投稿日:2020-07-26T11:47:03+09:00
【初心者】Rails6 でプロジェクトを立ち上げるまでとつまづいたところ
rubyのバージョン確認、インストール
$ ruby --versionrubyのバージョンは2.5.0以上であれば動きます。ただ、最新版の方がメソッド追加や処理が早くなっていたりします。今は2.7.1が安定版のようです。
#最新のrubyをインストール $ rbenv install 2.7.1rails6でプロジェクトの立ち上げ
$ mkdir sample_app $ cd sample_app #rubyのバージョン変更 $ rbenv local 2.7.1 #rails6インストール $ gem install rails -v '6.0.0' # DBをPostgreSQLに変更しています $ rails _6.0.0_ new -d postgresql $rails sつまづきポイント
通常なら上記で立ち上がるのですが、初めてrails6を使うと
$ rails s => Booting Puma => Rails 6.0.0.rc2 application starting in development => Run `rails server --help` for more startup options RAILS_ENV=development environment is not defined in config/webpacker.yml, falling back to production environment Exiting ... /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/webpacker-4.0.7/lib/webpacker/configuration.rb:91:in `rescue in load': Webpacker configuration file not found /home/ubuntu/environment/test-app1/config/webpacker.yml. Please run rails webpacker:install Error: No such file or directory @ rb_sysopen - /home/ubuntu/environment/test-app1/config/webpacker.yml (RuntimeError)こんなエラーが出てきます。
これはwebpackerがインストールできていないエラーなので$ rails webpacker:installすれば解決します。
再度、$ rails sを実施すれば
- 投稿日:2020-07-26T02:39:23+09:00
Rails Simple Calendarで記事投稿後、カレンダーに反映させたい。
Simple Calendarにて
記事投稿→カレンダー投稿画面(create)→カレンダーに反映という事を行いたいのですが、
現状、記事投稿は出来ていて、そこから記事投稿のタイトルデータを抜き出してカレンダー投稿画面にタイトルのみデータを表示取得させたいです。
※こちらの画面ではTitleが入力フォームになっていますがこのTitleを、記事のTitleを取得したいです。1、
- @tweets.each do |tweet|
= @tweet.title
を入力しましたが
このようなエラーが出ました。
blog(カレンダー)とtweet(記事)のアソシエーションとして
はこのように組んでいます。
blogのdb(データベース)はこちらになります。
create_table :blogs do |t| t.string :title t.text :content t.datetime :start_time t.references :user, foreign_key: true t.references :tweet, foreign_key: true t.string :tweet_title t.timestamps endblog_controllerはこちらになります。
ご回答いただければ幸いです。
未熟者ですがよろしくお願いします。
- 投稿日:2020-07-26T02:18:00+09:00
RailsTutorialチートシート
「RailsTutorialをやったはいいが、2週間もしたら何をしたか思い出せなくなる」
「結局自分には何ができるのか」「俺は...弱い...!」
そんな人のためのチートシートです。
とりあえず保存をして、こっそりコピペして、色々付け加えながら自分だけのチートシートを作りましょう。
機能要件
[Rails]検索機能
https://qiita.com/shin1rok/items/779e581e9d12a92310c3
[Rails]文章投稿機能
https://railstutorial.jp/chapters/user_microposts?version=5.1#cha-user_microposts
[Rails]画像を投稿したい(外部ライブラリ - CarrierWave)
https://railstutorial.jp/chapters/user_microposts?version=5.1#sec-basic_image_upload
[Rails]投稿した画像が大きくなっちゃう
https://railstutorial.jp/chapters/user_microposts?version=5.1#sec-image_validation
[Rails]本番環境での画像の保存方法 →クラウドストレージを使うのがいい。
https://railstutorial.jp/chapters/user_microposts?version=5.1#sec-image_upload_in_production
[Rails]フォロー、フォロワー機能の骨組み
https://railstutorial.jp/chapters/following_users?version=5.1#sec-the_relationship_model
[Rails]フォロー、フォロワー一覧ページを作りたい
https://railstutorial.jp/chapters/following_users?version=5.1#sec-following_and_followers_pages
[Rails]フォローボタンを作りたい(リダイレクトとAjax)
https://railstutorial.jp/chapters/following_users?version=5.1#sec-a_working_follow_button_the_standard_way
https://railstutorial.jp/chapters/following_users?version=5.1#sec-a_working_follow_button_with_ajax
[Rails]タイムライン機能
https://railstutorial.jp/chapters/following_users?version=5.1#sec-the_status_feed
[Rails]タイムライン、フィード
https://railstutorial.jp/chapters/user_microposts?version=5.1#sec-a_proto_feed
[Rails]新規登録をしたらメールアドレスが送られるやつ
https://railstutorial.jp/chapters/account_activation?version=5.1#cha-account_activation
[Rails]パスワードを忘れた時に再設定できるやつ
https://railstutorial.jp/chapters/password_reset?version=5.1#cha-password_reset
[Rails]成功しました!失敗しました!を知らせてくれるやつ - フラッシュメッセージ
https://railstutorial.jp/chapters/sign_up?version=5.1#sec-signup_error_messages
https://railstutorial.jp/chapters/basic_login?version=5.1#sec-rendering_with_a_flash_message
[Rails]ログイン、ログアウト機能(自前で)
https://railstutorial.jp/chapters/basic_login?version=5.1#cha-basic_login
[Rails]ログイン、ログアウト機能(外部ライブラリ- devise)
https://qiita.com/cigalecigales/items/f4274088f20832252374
[Rails]ログインしてブラウザを閉じてもログイン状態を維持したい! - Remember me
https://railstutorial.jp/chapters/advanced_login?version=5.1#sec-remember_me
[Rails]サムネイル画像を入れたい - Gravatar
https://railstutorial.jp/chapters/sign_up?version=5.1#sec-a_gravatar_image
[Rails]編集画面を表示したり更新したり削除したりしたい
https://railstutorial.jp/chapters/advanced_login?version=5.1#sec-remember_me
[Rails]入力する値が期待してるものと違ったら元に戻したい
- 期待してるものかどうかを判別→正規表現、バリデーション
https://railstutorial.jp/chapters/modeling_users?version=5.1#sec-user_validations
- 元に戻す→リダイレクト
https://railstutorial.jp/chapters/sign_up?version=5.1#sec-unsuccessful_signups非機能要件
Model系
[Rails]ActiveRecordはデータベースはデータベースのレベルでは一意性を保証していない(validateで一意性を保証したとしても)→indexをつければ解決
https://railstutorial.jp/chapters/modeling_users?version=5.1#sec-uniqueness_validation
[Rails]データベースに保存する前に保存する内容を検証したい - バリデーション
https://railstutorial.jp/chapters/modeling_users?version=5.1#sec-user_validationsView系
[Rails]ページネーション(ページ分割)
https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#sec-pagination
[Rails]編集フォーム
https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#sec-edit_form
[Rails]一覧表示させたい
https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#sec-showing_all_users
[Rails]部分テンプレートを呼び出したい → render ‘ファイル名’
https://pikawaka.com/rails/renderController系
[Rails]フォームの入力に失敗した時の実装
https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#sec-unsuccessful_edits
[Rails]Railsのredirect_toにおけるpathとurlの使い分け(同じようなもの)
https://teratail.com/questions/204077
[Rails]部分テンプレートを呼び出したい → render ‘ファイル名’
https://pikawaka.com/rails/render
[Rails]Destroyアクションとセキュリティ
https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#sec-the_destroy_action
[Rails]newとbuildの違い
newメソッドとbuildメソッドはともにインスタンスを生成するが、
buildは自動的にuser_idをセットしてインスタンスを生成することができる。
https://qiita.com/Kaisyou/items/8876f39e12631f4e5154
[Rails]CRUD(Create)
https://railstutorial.jp/chapters/sign_up?version=5.1#sec-signup_form
[Rails]CRUD(Read)
https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#sec-showing_all_users
[Rails]CRUD(Destroy)
https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#sec-the_destroy_actionURL,ルーティング系
[Rails]localhost3000/の状態でトップページを表示したい(3) →root
https://railstutorial.jp/chapters/static_pages?version=5.1#sec-setting_the_root_route[Rails]RESTfulなルーティング
https://railstutorial.jp/chapters/sign_up?version=5.1#table-RESTful_users
https://qiita.com/NagaokaKenichi/items/0647c30ef596cedf4bf2[Rails]resourceを使ったRESTfulなルーティングにRESTful以外の新しいアクションを追加したい。
https://qiita.com/ebihara99999/items/37afb1486442e7c16a8a
https://railstutorial.jp/chapters/following_users?version=5.1#sec-stats_and_a_follow_formDB,ActiveRecord系
[Rails]カラム名を変更したい
https://qiita.com/libertyu/items/93acd8733e34b1d0a63c[Rails]シードを作る(サンプルユーザーをDBにたくさん作りたい)
https://qiita.com/takehanKosuke/items/79a66751fe95010ea5ee
https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#sec-sample_users[Rails]マイグレーションファイルを削除する
https://qiita.com/tanaka-t/items/cd6aa0526725e88f5024[Rails]ActiveRecordのSQLの可読性をあげたい → Scope
https://qiita.com/ngron/items/14a39ce62c9d30bf3ac3[Rails]複数テーブルにまたがる検索をしたい
https://qiita.com/leon-joel/items/f26556c9e56833983856
https://qiita.com/makitokezuka/items/f13b2e7bad77b5594911[Rails]インデックス
インデックスを作成することでテーブルとは別に検索用に最適化された状態で必要なデータだけがテーブルとは別に保存される。検索用に並び替えをしている上、インデックスを貼ったカラムだけを検索できるので高速検索が可能。デメリットは、テーブルとは別に検索用のテーブルが保存される関係上データの追加に時間がかかること。なぜなら、データを追加するときに2つのテーブルを追加しなければならないから。
https://railstutorial.jp/chapters/user_microposts?version=5.1#sec-a_micropost_model
https://www.dbonline.jp/sqlite/index/index1.html
https://ja.wikipedia.org/wiki/%E7%B4%A2%E5%BC%95_%28%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%29[Rails]親を削除した時に子も削除できることをしたい dependent: :destroy
https://railstutorial.jp/chapters/user_microposts?version=5.1#sec-dependent_destroy[Rails]リレーションの時の別テーブルのデータの呼び出し方
https://railstutorial.jp/chapters/user_microposts?version=5.1#sec-destroying_microposts[Rails]あるユーザーが同じユーザーを2回以上フォローすることを防ぐこと- 複合キーインデックス
https://railstutorial.jp/chapters/following_users?version=5.1#sec-a_problem_with_the_data_modelセキュリティ系
[Rails]ストロングパラメーター(Strong parameters)
https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#sec-revisiting_strong_parameters
[Rails]マスアサインメント
→リクエストのデータをそのままデータベースに保存すること(セキュリティ的に弱い)
https://thinkit.co.jp/story/2015/09/03/6389[Rails]作業を元に戻したい!
rails destroy controller ~
https://railstutorial.jp/chapters/static_pages?version=5.1#sec-generated_static_pages[Rails]アカウントの有効化
https://railstutorial.jp/chapters/account_activation?version=5.1#cha-account_activation[Rails]SSL
ローカルのサーバーからネットワークに流れる前に、大事な情報を暗号化する技術
https://railstutorial.jp/chapters/sign_up?version=5.1#sec-ssl_in_production[Rails]CSRF対策[Controller]
protect_from_forgery with: :exception
https://qiita.com/tanaka7014/items/5b4e2204dc6bec83c90e[Rails]プレースホルダー
https://wa3.i-3-i.info/word118.html[Rails]パスワードをハッシュ化,暗号化して保存したい
https://railstutorial.jp/chapters/modeling_users?version=5.1#sec-adding_a_secure_passwordエラー系
[Rails]エラーメッセージをブラウザ画面に表示したい
https://railstutorial.jp/chapters/basic_login?version=5.1#sec-rendering_with_a_flash_message
https://railstutorial.jp/chapters/sign_up?version=5.1#sec-signup_error_messages
https://railstutorial.jp/chapters/sign_up?version=5.1#sec-the_flashタイミング系
[Rails]Emailアドレスをデータベースに保存する際に事前に小文字にしておきたい - コールバック
https://qiita.com/okamoto_ryo/items/458097542e826623b7ad
https://railstutorial.jp/chapters/account_activation?version=5.1#sec-activation_token_callback
コールバック。オブジェクトが生成(create)、更新(update)、破壊(delete)される時や、バリデーションを実行する時の前後に共通の処理を追加する仕組みのことを指す
before_save { self.email = email.downcase }
ブロックを渡してユーザーのメールアドレスを設定します
before_create :create_activation_digest
上のコードはメソッド参照と呼ばれるもので、こうするとRailsはcreate_activation_digestというメソッドを探し、ユーザーを作成する前に実行するようになります認可、認証、権限関連
[Rails]ログイン済みのユーザーだけにページを表示させたい
Controller
before_action :logged_in_user, only: [:edit, :update]
https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#sec-authorization[Rails]自分だけを編集したい
https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#sec-requiring_the_right_user[Rails]ユーザーがログインした後、ログイン直前に閲覧していたページヘとリダイレクトさせる機能
→ログインユーザー専用のページのURLにアクセスしたい→ログインする→さっき見てたログイン専用のページに行ける
フレンドリーフォワーディング request.url
https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#sec-friendly_forwarding
[Rails]一つ前のURLを返したい(リダイレクトしたい) request.referrer
https://railstutorial.jp/chapters/user_microposts?version=5.1#sec-destroying_microposts
[Rails]認可と認証の違い
https://qiita.com/kaysquare1231/items/c4e4736f2a924b03777bDRYに書きたい(可読性、保守性)
[Rails]似たようなコードを1つにまとめたい(パーシャル)
https://railstutorial.jp/chapters/filling_in_the_layout?version=5.1#sec-partials[Rails]DRYなコードを書きたいが、一部分が違う場合
https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#sec-edit_form[Rails]タイトルの可変要素をDRYする
https://railstutorial.jp/chapters/static_pages?version=5.1#sec-layouts_and_embedded_ruby
https://qiita.com/shumpeism/items/a0ad5930fa3bc0d24c70[Rails]privateメソッドをcontrollerに定義したが、とても汎用性があるので異なるControllerでも定義したメソッドを使いたい(継承→application_controller.rb)
https://railstutorial.jp/chapters/user_microposts?version=5.1#sec-micropost_access_control[Rails]記法一覧
https://qiita.com/gakkie/items/3afcd505c786364aa5fa
https://blog.mothule.com/ruby/ruby-percent-syntax[Ruby]記法一覧
https://railstutorial.jp/chapters/rails_flavored_ruby?version=5.1#cha-rails_flavored_ruby[Rails]キーワード引数とオプション引数の違い
https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#sec-users_index
- 投稿日:2020-07-26T01:34:19+09:00
Ruby + SinatraでLINE Botを作ろう - Part 2
こんにちは。
この連載では、複数回に渡りRubyとSinatraを使って
- 本の裏にあるISBNコードを送信すると検索して本の画像を表示してくれる
- その本を記録しておいて、あとから参照できる
という機能を搭載した「ほんめも!」というLINE Botを作ってみたいと思います。
この記事はPart 1の続編です。まだ読んでいない方はぜひ読んでみてください!
0. この記事で作るもの
この記事では、実際に書籍の情報を返してくれるAPIを利用して検索結果を返信するというプログラムを書いていきます。
プログラムの流れは下記の通りです。0_1. 書籍検索APIの紹介
今回は「openBD」を利用します。openBDは株式会社カーリル及び版元ドットコムによって運営されているAPIで、書籍の基本的な情報や書影(表紙の写真)等が取得出来ます。
https://api.openbd.jp/v1/get?isbn=9784873113944
といった感じでisbnを渡すと、書籍データの配列がJSON形式で返ってきます。カンマで区切ることで複数の書籍を指定することも出来ます。
詳しいAPIの仕様はOpenBD 書誌APIデータ仕様 (v1)に記載されています。基本的にはsummaryの中を見れば良いです。
0_2. 豆知識: ISBNって何?
ISBNとは国際標準図書番号の略で、任意の図書に一意に付けられる10桁あるいは13桁の数字です。ほとんどの書籍の裏表紙に記載されていて、バーコードにもなっています。古い本であれば奥付にのみ記載されている場合もあります。
書籍に2段のバーコードがあるのは、ISBNコードと書籍JANコード(商品コード)で分かれてるからなんです。1. openBD APIを呼び出す
以下のように取得する関数を作ります(コメントは移さなくて大丈夫ですよ)。
def getBookByISBN(isbn) return nil if !isbn.match(/^(\d{10}|978\d{10})$/) # ISBNの形式じゃなかったらnilを返す uri = URI.parse("https://api.openbd.jp/v1/get?isbn=" + isbn) res = Net::HTTP.get_response(uri) # APIを呼び出す return nil if res.code != "200" # エラーが発生したらnilを返す books = JSON.parse(res.body) # APIの結果をJSON形式として読み出す return nil if books.length == 0 # 該当した書籍が0件の場合はnilを返す return books[0] # APIの結果のうち1件目を返す end2. 書籍検索の結果をWebで表示してみよう
前回作った
get '/' do "Hello wolrd!" endを流用しましょう。
今回は、/?isbn=978xxxxxxxxxx
にアクセスされたら書籍名を返すという仕様にしてみたいと思います。get '/' do book = getBookByISBN(params['isbn']) return book['summary']['title'] endさて、ここまで書けたら
http://localhost:{PORT}/?isbn={好きな本のISBN}
アクセスしてみましょう。
本の名前が出てきたら成功です!
3. LINE Botに書籍の名前を返す機能をつけよう
前回書いたLINE Botのコードを思い出してみましょう。
post '/callback' do body = request.body.read signature = request.env['HTTP_X_LINE_SIGNATURE'] unless client.validate_signature(body, signature) error 400 do 'Bad Request' end end events = client.parse_events_from(body) events.each do |event| if event.is_a?(Line::Bot::Event::Message) if event.type === Line::Bot::Event::MessageType::Text message = { type: 'text', text: event.message['text'] } client.reply_message(event['replyToken'], message) end end end "OK" endこの中でも特に重要なのは
message = { type: 'text', text: event.message['text'] } client.reply_message(event['replyToken'], message)の部分です。
ここは、ユーザーから「テキストのメッセージ」が届いたときにする処理です。
event.message['text']
で送信されたテキストデータを取得することができます。
client.reply_message(event['replyToken'], message)
で返信を送信することができます。このコードを少し改良して書籍を検索出来るようにしてみましょう。
if event.type === Line::Bot::Event::MessageType::Text
からend
までの中身を以下のように書き換えてください。book = getBookByISBN(event.message['text']) # ISBNを検索して書籍情報を変数に代入する messages = [] # 返信するメッセージ用の変数 if book.nil? # 書籍が空だった場合 messages.push({ type: 'text', text: '書籍が見つかりませんでした' }) else # 書籍が見つかった場合 messages.push({ type: 'text', text: book['summary']['title'] }) end client.reply_message(event['replyToken'], messages)今まで、client.reply_messageにはmessageという
type
とtext
が入ったオブジェクトを渡していましたが、今回はmessageオブジェクトが入った配列を渡すようにしてみました。
こうすることによって、複数のメッセージを返信出来るようになります。次の章で登場する表紙画像を返信する機能を作る時に使用します!3_1. デプロイしよう
さて、LINE Botの機能追加が完了しました!
早速試してみたい所ですが、前回にも解説した通りデプロイしないとテストが出来ません。
下記コマンドを入力してデプロイしてみましょう。$ git add -A $ git commit -m "add search book" $ git push heroku masterデプロイが完了したら、LINE BotにISBNを送信してみましょう。
下のスクリーンショットの様に書籍名が返ってきたら成功です!4. LINE Botに書籍の画像を返す機能をつけよう
続いて、書籍の表紙画像を返す機能を付けていきましょう。
表紙画像は
book['summary']['cover']
で取得することができます。
ただし、全ての書籍に画像がある訳ではないので、存在するかチェックする必要があります。というわけで、下記の様に書いてみましょう。
book = getBookByISBN(event.message['text']) # ISBNを検索して書籍情報を変数に代入する messages = [] # 返信するメッセージ用の変数 if book.nil? # 書籍が空だった場合 messages.push({ type: 'text', text: '書籍が見つかりませんでした' }) else # 書籍が見つかった場合 if !book['summary']['cover'].empty? # 表紙画像があった場合 messages.push({ type: 'image', originalContentUrl: book['summary']['cover'], previewImageUrl: book['summary']['cover'], }) end messages.push({ type: 'text', text: book['summary']['title'] }) end client.reply_message(event['replyToken'], messages)4_1. デプロイしよう
これで表紙画像の返信機能は完成です!
下記コマンドを入力してデプロイしてみましょう。$ git add -A $ git commit -m "add cover image" $ git push heroku masterデプロイが完了したら、LINE BotにISBNを送信してみましょう。
下のスクリーンショットの様に表紙画像と書籍名が返ってきたら成功です!4_2. 余談
簡単なエラーチェックもしているので、存在しないISBNを送信すると、きちんとエラーが返ってきます。
また、表紙画像が無い書籍の場合はこのようにタイトルのみが返ってくるようになっているはずです。
5. まとめ
この記事では送られてきたISBNを元に書籍のタイトルと画像を返すBotを作りました!
複数のメッセージを送信する所など、Bot作りに便利な部分を含んでいるので、覚えておくと便利だと思います!連載記事一覧
- Ruby + SinatraでLINE Botを作ろう - Part 1 - オウム返しするBotを作ろう
- Ruby + SinatraでLINE Botを作ろう - Part 2 - 書籍を検索して情報を返そう ← イマココ
- Ruby + SinatraでLINE Botを作ろう - Part 3 - 検索した書籍を記録しよう(予定)
- 投稿日:2020-07-26T00:18:08+09:00
オブジェクトとクラスを理解する
irb
対話的な実行環境
オブジェクト型を確認する
"文字列"(レシーバ).class → String
1.class → Integerobject_idを確認する
"文字列".object_id → 実行されるたび別のオブジェクトが作られる
1.object_id → 同じ数値オブジェクトが提供されるオブジェクトを結合する
message1 = "メッセージ1"
message2 = "メッセージ2"
message1.concat(message2) ※括弧の省略可
message1 = メッセージ1メッセージ2文字列オブジェクト
人間が読むことのできる文字や記号で構成された、単語や文章のようなデータ
ダブルクォーテーションで内容を囲む(シングルクォーテーションでも可)数値オブジェクト
数を表すオブジェクト
クラスとインスタンス
オブジェクトがどんな機能持てるかは、そのオブジェクトがどんなクラスのオブジェクト出るかで変わってくる。Rubyが標準的に用意しているクラス(組み込みライブラリ・標準添付ライブラリ)の他に、自分でクラスを作ることも可能。
変数
abc = "氏名"
→ String命名方法
スネークケース
sample_name
キャメルケース
sampleMessage
※大文字から始まる名前は、保持する値が不変の「定数」と解釈されるコメント
「#コメントアウト」
メソッド
Rubyのオブジェクトの振る舞いは、メソッドとする。
「犬(クラス)のムギ(インスタンス)は人間に嘘をつく(メソッド)能力を持っている」
=ムギ.嘘をつく(人間)
※嘘をつく = インスタンスメソッド犬クラスに追いかけるを定義し、ムギだけでなくどの犬オブジェクトに対しても「嘘をつく」メソッドを呼び出すことができる。
class 犬 def 嘘をつく(人間) puts "犬は#{人間}に嘘をついた..." end end ムギ = 犬.newインスタンス変数
オブジェクトが抱える変数。オブジェクトのどのメソッド内からも利用できる。名前の先頭には必ず@をつける。
class Sample def samplemethod1 @number = 100 #インスタンス変数 end def samplemethod2 @number end end #object = Sample.new #object.samplemethod1もobject.samplemethod2 も呼び出し可能ローカル変数
その場限りの一時的な変数。メソッド内で定義したローカル変数はそのメソッド内でしか使うことができない。
class Sample def samplemethod1 number = 100 #ローカル変数 end def samplemethod2 number end end #object = Sample.new #object.samplemethod1は呼び出せるが、object.samplemethod2 は呼び出せないゲッター・セッター
class User def name=(name) #セッター @name = name end def name #ゲッター @name end end #↓簡単な書き方↓ class User attr_accessor :name, :address, :email end
- 投稿日:2020-07-26T00:18:08+09:00
Ruby入門
irb
対話的な実行環境
オブジェクト型を確認する
"文字列"(レシーバ).class → String
1.class → Integerobject_idを確認する
"文字列".object_id → 実行されるたび別のオブジェクトが作られる
1.object_id → 同じ数値オブジェクトが提供されるオブジェクトを結合する
message1 = "メッセージ1"
message2 = "メッセージ2"
message1.concat(message2) ※括弧の省略可
message1 = メッセージ1メッセージ2文字列オブジェクト
人間が読むことのできる文字や記号で構成された、単語や文章のようなデータ
ダブルクォーテーションで内容を囲む(シングルクォーテーションでも可)数値オブジェクト
数を表すオブジェクト
クラスとインスタンス
オブジェクトがどんな機能持てるかは、そのオブジェクトがどんなクラスのオブジェクト出るかで変わってくる。Rubyが標準的に用意しているクラス(組み込みライブラリ・標準添付ライブラリ)の他に、自分でクラスを作ることも可能。
変数
abc = "氏名"
→ String命名方法
スネークケース
sample_name
キャメルケース
sampleMessage
※大文字から始まる名前は、保持する値が不変の「定数」と解釈されるコメント
「#コメントアウト」
メソッド
Rubyのオブジェクトの振る舞いは、メソッドとする。
「犬(クラス)のムギ(インスタンス)は人間に嘘をつく(メソッド)能力を持っている」
=ムギ.嘘をつく(人間)
※嘘をつく = インスタンスメソッド犬クラスに追いかけるを定義し、ムギだけでなくどの犬オブジェクトに対しても「嘘をつく」メソッドを呼び出すことができる。
class 犬 def 嘘をつく(人間) puts "犬は#{人間}に嘘をついた..." end end ムギ = 犬.newインスタンス変数
オブジェクトが抱える変数。オブジェクトのどのメソッド内からも利用できる。名前の先頭には必ず@をつける。
class Sample def samplemethod1 @number = 100 #インスタンス変数 end def samplemethod2 @number end end #object = Sample.new #object.samplemethod1もobject.samplemethod2 も呼び出し可能ローカル変数
その場限りの一時的な変数。メソッド内で定義したローカル変数はそのメソッド内でしか使うことができない。
class Sample def samplemethod1 number = 100 #ローカル変数 end def samplemethod2 number end end #object = Sample.new #object.samplemethod1は呼び出せるが、object.samplemethod2 は呼び出せないゲッター・セッター
class User def name=(name) #セッター @name = name end def name #ゲッター @name end end #↓簡単な書き方↓ class User attr_accessor :name, :address, :email end演算子
Left align Left align + 足す、文字列の連結、配列の連結 - 引く、配列から一部の要素を削除 * かける、文字列を繰り返し連結、配列を繰り返し連結 / 割る % 割った余りを得る && / and AND演算 ^ XOR演算 ! / not 真偽を裏返す(否定) = 代入 == 等しいか調べる != 等しくないか調べる >, >=, <, <= 左辺が大きい、左辺が右辺以上、右辺が大きい、右辺が左辺以上 nil
空っぽの状態
sample = nil?
nilであるかを調べる真偽
Rubyではnilとfalseが偽、それ以外が真(0も真)。
条件分岐
number = 1 if number == 1 puts '数値は1' elseif number == 2 puts '数値は2' else puts '数値は1と2以外' end#unlessを用いた表現 age = 20 unless age >= 20 puts "投票権がない” end#ifを用いた表現 age = 20 if age < 20 puts "投票権がない” end後置if
puts "これは出力される" if true
puts "これは出力されない" if false配列
複数の要素が順番に格納された構造
array = ["123", false, nil, 1, [a,b,c]]
要素を追加は
array << aarray = [1,2,3] array.each do |element| puts element endclass User attr_accessor end user1 = User.new user1.name = 'mayu' user2 = User.new user2.name = 'ayako' user3 = User.new user3.name = 'kenji' users = [user1, user2, user3] #1つずつ取得したい場合、、 #方法①eachを使う names = [] users.each do |user| names << user.name end p names #==>["mayu", "ayako", "kenji"] #方法②mapを使う names = users.map do |user| user.name end #==>["mayu", "ayako", "kenji"] #方法②の省略① names = users.map{ |user| user.name } #==>["mayu", "ayako", "kenji"] #方法②の省略② names = users.map(&:name) #==>["mayu", "ayako", "kenji"]ハッシュ
内部的にデータをキーと対応づけて格納しておくデータ構造
様々な記法
#文字列をキーとする {"student1" => mayu, "student2" => asami} #文字列をキーとし、ハッシュロケットではなくコロンを使用 {"student1": mayu, "student2": asami} #シンボルをキーとする {:student1 => mayu, student2 => asami} #シンボルをキーと,ハッシュロケットではなくコロンを使用 ※一般的 {student1: mayu, student2: asami}値を取得
array = {:student1 => mayu, student2 => asami} puts array[:student1] #mayuと出力される値を更新
array = {:student1 => mayu, student2 => asami} array[:student1] = 'misaki' puts array[:student1] #misakiと出力される