20200726のRubyに関する記事は22件です。

Gitに管理されたファイルを削除したい

.gitignoreに書き忘れてcommitしてしまった時に、
「.gitignoreに追記すれば大丈夫...」
と誤解していませんか?
僕のように..汗

後から.gitignoreに追記するだけではすでにGitに管理されたファイルは除去されないので、そんな時の対処方法を書いておきます。

Gitに管理されたファイルを残して除外したい場合

$ git rm --cached [除外したいファイル名]

その後は必ず.gitignoreに除外したいファイルを追記する。
注意箇所:
--cachedを必ずつける!!
つけないとファイルごと削除してしまいます。

ファイルごと削除したい場合

$ git rm [削除したいファイル名]

ディレクトリごと削除したい場合

$ git rm -r [削除したいディレクトリ]

最後に

Gitを使い始めた時に何回か忘れてcommitして、都度調べていたので備忘録として残しておきます。

同じ境遇になった方のお役に立てば嬉しいです。

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

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 s

http://localhost:3000 にアクセスして馴染みの画像が出ていればOK。

スクリーンショット 2020-07-26 21.55.06.png

modelとcontrollerを作成

今回はpostという名前で。

$ bin/rails g model post name:string 
$ bin/rails db:migrate
$ bin/rails g controller posts

ルーティング

config/routes.rb
Rails.application.routes.draw do
  resources :posts
end

Active 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.rb
class Post < ApplicationRecord
  has_many_attached :images
end

controllerを記述

controllers/posts_controller.rb
class 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
end

viewを作成

投稿一覧ページ

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 にアクセス

新規投稿から画像を複数投稿する事ができた。

スクリーンショット 2020-07-26 20.51.02.png

任意の画像を削除する

controllerにメソッド追記

編集に必要なedit、updateメソッドを追記します。

controllers/posts_controllers.rb
  def 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-26 21.35.14.png

任意の画像だけ削除できました。

スクリーンショット 2020-07-26 21.36.05.png

参考

ActiveStorageを使って複数画像管理をしてみる
ActiveStorage で画像を複数枚削除する方法

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

【スクレイピングまとめ】| Python Node.js PHP Ruby Go VBA | 6種類の言語でヤフートップをスクレイピング

Python

動画

IMAGE ALT TEXT HERE

リポジトリ

https://github.com/yuzuru-program/scraping-python-yahoo

ソース

index.py
import 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

動画

IMAGE ALT TEXT HERE

リポジトリ

https://github.com/yuzuru-program/scraping-node-yahoo

ソース

package.json
{
  "dependencies": {
    "cheerio": "^1.0.0-rc.3",
    "node-fetch": "^2.6.0"
  }
}
index.js
const 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

動画

IMAGE ALT TEXT HERE

リポジトリ

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

動画

IMAGE ALT TEXT HERE

リポジトリ

https://github.com/yuzuru-program/scraping-ruby-yahoo

ソース

index.rb
require "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
end

Go

動画

IMAGE ALT TEXT HERE

リポジトリ

https://github.com/yuzuru-program/scraping-go-yahoo

ソース

index.go
package 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

動画

IMAGE ALT TEXT HERE

ソース

'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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ポートフォリオ を作成される方へ】Dockerでbinding.pryを使う方法

ポートフォリオを作成中、dockerとCircleCIを使ってHerokuにデプロイするためにこちらの記事を参考に作っていました。
そこにbinding.pryを使いたいと思った時の導入の方法をお伝えしようと思います。

docker-compose.yml
version: '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.yml
version: '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

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

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で二箇所に出したいときは,どっちがどっちかわからなくなりますね.その辺り,自動で更新してくれるようにできんかな...

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

タスク管理 アプリケーション作成手順

1. アプリケーション作成準備

1-1. 作成するアプリケーションの内容を考える

機能     機能内容
一覧表示    全てのタスクを一覧表示する
詳細表示       一つのタスクの詳細内容を表示する
新規登録       新しいタスクをdbに登録する
編集         登録済みのタスクを編集し、dbを更新する
削除         登録済みのタスクをdbから削除

1-2. アプリケーション名を決め、雛形を作成

rails new アプリケーション名 [オプション]

#例)

rails new taskmanage -d postgresql

1-3. dbを作成する

アプリケーションを作成したディレクトリに移動しdbを作成する

rails db:create

1-4. サーバーを起動してみる

以下のコマンドを叩き、
http://localhost:3000」
にアクセスし、railsのデフォルトページが表示されるか確認

rails s

2. モデルを作成

モデルの構成要素

  • モデルに対応するRubyのクラス
    キャメルケース

  • モデルに対応するデータベースのテーブル
    モデルのクラス名を複数名にしたもの、スネークケース

2-1. モデルの属性を設計する

属性  属性名 データ型 nullを許可するか デフォルト値
名称   name string   許可しない   なし  
詳しい説明   description text   許可する   なし  

2-2. モデルの雛形を作成

rails g model [モデル名] [属性名:データ型 属性名:データ型 ...] [オプション]

#例)

rails g model Task name:string description:text

2-3. マイグレーションをし、dbにテーブルを追加

作成されたマイグレーションファイルを確認し、rails db:migrateを実行

3. コントローラーとビューの作成

rails g controller コントローラー名 [アクション名 アクション名...] [オプション]

#例)

rails g controller tasks index show new edit
Rails.application.routes.draw do
 root to: 'tasks#index'
 resources :tasks
end

3. 新規登録機能の実装

3-1. 一覧画面に新規登録ページへ遷移するリンクを追加

= link_to '新規登録', new_task_path

3-2. 新規登録画面のためのアクションを実装

tasks_controller.rb
def new
 @task = Task.new
end

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

3-4. 登録アクションの実装

createアクション = 「登録フォームから送られてきたデータをdbに保存し、一覧画面に遷移」

tasks_controller.rb
def 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)
 end

renderとRedirect_toの違い

render Redirect_to
アクションに続けてビューを表示させる アクションを処理した直後にビューを表示せず、別のURLに案内する

flashメッセージ

リダイレクト時に、次のリクエストに対してちょっとしたデータを伝える

 redirect_to tasks_url, notice:"タスク「#{task.name}」を登録しました。"
#タスク登録完了メッセージをしてからリダイレクトするという意味

flash.now[:alert] = "すぐにアラート"
#同じリクエスト内の場合は、flash.now

4. 一覧表示機能の実装

4-1. 新規登録画面のためのアクションを実装

tasks_controller.rb
def index
 @tasks = Task.all
end

4-2. 一覧ページで全てのタスクを表示

@tasks.each do |task|
 task.name
 task.created_at

5. 詳細表示機能の実装

5-1. 一覧ページから詳細ページへのリンクを追加

index.html.slim
link_to task.name, task_path(task)

5-2. 選択されたタスクを詳細表示アクションで取得

tasks_controller.rb
def show
 @task = Task.find(params[:id])
end

5-3. 選択されたタスクを詳細表示アクションで取得

show.html.slim
@task.id
@task.name
@task.created_at
@task.updated_at

6. 編集機能の実装

6-1. 一覧ページと詳細ページに編集ページへのリンクを追加

index.html.slim
link_to '編集', edit_task_path(task)
show.html.slim
link_to '編集', edit_task_path

6-2. editアクションとupdateアクションを定義づける

tasks_controller.rb
def 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}」を更新しました。"
end

6-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.rb
def destroy
 task = Task.find(params[:id])
 task.destroy
 redirect_to tasks_url, notice: "タスク「#{task.name}」を削除しました。"
end

参考|現場で使える Ruby on Rails 5速習実践ガイド

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

【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.yml
version: "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」で保存をして終了してください。
Dockerfile
FROM 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=utf8mb4
terminal
# 最後に 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」で保存をして終了してください。
Dockerfile
FROM 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.yml
default: &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-26 16.49.15.png

以上でサンプルサイトの環境構築終了です!

■ 終わりに

初心者の方だけでなく、経験者の方でもあっても、初めて触る技術の学習にはかなり苦戦すると思います。
そのため、先に「どういうサイトがどのように動いているか」ということを知るだけでも学習効率は上がるんじゃないかなーと思い、今回の作業に取り掛かりました。
まだ私も環境構築をしてすぐにこの記事を書いているので、この環境を使った学習は始めていませんが、実際にログを仕込んでみたりしながら体感的に学ぶ方が飲み込みは早いと考えるので、ガンガンこの環境とこのサイトを元に学習に取り組んでいこうと思います。

この記事は解説部分を完全に端折って、ただただ完成させることだけを目標に書き上げました!
そのため、しっかりと解説を確認したい方は、参考文献様の記事をご確認いただいて、照らし合わせながら進めてほしいと思います。

僕「本当に先人の方々はすげえや。」

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

【ポートフォリオ を作成される方へ】ransackで作る検索機能

ransackとは

検索機能を少ないコードで簡単に実装できるgemです。
設定も簡単でできることもたくさんあります。

導入方法

Gemfile
gem 'ransack'
$ bundle install

使い方

①検索パラメーターは ":q"
②Ransack版form_forは"search_form_for"
③検索結果は"resultメソッド"で取得できる

controller.rb
class ProductsController < ApplicationController
  def index
    @q = Product.ransack(params[:q])
    @products = @q.result(distinct: true)
  end
end
view.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 で終わる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Ruby] 標準入力を受け付ける際に ^H などの ASCII 制御文字を意図した通りに認識させる

結論

STDIN.gets ではなく Readline.readline を使おう

STDIN.gets

Ruby で標準入力を受け取る方法を調べると、多くの場合、以下の実装方法が出てきます。

stdin.rb
print '> '
text = STDIN.gets.strip
puts "You said #{text}!"
$ ruby stdin.rb
> hello
You said hello!

gets の他にも readreadlinereadlines があるようです。
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.rb
require '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 をご覧ください。

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

ランキング機能のつくりかた【Railsの内部結合と外部結合を理解する】

概要

この記事は、実際に私がBooQsで運用している「ランキング機能」を例にして、Railsのテーブル結合(内部結合と外部結合)を解説する記事です。

前提条件

Rails 5.1

【?‍♂️Before】テーブル結合を使わないランキング機能

私が開発しているBooQsという英単語学習サービスでは、ゲーミフィケーションを用いてユーザーの学習意欲を高めるために、英単語の解答数(userのもつanswer_historiesの数)によるユーザーのランキング機能を実装しています。

スクリーンショット 2020-07-25 12.00.25.png

しかし、以下のNewrelicのダッシュボードを見ていただくとわかるように、自分の書いたランキング機能の処理速度はお世辞にも良いものとは言えませんでした。

88397035-546c6a80-cdfe-11ea-86a2-84e98d80bd51.png

それではまず、自分が組んだ(下手くそな)コードから見ていきましょう。
恥ずかしながら、自分はテーブル結合をよく理解していなかったため、次のようなまどろっこしいコードでランキング機能を実装していました。

home_controller.rb
def 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.rb
class User < ApplicationRecord
  has_many :answer_histories, dependent: :destroy
end
models/answer_history.rb
class AnswerHistory < ApplicationRecord
  # 非ログインユーザーの解答記録は、user_idをnilで記録する。
  belongs_to :user, optional: true
end

View

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: '&#8592; &nbsp;前へ', next_label: '次へ &#8594;',
                        page_links: false %>

    </div>

  </div>

</div>

実際のランキングページは以下になります。

ランキングページ: https://www.booqs.net/home/user_ranking

【?‍♂️After】内部結合を使ったランキング機能

Beforeのテーブル結合を利用しない方法だと、user_idsを取得するためにクエリを発行し、さらにユーザーを取得するたびに、user = User.find(id)でクエリが発行されるため、処理が遅くなります。

そのため、知り合いのベテランの開発者の方に、下のような方法をオススメいただきました。

home_controller.rb
def 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つがありますね!

  1. answer_historiesのuser_id(外部キー)の数だけ山田さんと清水さんが増殖した。
  2. answer_historiesのuser_idに存在しない長谷川さんは排除された。
  3. user_idがnilであるanswer_historiesのidが2と7のレコードが排除された

注意:例ではanswer_historiesの主キー(id)の昇順でレコードを並べていますが、私の調べた限りでは、結合結果のレコードの順序については、特定のidを基準にした昇順・降順などはないようでした。もし私が間違っていたら誰かぜひ教えてください。)

 
 

このようなjoinsメソッドを利用したテーブル同士の結合方法は、【内部結合】と呼ばれます。

内部結合では、結合条件に合致しないレコードは排除されます。

内部結合の結合条件とは、結合先の外部キー = 結合元の主キーです。

BooQsの例で紹介しましょう。
たとえば、今回のUser.joins(:answer_histories)によって内部結合を行うと、次のようなSQLが発行されます。

「内部結合」で発行されるSQL
SELECT  "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種類のレコードが排除されていることがわかるはずです。

  1. answer_historiesの外部キー(user_id)に存在しないusersレコード(例:長谷川さんのレコードが排除されている)
  2. 外部キーが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における「左外部結合」のやり方.rb
User.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における「右外部結合」のやり方.rb
User.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のクエリは、次のようになっています。

ランキング順位で並べ替え時に発行されるSQL
SELECT  "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" でなくてはならないのでしょうか??

何卒ご教示いただければ幸いです...!?‍♂️

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

メソッドや定数を使った出力 学習メモ

学習メモです。

そのまま出力

puts "うにが好物です!"

メソッドを使って出力

class Favorite
  def self.food
    puts "うにが好物です!"
  end
end

Favorite.food

定数を使って出力

FAVORITE = "うにが好物です!"

puts FAVORITE

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

gem 'kaminari' でページネーション実装

準備

  • Gemfileに'kaminari'追加
gem 'kaminari'
  • bundle install

実装例

  • Itemモデルから1回の検索で50レコード取得する
Item.all.page(1).per(50)
  • 配列に対して実装する場合
1ページに対して10レコードずつ表示する  
Kaminari.paginate_array(array).page(params[1]).per(10)  
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby入門2

initialize

newメソッドでオブジェクトを生成するとき、そのオブジェクトのinitializeメソッドが実行される

class User
 attr_reader :name, :address, :email
 def initialize(name, address, email)
  @name = name
  @address = address
  @email = email
 end 
end

private

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」を使い、まとめてクラスに取り込んでもらうことができる。
Screen Shot 2020-07-26 at 12.21.19.png

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

例外捕捉

  1. 例外が発生するかもしれないコードをbeginのなかに記述
  2. その中で発生した例外への対応の仕方をrescueの中に記述
  3. さらに、例外が出た場合も出なかった場合も必ず行いたい後処理をensureののなかに記述
begin
 #例外が発生するかもしれないコード
rescue
 #例外に対応するコード
ensure
 #例外が発生してもしなくても必ず実行したいコード
end

nilガード

もし変数が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]

参考|現場で使える Ruby on Rails 5速習実践ガイド

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

【Rails】JavaScriptが絡むテスト

はじめに

Capybaraでのテストで、JavaScriptが作動する処理をする場合には少し設定を変える必要があります。
今回は簡単な例としてクリックするとテキストが変わる処理についてテストしてみます。

36a2e16cd12c7ebb3b25be633051a96a.gif
railsのバージョンは5.2.3を使用しています。

設定

まずは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.rb
require '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
end

jsを動作させたい場合は上述のように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

fcbc5bed345b38c37a9394684c68513a.png
うまくテストが通ってます。

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

メソッドや定数を使って出力 学習メモ

学習メモです。

そのまま出力

puts "うにが好物です!"

メソッドを使って出力

class Favorite
  def self.food
    puts "うにが好物です!"
  end
end

Favorite.food

定数を使って出力

favorite_food = "うにが好物です!"

puts favorite_food

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

同時に同一のレコードにアクセスして編集しがちなテーブルには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)  COMMIT

look_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した方に無事エラーが出ました。
スクリーンショット 2020-07-26 11.22.12.png

あとはこれまたRailsApiにある通りエラーハンドリングして対応しましょう。
対応の仕方としては色々考えられますね。

  • 2人が別々の属性を編集したのであればそのまま順番にupdateして同一の属性を編集した時は画面に表示してユーザーに確認
  • 自分よりも前の人がupdateした他の属性には影響が及ばない様にした上でupdateさせる

とか。この辺はデータがアプリ上でどの様に使用されているかによりけりと言ったところでしょうか。

最後に

僕は実務で利用したことないので「ウチではこうやってるよ!」的な共有がいただけると嬉しいです!

参考記事

Rails Guide
What is Optimistic Locking

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

【初心者】Rails6 でプロジェクトを立ち上げるまでとつまづいたところ

rubyのバージョン確認、インストール

$ ruby --version

rubyのバージョンは2.5.0以上であれば動きます。ただ、最新版の方がメソッド追加や処理が早くなっていたりします。今は2.7.1が安定版のようです。

#最新のrubyをインストール
$ rbenv install 2.7.1 

rails6でプロジェクトの立ち上げ 

$ 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-26 11.45.52.png

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

Rails Simple Calendarで記事投稿後、カレンダーに反映させたい。

Simple Calendarにて
記事投稿→カレンダー投稿画面(create)→カレンダーに反映という事を行いたいのですが、
現状、記事投稿は出来ていて、そこから記事投稿のタイトルデータを抜き出してカレンダー投稿画面にタイトルのみデータを表示取得させたいです。
540d264cf48615032c7353a06dffbdf6.png
※こちらの画面ではTitleが入力フォームになっていますがこのTitleを、記事のTitleを取得したいです。

試した事ですが
5336e6a17d83d2db786feb63021d995c.png

1、
- @tweets.each do |tweet|
= @tweet.title
を入力しましたが
d2cf2caa748b7b3ad991e29b02e302bb.png

このようなエラーが出ました。

blog(カレンダー)とtweet(記事)のアソシエーションとして
4c606c184b85049ae3185dc556a08022.png
025a6b6e9003a3707599aa75945c6d5e.png

はこのように組んでいます。

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
end

blog_controllerはこちらになります。

19b8126718b90974d1a4db94d7d11982.png

記事投稿の際に他のページだとビューの表示は出来ました。
52b32613dc3a1aa18170323ae8d61abe.png
db9032b4f568d364a6a228d3c5415f0b.png

ご回答いただければ幸いです。
未熟者ですがよろしくお願いします。

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

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_validations

View系

[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/render

Controller系

[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_action

URL,ルーティング系

[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_form

DB,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/c4e4736f2a924b03777b

DRYに書きたい(可読性、保守性)

[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

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

Ruby + SinatraでLINE Botを作ろう - Part 2

こんにちは。
この連載では、複数回に渡りRubyとSinatraを使って

  • 本の裏にあるISBNコードを送信すると検索して本の画像を表示してくれる
  • その本を記録しておいて、あとから参照できる

という機能を搭載した「ほんめも!」というLINE Botを作ってみたいと思います。

この記事はPart 1の続編です。まだ読んでいない方はぜひ読んでみてください!

0. この記事で作るもの

この記事では、実際に書籍の情報を返してくれるAPIを利用して検索結果を返信するというプログラムを書いていきます。
プログラムの流れは下記の通りです。

linebot.png

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件目を返す
end

2. 書籍検索の結果をWebで表示してみよう

前回作った

get '/' do
  "Hello wolrd!"
end

を流用しましょう。
今回は、/?isbn=978xxxxxxxxxxにアクセスされたら書籍名を返すという仕様にしてみたいと思います。

get '/' do
  book = getBookByISBN(params['isbn'])
  return book['summary']['title']
end

さて、ここまで書けたらhttp://localhost:{PORT}/?isbn={好きな本のISBN}アクセスしてみましょう。
本の名前が出てきたら成功です!
スクリーンショット 2020-07-26 0.22.12.png

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というtypetextが入ったオブジェクトを渡していましたが、今回はmessageオブジェクトが入った配列を渡すようにしてみました。
こうすることによって、複数のメッセージを返信出来るようになります。次の章で登場する表紙画像を返信する機能を作る時に使用します!

3_1. デプロイしよう

さて、LINE Botの機能追加が完了しました!
早速試してみたい所ですが、前回にも解説した通りデプロイしないとテストが出来ません。
下記コマンドを入力してデプロイしてみましょう。

$ git add -A
$ git commit -m "add search book"
$ git push heroku master

デプロイが完了したら、LINE BotにISBNを送信してみましょう。
下のスクリーンショットの様に書籍名が返ってきたら成功です!

IMG_0541.PNG

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を送信してみましょう。
下のスクリーンショットの様に表紙画像と書籍名が返ってきたら成功です!

IMG_0542.PNG

4_2. 余談

簡単なエラーチェックもしているので、存在しないISBNを送信すると、きちんとエラーが返ってきます。

image.png

また、表紙画像が無い書籍の場合はこのようにタイトルのみが返ってくるようになっているはずです。

image.png

5. まとめ

この記事では送られてきたISBNを元に書籍のタイトルと画像を返すBotを作りました!
複数のメッセージを送信する所など、Bot作りに便利な部分を含んでいるので、覚えておくと便利だと思います!

連載記事一覧

  1. Ruby + SinatraでLINE Botを作ろう - Part 1 - オウム返しするBotを作ろう
  2. Ruby + SinatraでLINE Botを作ろう - Part 2 - 書籍を検索して情報を返そう ← イマココ
  3. Ruby + SinatraでLINE Botを作ろう - Part 3 - 検索した書籍を記録しよう(予定)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オブジェクトとクラスを理解する

irb

対話的な実行環境

オブジェクト型を確認する

 "文字列"(レシーバ).class → String
 1.class → Integer

object_idを確認する

 "文字列".object_id → 実行されるたび別のオブジェクトが作られる
 1.object_id → 同じ数値オブジェクトが提供される

オブジェクトを結合する

 message1 = "メッセージ1"
 message2 = "メッセージ2"
 message1.concat(message2) ※括弧の省略可
 message1 = メッセージ1メッセージ2

文字列オブジェクト

人間が読むことのできる文字や記号で構成された、単語や文章のようなデータ
ダブルクォーテーションで内容を囲む(シングルクォーテーションでも可)

数値オブジェクト

数を表すオブジェクト

クラスとインスタンス

オブジェクトがどんな機能持てるかは、そのオブジェクトがどんなクラスのオブジェクト出るかで変わってくる。Rubyが標準的に用意しているクラス(組み込みライブラリ・標準添付ライブラリ)の他に、自分でクラスを作ることも可能。

Screen Shot 2020-07-25 at 22.42.22.png

Screen Shot 2020-07-26 at 0.15.32.png

変数

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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby入門

irb

対話的な実行環境

オブジェクト型を確認する

 "文字列"(レシーバ).class → String
 1.class → Integer

object_idを確認する

 "文字列".object_id → 実行されるたび別のオブジェクトが作られる
 1.object_id → 同じ数値オブジェクトが提供される

オブジェクトを結合する

 message1 = "メッセージ1"
 message2 = "メッセージ2"
 message1.concat(message2) ※括弧の省略可
 message1 = メッセージ1メッセージ2

文字列オブジェクト

人間が読むことのできる文字や記号で構成された、単語や文章のようなデータ
ダブルクォーテーションで内容を囲む(シングルクォーテーションでも可)

数値オブジェクト

数を表すオブジェクト

クラスとインスタンス

オブジェクトがどんな機能持てるかは、そのオブジェクトがどんなクラスのオブジェクト出るかで変わってくる。Rubyが標準的に用意しているクラス(組み込みライブラリ・標準添付ライブラリ)の他に、自分でクラスを作ることも可能。

Screen Shot 2020-07-25 at 22.42.22.png

Screen Shot 2020-07-26 at 0.15.32.png

変数

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

array = [1,2,3]
array.each do |element|
 puts element
end
class 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と出力される

参考|現場で使える Ruby on Rails 5速習実践ガイド

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