20191012のRubyに関する記事は27件です。

Rails、Slimでflashメッセージがうまくだせない

Slimで新規登録に成功したらフラッシュメッセージを表示してindexに戻りたいのに次のように書いたらエラーが出る。

classを書かずにやると素テキストで成功メッセージが出るのでコントローラー側は大丈夫なはず。

app/controllers/blogs_controller.rb
  def create
    @blog = Blog.create(blog_params)
    redirect_to blogs_path
    flash[:success] = 'add success'
  end
index.html.slim
= flash[:success], class: 'alert alert-primary'
h1 Blog articles

= link_to '新規登録', new_blog_path, class: 'btn btn-primary'

- @blogs.each do |blog|
  br
  = blog.id
  br
  = blog.title
  br
  = blog.article

error
syntax error, unexpected ':' ..._safe((flash[:success], class : 'alert alert-primary'))).to_... ... ^


いろいろぐぐり、試行錯誤した。
classの書き方が違っていた。
link_toのクラスの書き方が、「class: 'btn btn-primary'」だったから同じだと思っていたが違うみたい。

ネットでいろいろぐぐったけど、Slimでflashのclassの書き方の参考が見つけられなかった。
これでエラーが取れたけど、ずっとフラッシュの緑枠がある。

  .alert.alert-success
    = flash[:success]

h1 Blog articles

勘違いしていた。if文を入れないと常にフラッシュが出てしまう。
flashでるときだけ値をコントローラーから勝手に渡してくれるような気がしていた。
if文を書く。
これでOK。

- if flash[:success]
  .alert.alert-success
    = flash[:success]

h1 Blog articles

参考サイト
redirect_to使った時にBootstrap対応のフラッシュメッセージを表示させる - Qiita

railsでflashを使ってサクセス・エラーメッセージを表示する - Qiita

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

Gmailを用いた ActionMailer の使い方

はじめに

ActionMailerの実装は割と簡単にできたのですが、エラーコードが出ずにGmailの設定でかなりの時間を費やしてしまいました。
次にやるときには時間がかけずにすむようにまとめておきたいと思います。

コーディング

コマンド(自動生成)

rails g mailer UserMailer

create  app/mailers/user_mailer.rb
create  app/mailers/application_mailer.rb
invoke  erb
create    app/views/user_mailer
create    app/views/layouts/mailer.text.erb
create    app/views/layouts/mailer.html.erb
invoke  test_unit
create    test/mailers/user_mailer_test.rb
create    test/mailers/previews/user_mailer_preview.rb

Mailer

app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: "from@example.com"
  layout 'mailer'
end
app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  default from: '~~~@gmail.com'

  # ここのメソッド名をメール文面の view ファイルの名前にする。
  # user と url は呼び出し元の コントローラーのメソッドから渡される。
  def user_create(user, url)
    # @user, @url に値を入れて view に渡す。
    @user = user
    @url = url
    mail(to: "user.name", subject: "ユーザーを新規登録しました。")
  end

Controller

user_controller.rb
class UsersController < ApplicationController

  def index
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)

    if @user.save
      @url = request.url + "/" + @user.id.to_s
      NotificationMailer.user_create(@user, @url).deliver
      redirect_to(user_path(@user))
    else
      render :new
    end
  end

  def show
    @user = User.find(params[:id])
  end

  def edit
    @user = User.find(params[:id])
  end

  def update
    @user = User.find(params[:id])
    if @user.update(user_params)

      @user = current_user
      @url = request.url
      NotificationMailer.user_update(@user, @url).deliver
      redirect_to(user_path(@user))
    else
      render :new
    end
  end
end

View

メール本文のテンプレートはデフォルトで二種類の形式が提供されています。

views/user_create/user_create.html.erb
<h1>UserMailer#user_create</h1>
<p>
<%= @user %> 様
</p>
<p>
  <%= @url %>, find me in app/views/user_mailer/user_create.html.erb
</p>
views/user_create/user_create.text.erb
UserMailer#user_create
<%= @user %> 様
<%= @url %>, find me in app/views/user_mailer/user_create.text.erb

Gmail googleの設定(重要)

google 二段階認証プロセスの設定
https://myaccount.google.com/signinoptions/two-step-verification/enroll-welcome

google アプリパスワードの作成
https://security.google.com/settings/security/apppasswords

config/initializers/mail.rb
ActionMailer::Base.delivery_method = :smtp
ActionMailer::Base.smtp_settings = {
  # address のところは変更しないので注意。自分はここでハマりました。
  address: 'smtp.gmail.com', 
  domain: 'gmail.com',
  port: 587,
  # user_name は自分のメールアドレスを記載。
  user_name: 'Gmail のメールアドレス',
  # password は作成したアプリパスワードを記載。
  password: 'Gmail のパスワード',
  authentication: 'plain',
  enable_starttls_auto: true
}

まとめ

駆け出しのエンジニアですので、何か不備がありましたらご連絡をお願いします。

参考

https://railsguides.jp/action_mailer_basics.html
https://qiita.com/hirotakasasaki/items/ec2ca5c611ed69b5e85e
https://thr3a.hatenablog.com/entry/20171202/1512175059

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

文字列中の文字の出現回数を調べる(Ruby)

最近必要になったので備忘録程度に。

ioowrrrrnxibop

上記のような文字列中に文字が何回出現しているか調べます。

次のようなコードで実装できます。

str = "ioowrrrrnxibop"
count_hash = str.split("").each_with_object(Hash.new(0)){ |char,hash| hash[char] += 1 }
# count_hash : {"i"=>2, "o"=>3, "w"=>1, "r"=>4, "n"=>1, "x"=>1, "b"=>1, "p"=>1}
(2019/10/13) 0:51追記

コメントでご指摘いただきましたので追記します。

count_hash = str.split("").each_with_object(Hash.new(0)){ |char,hash| hash[char] += 1 }

は次のように書くこともできます。

# charsを用いた場合
count_hash = str.chars.each_with_object(Hash.new(0)){ |char,hash| hash[char] += 1 }

# each_charとwith_objectを用いた場合
count_hash = str.each_char.with_object(Hash.new(0)){ |char,hash| hash[char] += 1 }

出現回数順に並べた二次元配列を作成するときは下記のコードでできます。

array = count_hash.sort_by{ |key,value| [-value,key] }
# array : [["r", 4], ["o", 3], ["i", 2], ["b", 1], ["n", 1], ["p", 1], ["w", 1], ["x", 1]]

今回は出現回数で降順に、出現回数が同じ場合はアルファベット順で昇順に並んだ配列を作っています。

transposeすれば出現回数だけの配列を取り出せます。

new_array = array.transpose
# new_array[0] : ["r", "o", "i", "b", "n", "p", "w", "x"]
# new_array[1] : [4, 3, 2, 1, 1, 1, 1, 1]

参考記事

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

Rails ファイル添付機能 複数モデルでの共通化

はじめに

DRYの考え方を意識しはじめ、効率的なコーディングだと感じたので備忘録としてまとめます。

構成

UserモデルとPostモデルの両方にファイル添付機能を持たせたいとき、
Attachment_fileモデルに添付ファイルを格納して、
中間テーブルとしてUser_attachment_fileモデルとPost_attachment_fileモデルを設ける。

  • Attachment_fileモデル
  • Userモデル
  • User_attachment_fileモデル
  • Postモデル
  • Post_attachment_fileモデル

Gemfile

下記を書いて、bundle install

gem 'carrierwave'

コーディング

 Model

  • Attachment_fileモデル
  • Userモデル
  • User_attachment_fileモデル
  • Postモデル
  • Post_attachment_fileモデル
attachment_file.rb
mount_uploader :file, FileUploader
after_destroy :destory_intermediate

has_many :user_attachment_files
has_many :users, through: :user_attachment_files

has_many :post_attachment_files
has_many :posts, through: :post_attachment_files

def destory_intermediate
  user_attachment_files.destroy if user_attachment_files.present?
  post_attachment_files.destroy if post_attachment_files.present?
end
user.rb
class User < ActiveRecord::Base
  has_many :user_attachment_files, dependent: :destroy
  has_many :user_files, through: :user_attachment_files, dependent: :destroy
end
user_attachment_file.rb
class UserAttachmentFile < ActiveRecord::Base
  belongs_to :user
  belongs_to :attachment_file
end
post.rb
class Post < ActiveRecord::Base
  has_many :post_attachment_files, dependent: :destroy
  has_many :post_files, through: :post_attachment_files, dependent: :destroy
end
post_attachment_file.rb
class PostAttachmentFile < ActiveRecord::Base
  belongs_to :post
  belongs_to :attachment_file
end

Controller

  • Attachment_fileモデル

  • Attachment_fileモデル

attachment_files_controller.rb
class AttachmentFilesController < ApplicationController
  def create
    @attachment_file = AttachmentFile.create!(file_params)

    respond_to do |format|
      format.html
      format.json {render json: @attachment_file }
    end
  end

  def destroy
    @attachment_file = AttachmentFile.find(params[:id])
    if @attachment_file.destroy
      render :json => { result: 'success' }
    else
      render :json => { result: 'error' }
    end
  end

  private

  def file_params
    params.require(:attachment_file).permit(:id, :file)
  end
end

View

Dropzoneを使っています。ここでは詳細は割愛します。

(Dropzone.jsで複数ファイルアップロード後、削除したいファイルを指定したい:https://qiita.com/saekis/items/207379a056af73f143b7)

users/new.html.slim
tr
  th = label :user, :user_file
  td
    #user_file_uploader.dropzone                  / ポイント
    table.files
      - for file in @user.attachment_files do
        tr id="file_row_#{file.id}"
          td
            = file.original_filename
            = hidden_field_tag nil, file.id, name:'user[attachment_file_ids][]'
          td
            = link_to file.file_url, download: file.original_filename do
              = fa_icon "download"
              |ダウンロード
          td
            = link_to '#', onclick: "#", file_id: file.id, remote: true do
              |削除

Table

中間テーブル(User_attachment_file、Post_attachment_file)を持っているため、UserモデルとPostモデルではカラムを設ける必要はありません。

  • Userモデル
  • User_attachment_fileモデル
  • Postモデル
  • Post_attachment_fileモデル
  • Attachment_fileモデル
schema.rb
# Userモデル
create_table "users", force: :cascade, options: do |t|
  t.datetime "created_at",  null: false
  t.datetime "updated_at",  null: false
end

# User_attachment_fileモデル
create_table "user_attachment_files", force: :cascade, options: do |t|
  t.integer  "user_id",            limit: 4, null: false          # ポイント
  t.integer  "attachment_file_id", limit: 4, null: false          # ポイント
end

# Postモデル
create_table "posts", force: :cascade, options: do |t|
  t.datetime "created_at",  null: false
  t.datetime "updated_at",  null: false
end

# Post_attachment_fileモデル
create_table "post_attachment_files", force: :cascade, options: do |t|
  t.integer  "post_id",            limit: 4, null: false          # ポイント
  t.integer  "attachment_file_id", limit: 4, null: false          # ポイント
end

# Attachment_fileモデル
create_table "attachment_files", force: :cascade, options: do |t|
  t.string   "file",              limit: 255                      # ポイント
  t.string   "original_filename", limit: 255                      # ポイント
  t.datetime "created_at",                    null: false
  t.datetime "updated_at",                    null: false
end

まとめ

先輩のコードを見てこの方法を知りました。
人のコードを読むことの重要性を強く感じたので、コーディング → リファクタリング のサイクルを大切にしたいです。

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

可変クラスの作り方 Rails Slim

はじめに

可変クラスを作るテクニック(?)を忘れないようにまとめておきたいと思います。

可変クラスの作り方

slim

slimで書くことが多いので、slimでまとめます。

ポイントとしては下記の形で書くことです。

  • "固定クラス + 可変クラス"
ruby.html.slim
.btn.outline.active class="#{a[num]}"
/ 固定クラス: .btn.outline.active
/ 可変クラス: #{a[num]}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rootへのルーティング設定方法

目的

アプリの開発で仕様するrootメソッドの使い方をアウトプットを目的に備忘録的に残します。

ルーティング設定を行う

Railsアプリを作成、起動後「http://localhost:3000/」 にアクセスすると「public.index.html」に記載された内容が呼び出されています。
ただ、この状態はルートへアクセスしたときに用意したhtmlファイルを返しているだけです。

もしルートにアクセスした場合に特定のアクションを実行させたい場合、「root」メソッドを使ったルーティングを設定する必要があります。
そこで、ルーティングを設定して特定のアクションを実行し、viewを表示する方法を紹介していきます。

ルーティングの設定のため「config/routes.rb」ファイルを編集します。
indexアクションのビューをrootに設定されるように追記します。

config/routes.rb
root to: 'home#index'

まとめ

rootメソッドを定義することによって特定のアクションを実行することができます。

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

はじめてのPostgreSQL

この記事が対象とする読者

  • PostgreSQLをはじめて使う人

Homebrewを使ったインストールの仕方を簡潔にまとめます。

インストールの流れ

1. ターミナルからインストール

まずは、以下のコマンドを入力・実行します。

$ brew install postgresql

2. インストールの確認

正常にインストールできているかどうかを確認します。
バージョンを表す-Vは大文字なので注意です。

$ postgres -V
postgres (PostgreSQL) 11.5

3. PostgreSQLを起動

データベースとして使用するためには以下の起動のコマンドも必要です。

$ brew services start postgresql
==> Successfully started `postgresql` (label: homebrew.mxcl.postgresql)

ここまで完了すればデータベースが作成できるようになるはずです!

さいごに

PostgreSQL初心者による初心者のためのQiitaでした。
簡単な工程なのですが、検索しながらやっていたら迷ってしまったので共有しておきます。少しでも参考になればうれしいです。

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

GAE(FE)にDocker環境上のRuby on Railsをデプロイする

はじめに: Dockerで開発したら手軽にデプロイまでしたい。

Ruby on Railsの開発で今や主流のDocker環境。
ネット上に環境構築の情報は豊富に転がっているし、local開発は出来たし、Herokuにもデプロイできた。

でもやっぱりHerokuだとイマイチ速度が出ないし、
何よりAWSとかGCP使ってるほうがカッコイイ気がする!(小並感)

圧倒的に情報不足なDockerに乗っけたままAWSやGCPにデプロイをする方法。
今回は既にHerokuで運用中のwebサービスをGAE環境に移し替えたのでその際に行った手順と、躓いた点を書いていこうと思います。

自分自身も右往左往しながら、なんとかデプロイできたー!!動いたー!!
という感じなので、間違っている点や、改善点などが有りましたら是非色々とご教示いただけると助かります。

前提

  • 既にDocker+ docker-composeで環境構築済み、動作確認済みのRuby on Railsアプリケーションがある
  • GCPアカウント及びプロジェクトが作成済
  • Cloud SDKが導入済み

【参考】
Dockerを使ってRuby on Rails環境の構築をしてみる
https://qiita.com/me-654393/items/d11b871ce8d76e153b21

Cloud SDK のインストール
https://cloud.google.com/sdk/downloads?hl=ja

App Engineにアプリを作成する

まず最初に、ターミナルからGAEアプリを作成します。

$ gcloud app create
You are creating an app for project [project-name].
WARNING: Creating an App Engine application for a project is irreversible and the region
cannot be changed. More information about regions is at
<https://cloud.google.com/appengine/docs/locations>.

Please choose the region where you want your App Engine application
located:

 [1] asia-east2    (supports standard and flexible)
 [2] asia-northeast1 (supports standard and flexible)
 [3] asia-northeast2 (supports standard and flexible)
 [4] asia-south1   (supports standard and flexible)
 [5] australia-southeast1 (supports standard and flexible)
 [6] europe-west   (supports standard and flexible)
 [7] europe-west2  (supports standard and flexible)
 [8] europe-west3  (supports standard and flexible)
 [9] europe-west6  (supports standard and flexible)
 [10] northamerica-northeast1 (supports standard and flexible)
 [11] southamerica-east1 (supports standard and flexible)
 [12] us-central    (supports standard and flexible)
 [13] us-east1      (supports standard and flexible)
 [14] us-east4      (supports standard and flexible)
 [15] us-west2      (supports standard and flexible)
 [16] cancel
Please enter your numeric choice:  2

Creating App Engine application in project [project-name] and region [asia-northeast1]....done.
Success! The app is now created. Please use `gcloud app deploy` to deploy your first app.

asia-northeast1が東京リージョンですので、選択します。
※[project-name]となっている部分は各々作成したGCPプロジェクト名が入ります。

app.yamlを作成する

続いて、Dockerfileと同階層にapp.yamlを作成します。
app.yamlの説明に関してはここでは省略します。
【参考】https://cloud.google.com/appengine/docs/standard/go/config/appref?hl=ja

app.yaml
entrypoint: bundle exec rackup --port $PORT
env: flex
runtime: custom

env_variables:
  SECRET_KEY_BASE: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX(各々のsecret key baseを入れる)

SECRET_KEY_BASEの取得方法は、
コンソール上で、

$ docker-compose exec web bundle exec rails secret

表示される文字列をコピペすればOKです。

Railsサーバの起動ポートを8080に合わせる

GAEでは、8080番に対してアクセスするので、3000番などに設定している方は8080番に変更しましょう。

Dockerfile(例)
FROM ruby:2.6.3

RUN apt-get update -qq && \
  apt-get install -y build-essential \
  libpq-dev \
  nodejs

RUN mkdir /app_name
ENV APP_ROOT /app_name
WORKDIR $APP_ROOT

ADD ./Gemfile $APP_ROOT/Gemfile
ADD ./Gemfile.lock $APP_ROOT/Gemfile.lock

RUN bundle install
ADD . $APP_ROOT

# 8080番でポートを起動する
CMD /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 8080 -b '0.0.0.0'"

docker-compose.yml(例)
version: "3"
services:
  web:
    environment:
      TZ: Asia/Tokyo
    build: .
    volumes:
      - .:/app
    ports:
      - 8080:8080
    tty: true
    stdin_open: true

volumes:
  db-volume:

基本的な準備はここまで。

いざデプロイ!

さて、待ちに待ったデプロイ。この瞬間が一番ドキドキします。
ターミナルを開いて、

$ gcloud app deploy

descriptor:      [/project-name/app.yaml]
source:          [/projects/project-name]
target project:  [project-name]
target service:  [default]
target version:  [20190000t123456]
target url:      [https://project-name.appspot.com]
Do you want to continue (Y/n)?  Y(←上記の内容で問題なければ、Yを入力します。)

ビルドが始まり、しばらく待ち、

Updating service [default] (this may take several minutes)...done.
Setting traffic split for service [default]...done.
Deployed service [default] to [https://project-name.appspot.com]

You can stream logs from the command line by running:
  $ gcloud app logs tail -s default

To view your application in the web browser run:
  $ gcloud app browse

と、表示されれば成功。

$ gcloud app browse

で確認しましょう。

トラブルシューティング

「Switch to inspect mode.」と怒られる件について

Updating service [default] (this may take several minutes)...failed.
ERROR: (gcloud.app.deploy) Error Response: [9]
Application startup error:
Switch to inspect mode.

色々ググったけど今ひとつ情報源に当たれずにめちゃくちゃ困ってた。
8080番でRailsが起動しておらず、GAEがアクセスできていないってことだと思う。

原因はdocker-compose側でサーバーを起動していたこと。

docker-compose.yml(例)
version: "3"
services:
  web:
    build: .

    # ここがアウト
    command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 8080 -b '0.0.0.0'"

    volumes:
      - .:/app_name
    ports:
      - "8080:8080"
    links:
      - db

これだとこのエラーが出るみたい。
原因はいまいちよくわかっていないが、もしかしたらdocker-compose読んでいないのかも?

8080番ポートの下りで記載したDockerfileのように、

Dockerfile
CMD /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 8080 -b '0.0.0.0'"

で、8080を開いてあげるのがいいのかも。

【参考】
A weekend, a Rails app, a Kubernetes and an Azure.
https://medium.com/@michiels/a-weekend-a-rails-app-a-kubernetes-and-an-azure-d330b003d7c2

最後に:Herokuの方がわかりやすいけど、やっぱりGAE早い

今回は実行速度が気になったので、HerokuからGAEに乗り換えることを決意したわけですが、
予想通りGAEの方が速度が出ました。

左: Heroku
右: GAE
sp-heroku.jpg sp-gae.jpg

特にRailsの中身をいじること無く、20程度速度が速度が上昇しました。
改善案の部分でサーバー応答時間の短縮(TTFB)が表示されなくなったのがでかい気がします。

おまけ:宣伝

今回HerokuからGAEに移行したのはスポチューバーTVという、野球の技術指導メディアです。
スポーツ教育×Techの、主に野球の分野を伸ばしていけたらなと思っているので、興味のある方は是非御覧ください。
https://spotuber-tv.com/

参考

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

Sessionメソッドの使い方、宣言方法

目的

ログイン機能を実装するときにSessionメソッドを使用するが、
機能の流れをおさらいするために備忘録として残しておく。

セッションとは?

・主にログイン機能に使用されるもので、ログイン状態を持続させるためにある機能。
・ステートフルな通信を実現するために必要な機能。

セッションという仕組みがなけれはページを移動するたびにログインし直さなければならない。

ログイン機能を実装するためRailsではあらかじめセッションを実装するためのメソッドが用意している。
また、セッションの情報はRailsの標準では、ブラウザ側のクッキーに保存される。

Sessionメソッドの使い方

まずは、ユーザーを登録するためにユーザーモデルを準備する。
ユーザーモデルには、名前とメールとパスワードが登録できるようにします。

gemをGemfileに追加

パスワードを暗号化するための
(コメントアウトされているためコメントインする必要がある)

Gemfile
gem 'bcrypt', '~> 3.1.7'

コメントインが完了したらgemをインストールする必要がある。

bundle install

Userモデルを作成

rails g model Users name:string email:string password_digest:string 

password_digestというカラムにはパスワードが入ります。
こちらはgemであるbcryptの仕様ですが、このカラムにするとパスワードが暗号化されて保存されます。

migrationファイルが作成されたので、データベースに反映しましょう。

rails db:migrate

これでユーザーモデルの作成は完成です。

次に作成したユーザモデルにパスワードの暗号化を有効にするための設定を加えます。

app/models/user.rb
has_secure_password
Sessionコントローラーを作成

ログイン機能を作成するためセッションコントローラーを作成する。

rails g controller sessions
app/controllers/session_sontroller.rb
class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      session[:user_id] = user.id
      redirect_to user_path(user.id)
    else
      render 'new'
    end
  end
end

ログインボタンを押した後に、セッションに情報が保存されているか判断しています。

user = User.find_by(email: params[:session][:email].downcase)

params[:session][:email]で、ユーザのポストでした値を取り出し、メールアドレスには小文字しか入力できないため.downcaseで小文字に変換しています。
その後find_byメソッドで入力したemailをカラムに保存されたemailが一致した場合にユーザーに代入しています。

次に、

if user && user.authenticate(params[:session][:password])

&&を使って、条件を2つ指定しています。
user:userの中身があるかどうか
user.authenticate:userに代入されたレコードのパスワードがポストした値と一致しているか

パスワードが登録しているユーザー情報と一致していたらセッションにユーザーIDを登録しています。

session[:user_id] = user.id

session[:名前]は名前をつけて、セッションを登録できます。

また、elseの場合、

render 'new'

パスワードが登録しているユーザ情報と一致していなかった場合、入力フォームに再度飛ぶように設定されている。

これでセッションを作成することができました。

まとめ

セッションを作成する際に必要なものは以下である。

・パスワードを暗号化するためのgem:bcrypt
・gem:bcryptで暗号化したパスワードを保存するカラム:password_digest
・パスワードの暗号化を有効にするための設定:has_secure_password
・セッションを作成するためにメソッド:session[:名前] = params[:~]

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

MacでRailsにgem mysql2をインストールする時のトラブルシューティング

はじめに

2019年10月現在MacでRailsアプリ開発時にDBにMySQLを選択する場合、デフォルトではmysql2というgemをインストールします。
その時にいくつかエラーに出会ったので、まとめておきます。

実行環境

  • macOS Mojave v10.14.6
  • ruby v2.6.4
  • Rails v6.0.0

MySQLのクライアントが必要

MacにMySQLクライアントをインストールしていない場合、以下のようなエラーが表示されます。

Fetching mysql2 0.5.2
Installing mysql2 0.5.2 with native extensions
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    current directory: /Users/hogehoge/sample-app/vendor/bundle/gems/mysql2-0.5.2/ext/mysql2
/Users/hogehoge/.rbenv/versions/2.6.4/bin/ruby -I /Users/hogehoge/.rbenv/versions/2.6.4/lib/ruby/2.6.0 -r
./siteconf20191012-40688-181m253.rb extconf.rb
checking for rb_absint_size()... yes
checking for rb_absint_singlebit_p()... yes
checking for rb_wait_for_single_fd()... yes
checking for -lmysqlclient... no
-----
mysql client is missing. You may need to 'brew install mysql' or 'port install mysql', and try again.

Homebrewなどでインストールしておきましょう。
なお、2019年10月現在gemが対応しているバージョンは以下のようですので、採用バージョンに応じたものをインストールしましょう。

This gem is tested with the following MySQL and MariaDB versions:
MySQL 5.5, 5.6, 5.7, 8.0
MySQL Connector/C 6.0 and 6.1 (primarily on Windows)
MariaDB 5.5, 10.0, 10.1, 10.2, 10.3

(https://github.com/brianmario/mysql2 READMEより引用)

# MySQL最新バージョン(8.0)
$ brew install mysql

# 5.xの場合
$ brew install mysql@5.x

5.x系の場合はmysqlコマンドを使えるよう以下のようにPATHを通しておきましょう。

$ export PATH="/usr/local/opt/mysql@5.7/bin:$PATH"

bundle install時のlinkerエラー

無事MySQLクライアントをインストールできていても、以下のようなエラーが起こる場合があります。

Fetching mysql2 0.5.2
Installing mysql2 0.5.2 with native extensions
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    current directory: /Users/hogehoge/sample-app/vendor/bundle/gems/mysql2-0.5.2/ext/mysql2
/Users/hogehoge/.rbenv/versions/2.6.4/bin/ruby -I /Users/hogehoge/.rbenv/versions/2.6.4/lib/ruby/2.6.0 -r
./siteconf20191012-62886-155hohe.rb extconf.rb
checking for rb_absint_size()... yes
checking for rb_absint_singlebit_p()... yes
checking for rb_wait_for_single_fd()... yes
-----
Using mysql_config at /usr/local/bin/mysql_config
-----
checking for mysql.h... yes
checking for errmsg.h... yes
checking for SSL_MODE_DISABLED in mysql.h... yes
checking for SSL_MODE_PREFERRED in mysql.h... yes
checking for SSL_MODE_REQUIRED in mysql.h... yes
checking for SSL_MODE_VERIFY_CA in mysql.h... yes
checking for SSL_MODE_VERIFY_IDENTITY in mysql.h... yes
checking for MYSQL.net.vio in mysql.h... yes
checking for MYSQL.net.pvio in mysql.h... no
checking for MYSQL_ENABLE_CLEARTEXT_PLUGIN in mysql.h... yes
checking for SERVER_QUERY_NO_GOOD_INDEX_USED in mysql.h... yes
checking for SERVER_QUERY_NO_INDEX_USED in mysql.h... yes
checking for SERVER_QUERY_WAS_SLOW in mysql.h... yes
checking for MYSQL_OPTION_MULTI_STATEMENTS_ON in mysql.h... yes
checking for MYSQL_OPTION_MULTI_STATEMENTS_OFF in mysql.h... yes
checking for my_bool in mysql.h... no
-----
Dont know how to set rpath on your system, if MySQL libraries are not in path mysql2 may not load
-----
-----
Setting libpath to /usr/local/Cellar/mysql/8.0.17_1/lib
-----
creating Makefile

current directory: /Users/hogehoge/sample-app/vendor/bundle/gems/mysql2-0.5.2/ext/mysql2
make "DESTDIR=" clean

current directory: /Users/hogehoge/sample-app/vendor/bundle/gems/mysql2-0.5.2/ext/mysql2
make "DESTDIR="
compiling client.c
compiling infile.c
compiling mysql2_ext.c
compiling result.c
compiling statement.c
linking shared-object mysql2/mysql2.bundle
ld: library not found for -lssl
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [mysql2.bundle] Error 1

make failed, exit code 2

以下がこのエラーの原因で、ざっくり言うとgemのビルド時に、必要なopensslライブラリを見つけられないために発生しています。

ld: library not found for -lssl

詳細な原因は以下記事が大変参考になります。
https://qiita.com/HrsUed/items/ca2e0aee6a2402571cf6

解決策としては、bundle install時に以下オプションでpathを指定する必要があります。

  • --with-cppflags
  • --with-ldflags

brew infoでインストールされているopensslを確認すると、自分の環境のLDFLAGSとCPPFLAGSのpathが確認できます。

$ brew info openssl@1.1

openssl@1.1: stable 1.1.1d (bottled) [keg-only]
...
For compilers to find openssl@1.1 you may need to set:
  export LDFLAGS="-L/usr/local/opt/openssl@1.1/lib"
  export CPPFLAGS="-I/usr/local/opt/openssl@1.1/include"
...

こちらのpathをbundle configで設定してからインストールしましょう。

$ bundle config --local build.mysql2 "--with-cppflags=-I/usr/local/opt/openssl@1.1/include"
$ bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl@1.1/lib"
$ bundle install

以上、自分が遭遇したエラー内容でした。

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

Railsチュートリアル 第10章<復習>

第10章の復習メモです。
個人的に重要と思ったことを書きます。

前回と同様、以下3つの視点で書きます。

  • 分かったこと
  • 分からなかったこと
  • 今回はスルーしたこと

分かったこと

PATCHリクエストについて

ユーザの新規作成画面(app/views/users/new.html.erb)と、編集画面(app/views/users/edit.html.erb)は、画面項目が同じなため、フォームを部分テンプレートに共通化した。

app/views/users/_form.html.erb
<%= form_for(@user) do |f| %>
  <%= render 'shared/error_messages', object: @user %>

  <%= f.label :name %>
  <%= f.text_field :name, class: 'form-control' %>

  <%= f.label :email %>
  <%= f.email_field :email, class: 'form-control' %>

  <%= f.label :password %>
  <%= f.password_field :password, class: 'form-control' %>

  <%= f.label :password_confirmation %>
  <%= f.password_field :password_confirmation, class: 'form-control' %>

  <%= f.submit yield(:button_text), class: "btn btn-primary" %>
<% end %>

共通化により懸念されるのが、

  • リクエスト先URLの違い
  • リクエストのメソッドの違い

ルーティングについて、7章で以下のように設定した。
image.png

これらの違いがあるので、普通に考えたら、分岐の処理を入れる必要がある。しかし、これについてはRailsが上手くやってくれる

Railsは、form_for(@user)を使ってフォームを構成すると、@user.new_record?がtrueのときにはPOSTを、falseのときにはPATCHを使います。

DBへのデータ一括登録

画面やコンソールから一件ずつ登録せずとも、DBにデータを一括登録する方法がある。
手順は以下の通り。

  1. Gemfileにgemを追加する
  2. 登録したいデータを用意する
  3. DBに反映させる

手順1. Gemfileにgemを追加する

Faker gemを追加する

source 'https://rubygems.org'

gem 'rails',          '5.1.6'
gem 'bcrypt',         '3.1.12'
gem 'faker',          '1.7.3'  # ← これを追加
.
.
.

手順2. 登録したいデータを用意する

db/seeds.rbファイルに、登録したいデータを記載する

db/seeds.rb
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar")

99.times do |n|
  name  = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name:  name,
               email: email,
               password:              password,
               password_confirmation: password)
end

Example Userという名前とメールアドレスを持つ1人のユーザと、それらしい名前とメールアドレスを持つ99人のユーザーを作成している。

手順3. DBに反映させる

現在登録されているデータを消しておきたい場合、

$ rails db:migrate:reset

を実行する。

手順2をDBに反映させるには、

$ rails db:seed

を実行する。

ページネーション

一覧画面で、表示件数を区切って出力できる。手順は以下の通り。

  1. Gemfileにgemを追加する
  2. ビュー、コントローラを編集する

手順1. Gemfileにgemを追加する

will_paginate gembootstrap-will_paginate gemを追加する

source 'https://rubygems.org'

gem 'rails',                   '5.1.6'
gem 'bcrypt',                  '3.1.12'
gem 'faker',                   '1.7.3'
gem 'will_paginate',           '3.1.6'  # ← これを追加
gem 'bootstrap-will_paginate', '1.0.0'  # ← これを追加
.
.
.

手順2. ビュー、コントローラを編集する

ビューに処理を追記

app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">         # ← これを追加
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 50 %>
      <%= link_to user.name, user %>
    </li>
  <% end %>
</ul>

<%= will_paginate %>       # ← これを追加

Railsチュートリアルの説明を引用

このwill_paginateメソッドは少々不思議なことに、usersビューのコードの中から@usersオブジェクトを自動的に見つけ出し、それから他のページにアクセスするためのページネーションリンクを作成しています。

コントローラの処理を変更

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]
  .
  .
  .
  def index
    @users = User.paginate(page: params[:page])  # ← ここを変更
  end
  .
  .
  .
end

Railsチュートリアルの説明を引用

paginateでは、キーが:pageで値がページ番号のハッシュを引数に取ります。User.paginateは、:pageパラメーターに基いて、データベースからひとかたまりのデータ (デフォルトでは30) を取り出します。
paginateを使うことで、サンプルアプリケーションのユーザーのページネーションを行えるようになります。具体的には、indexアクション内のallをpaginateメソッドに置き換えます。ここで:pageパラメーターにはparams[:page]が使われていますが、これはwill_paginateによって自動的に生成されます。

モデルの論理値属性

モデルの属性に、論理値(boolean)を設定できる。

migrationファイルの作成

$ rails generate migration add_admin_to_users admin:boolean

migrationファイルの編集

default: falseを追加することで、全レコードfalse(0)で登録できる。

db/migrate/[timestamp]_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :admin, :boolean, default: false
  end
end

属性の状態を確認

<インスタンス>.<属性>?メソッドで、属性の状態を確認できる。

$ rails console --sandbox
>> user = User.first
>> user.admin?
=> false

分からなかったこと、今回はスルーしたこと

  • Strong Parameters
  • テスト全般
  • アプリケーションの仕様、ロジックの詳細
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【53日目】掲示板にタグを設定しよう!② タグ検索機能の実装、

タグ検索の実装

昨日の続きとなります。
タグの初期データを用意したり、個別の掲示板にタグを設定して保存したり、掲示板詳細画面に関連づけられたタグを表示する機能については【52日目】を参照してください。

今日の内容は下記のとおりです。
○ 掲示板一覧でタグによる絞り込み機能の実装
 ・ セレクトボックスの作成方法
○ ヘッダーメニュー(グローバルメニューの作成)
 ・ ヘルパーメソッドの自作方法
 ・ HTMLタグの自作方法
 ・ ルーティングの自作方法

掲示板一覧画面にタグのセレクトボックスを用意し、タグを入力して、該当のタグのある掲示板だけ残すようにしていきます。

掲示板一覧にタグのセレクトボックスを追加する(view)

indexのviewを編集して、掲示板一覧からタグを選び、選んだタグに関連づけられた掲示板のみを表示するようにします。

タグの選択は、プルダウン式のセレクトボックス(select_tagヘルパー使用)によって実装します。
セレクトボックスを選んだ瞬間に掲示板が絞り込まれるのは、「onchange」という属性によってJSを実行します。

index.html.erb
<%= form_tag boards_path, method: :get, class: 'boards__searchForm' do %>
 # form_tag, アクションへのパス, HTTPメソッド, HTMLオプション
 # classはセレクトボックスの位置を調整するためのHTMLオプションで、あとでCSSを編集する。
    <%= select_tag :tag_id, #  セレクトボックスのname属性がtag_idになる。
                   options_from_collection_for_select(Tag.all, :id, :name, params[:tag_id]),
                   #ヘルパーによってセレクトボックスの選択肢となるオプション要素を複数作成している
                   #選択肢としたいオブジェクトのリスト, オプションのvalue, オプションの表示名, 何を選択状態とするの指定
                   # 第四引数の指定によってGET通信でURLに含まれるクエリパラメータのtag_idを選択状態とすることができ、検索後も選択した状態に出来る。
                  { #ハッシュの中身全部が第三引数
                   prompt: 'タグで絞り込み', #空白の時に表示する文字
                   class: 'form-control boards__select', # クラス属性に設定する値
                   onchange: 'submit(this.form);'
                   #選択されるたびに実行するJSのコードを記載している
                   #onchange属性はセレクトボックスに限らず、値が変わった際に実行されるJSのコードを設定できるイベント属性。
                   #ここでは、何か選択肢から選んだ時点でフォーム(this.form)を作成するようになっている
                  }
    %>
<% end %>

CSSの編集

.boards__searchForm {
  display: inline-block;
}

.boards__select {
  display: inline-block;
  width: auto;
}

これによって、掲示板一覧画面の上部にタグのセレクトボックスが表示されます。
ここでタグを選ぶことによって、選んだタグのtag_idをindexアクションに送ることができる。
ただし、現状のコントローラーにはviewから渡ってきたtag_idを使って検索する機能がないので、コントローラーを編集します。

tag_idを用いた検索機能の実装(controller)

tag_idに該当するタグの取得 => タグオブジェクトから関連する掲示板を取得する。

boards_controller.rb
def index
  @boards = params[:tag_id].present? ? Tag.find(params[:tag_id]).boards : Board.all
  # tag_idがあるかを確認(present? ?)
  # tag_idがある場合には、tag_idのパラメータを持つタグを検索(.find)し、
  # 該当するタグにhas_manyでアソシエーションされている掲示板のリストを取得している(親モデル.<has_manyアソシエーション名>)
  # tag_idがない場合は(present? ? 存在する場合の処理 : 存在しない場合の処理)全ての掲示板(Board.all)を表示する
  # @boardsを参照するまではBoard.allを取得することはない。
  @boards = @boards.page(params[:page])
  # 上記で定義された@boardsに対して、page内に表示する件数の制限をかけてから、取得する。
  # 例えば10件とした場合、10件分だけのSQLが発行される。ためBoard.allとしてもパフォーマンスに悪影響しない
end

ヘッダーメニューの追加

ブートストラップのデザインを使用し(もちろんCSSでもいい)viewを作成し、ページを遷移しても常に表示されるメニューを作成する。

touch app/views/application/_header.html.erb

を編集していく。
ここではブートストラップのナビゲーションバーのデザインを使用し、部分的に修正している。
修正箇所にコメントを付記する。
独自のヘルパーを用いる部分があるので、この後ヘルパーを定義する。

_header.html.erb
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
  <a class="navbar-brand" href="#">BoardApp</a> #タイトル名を変更
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-        controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>

<div class="collapse navbar-collapse" id="navbarSupportedContent">
  <ul class="navbar-nav mr-auto">
   # リンク部分の修正。header_link_iteというへルパーを作って使用する。
   # link_toとしないのは、現在表示中のページのみをアクティブ(リンクが白くなる)にするために設定を加えたいから。
   # header_link_item('表示名', リンク先のパス) とすることでリンクを張れる。
    <%= header_link_item('Home', root_path) %> #root_pathはrootingを追加しないと存在しないので後で追加します。ホーム画面です。
    <%= header_link_item('Boards', boards_path) %> 
    <li class="nav-item dropdown">
      <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
        Dropdown
      </a>
      <div class="dropdown-menu" aria-labelledby="navbarDropdown">
        <a class="dropdown-item" href="#">Action</a>
        <a class="dropdown-item" href="#">Another action</a>
        <div class="dropdown-divider"></div>
        <a class="dropdown-item" href="#">Something else here</a>
     </div>
    </li>
    <li class="nav-item">
      <a class="nav-link disabled" href="#">Disabled</a>
    </li>
  </ul>
  <form class="form-inline my-2 my-lg-0">
    <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">
    <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
    </form>
  </div>
</nav>

ヘルパーメソッドの作成

今回はグローバルメニューに使う=全ページで使うヘルパーになるので

app/helpers/application_helper.rb

の中に設定する。
特定のコントローラーでしか使用しない場合は、

boards_helper.rb

のように区別した方が用途がわかりやすくて間違いにくい。

application_helper.rb
module ApplicationHelper
  def header_link_item(name, path)
    class_name = 'nav-item' # ブートストラップのデザインに合わせるためのクラス
    class_name << ' active' if current_page?(path) # 表示中のパスと引数のパスが同一であるかを判定する
    # 表示するパスと引数のパスが同じであれば、クラスの属性に「active」を追加する、というもの。

    content_tag :li, class: class_name do
    # content_tag(HTMLタグ名, HTMLクラス)で任意のHTMLタグを作成できる。
    # ここでは上で作成した「class_name」を指定している。
      link_to name, path, class: 'nav-link'
      # content_tagのブロック内に書いたHTMLが作成したタグの中で展開される。
      # ここでは仮引数で受け取ったとおりにリンクを貼るアンカータグを埋め込むことになる。
    end
  end
end

root_pathの作成

次に、ルーティングを追加する。
ここではまだページは作成していない。

routes.rb
root 'home#index'
# パス名 'コントローラー名#アクション名'

ヘッダーのview(パーシャル)の作成

ヘッダーのパーシャルをrenderで呼び出すだけ。

application.html.erb
<%= render 'header' %>

この後認証機能の学習に進みますが、かなりジャンルが変わるのでここで一旦投稿します。

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

【Ruby】=と==、&と&&、|と||の違い

RubySilverの勉強でつまづいたのでメモします。

=と==の違い

=が左辺に代入
==が同じ
です。

example.html.slim
- dinner = @dinner.vegetables? ? "食べたくない" : "食べる"
= dinner
example.rb
def hoge
  return false if @dinner.vegetables == "きゅうり"
  return false if @dinner.vegetables == "レタス"
  return false if @dinner.vegetables == "なす"

  true
end

&と&&の違い

&が積集合(共通する要素を取り出す)
&&がtrue/falseを返す(全部true(存在する)ならtrue出力)
です。

a = [1, 2, 3]
b = [2, 3, 4]

# 共通する要素を出力
a & b
=> [2, 3]

# 最後に評価されたb(true)を出力
a && b
=> [1, 2, 3]

# aが偽のためnil(右辺を見ない)
a = nil

a && b
=> nil

|と||の違い

・|が和集合
・||がtrue/falseを返す(true(存在する)の時点でtrue出力)
です。

a = [1, 2, 3]
b = [2, 3, 4]

# いずれかに含まれる要素を出力
a | b
=> [1, 2, 3, 4]

# 最初に評価されたa(true)を出力
a || b
=> [1, 2, 3]

# aが偽のためbを出力(右辺まで見る)
a = nil

a || b
=> [2, 3, 4]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rbenvを使ってUbuntu 18.04にRubyをインストールする

はじめに

UbuntuにRubyをインストールしたときの記録です。

各種バージョン

Ubuntu 18.04 LTS
Ruby 2.6.5

インストール方法

rbenvのインストール

rbenvはRubyのバージョン管理ツールです。rbenvを使ってRubyをインストールします。

まずは、関連パッケージをインストール

$ sudo apt update
$ sudo apt install autoconf bison build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm5 libgdbm-dev

rbenvをGitHubからcloneする

$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv

~/.rbenv/bin にパスを通す

$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc

rbenvをセットアップ

$ ~/.rbenv/bin/rbenv init

セットアップを自動で行うために.bashrcに追記。
メッセージには.bash_profileに追記するように出るが、GUI版Ubuntuの場合は.bashrcに追記。

$ echo 'eval "$(rbenv init -)"' >> ~/.bashrc

シェルを再起動 or 以下のコマンドを実行。

$ source ~/.bashrc

rbenv-doctorを使ってrbenvの設定ができたか確認する。

$ curl -fsSL https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-doctor | bash

ruby-buildがインストールされていない場合は、以下が表示される

Checking `rbenv install' support: not found
Unless you plan to add Ruby versions manually, you should install ruby-build.
Please refer to https://github.com/rbenv/ruby-build#installation

ruby-buildのインストール

GitHubからrbenvのプラグインとしてインストール

$ mkdir -p "$(rbenv root)"/plugins
$ git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build

もう一度rbenv-doctorを実行する

$ curl -fsSL https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-doctor | bash

これで、rbenv installを実行できるようになる

Rubyのインストール

インストールできるバージョンをチェック

$ rbenv install -l

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

$ rbenv install 2.6.5

Rubyのデフォルトバージョンを指定

$ rbenv global 2.6.5

確認して終了

$ ruby -v
ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-linux]

参考サイト

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

Rubyの例外のうち StandardError に属さないものを発生させる実験

Rubyの例外クラスは全て Exception のサブクラスだが、そのうちプログラムの継続実行が困難なエラーStandardError のサブクラスではなく、例外の種類を省略した rescue では捕捉しないようになっている。

制御構造 例外処理: begin - Rubyリファレンスマニュアル

例外の一致判定は,発生した例外が rescue 節で指定したクラスのインスタンスであるかどうかで行われます。

error_type が省略された時は StandardError のサブクラスである全ての例外を捕捉します。Rubyの組み込み例外は(SystemExitInterrupt のような脱出を目的としたものを除いて) StandardError のサブクラスです。

test_rescue.rb
begin
    raise StandardError, 'test'
rescue => e
    p e
end
#=> #<StandardError: test>

begin
    raise Exception, 'test'
rescue => e
    p e
end
# Traceback (most recent call last):
# test_rescue.rb:9:in `<main>': test (Exception)

もちろんこの例のように raise 時に例外の種類を明示すれば簡単に発生させられるが、それ以外の場合では(一部を除いて)滅多に見ることがない。実際の例を見てみたくなったので試してみる。

例外の一覧

実際にコードを実行して一覧を手に入れる。

list_exceptions.rb
pp ObjectSpace.each_object(Class)
    .select { |cls| cls <= Exception }
    .reject { |cls| cls <= StandardError }
    .group_by(&:superclass)

結果を継承関係で木構造にまとめると次の通りになる。

  • Exception
    • NoMemoryError
    • ScriptError
      • LoadError
        • Gem::LoadError
          • Gem::ConflictError
          • Gem::MissingSpecError
            • Gem::MissingSpecVersionError
      • NotImplementedError
      • SyntaxError
    • SecurityError
    • SignalException
      • Interrupt
    • SystemExit
      • Gem::SystemExitException
    • SystemStackError
    • fatal
    • MonitorMixin::ConditionVariable::Timeout

GemMonitorMixin にも例外が存在するようだが、その他はリファレンスマニュアルの組み込みライブラリの内容と一致する。

例外発生実験

raise で明示せず出せる例外を試していく。コードは全て beginrescueend にしてあり、 rescue Exception => e と直せば例外を捕捉して表示する。

Exception

全ての例外の祖先のクラスです。

恐らく継承のために用意しているもので、明示せずに出せるものではないと思う。

NoMemoryError

メモリの確保に失敗すると発生します。

という単純明快な例外。プログラムを継続実行しづらいことも納得しやすい。

しかしメモリを大量に消費しないといけないので、他のプロセスにも影響が出そうで気軽に試すのは怖い。以下はWindows Subsystem for Linux (WSL)上で影響なく動いたが、実行は自己責任で。

no_memory_error.rb
begin
    String.new(capacity: 2 ** 40) # 1 TiB
rescue => e
    p e
end
# Traceback (most recent call last):
# no_memory_error.rb: failed to allocate memory (NoMemoryError)

ScriptError

スクリプトのエラーを表す例外クラスです。

以下の例外クラスのスーパークラスです。

  • LoadError
  • NotImplementedError
  • SyntaxError

これらの例外が発生したときは Ruby スクリプト自体にバグがある可能性が高いと考えられます。

これも恐らく継承のために用意しているもので、明示せずに出せるものではないと思う。

LoadError

Kernel.#requireKernel.#load が失敗したときに発生します。

ライブラリ名を間違えたりすれば出るので、よくお世話になる。

load_error.rb
begin
    require 'this/file/does/not/exist'
rescue => e
    p e
end
# Traceback (most recent call last):
#   2: from load_error.rb:2:in `<main>'
#   1: from $(gem environment gemdir)/rubygems/core_ext/kernel_require.rb:54:in `require'
# $(gem environment gemdir)/rubygems/core_ext/kernel_require.rb:54:in `require': cannot load such file -- this/file/does/not/exist (LoadError)

NotImplementedError

実装されていない機能が呼び出されたときに発生します。

恐らくユーザー(やライブラリ)が使うために用意された例外。明示する以外に出す方法は無いと思う。Rubyのソースコードで NotImplementedErrorrb_eNotImpError を検索すると、いくつかの場所で使われていることがわかる。

not_implemented_error.rb
begin
    require 'digest'
    puts Digest::Base.hexdigest('test')
rescue => e
    p e
end
# Traceback (most recent call last):
#   2: from not_implemented_error.rb:3:in `<main>'
#   1: from not_implemented_error.rb:3:in `hexdigest'
# not_implemented_error.rb:3:in `digest': Digest::Base is an abstract class (NotImplementedError)

これまで未実装の部分やオーバーライド前提のメソッドに raise 'Not implemented.' なんて仕込んできた(→到達すると RuntimeError になる)が、こっちの例外を使うのが正しかった?

SyntaxError

ソースコードに文法エラーがあったときに発生します。

これもよくお世話になる。とはいえ試すとなると、コード読み込み時に文法チェックに引っかかったら実行できないので、 eval 内でわざと間違えることにする。

syntax_error.rb
begin
    eval ')'
rescue => e
    p e
end
# Traceback (most recent call last):
#   1: from syntax_error.rb:2:in `<main>'
# syntax_error.rb:2:in `eval': (eval):1: syntax error, unexpected ')' (SyntaxError)
terminal
$ ruby -c syntax_error.rb
Syntax OK

$ ruby syntax_error.rb
Traceback (most recent call last):
    1: from syntax_error.rb:2:in `<main>'
syntax_error.rb:2:in `eval': (eval):1: syntax error, unexpected ')' (SyntaxError)

SecurityError

セキュリティ上の問題が起きたときに発生します。

セキュリティモデルも参照してください。

使ったことのない仕組みなのでピンとこないが、とりあえず eval で試してみる。さっきと同じ文法エラーのコードを与えても、「汚染させた」場合はそもそも実行されなくなる。

security_error.rb
begin
    $SAFE = 1
    eval ')'.taint
rescue => e
    p e
end
# Traceback (most recent call last):
#   1: from security_error.rb:3:in `<main>'
# security_error.rb:3:in `eval': Insecure operation - eval (SecurityError)

SignalException

捕捉していないシグナルを受け取ったときに発生します。

Signal.#trap で割り込みシグナルをハンドリングしないと発生する。

Process.#kill で自身のプロセスに SIGTERM を送ってみる。

signal_exeption.rb
begin
    Process.kill(:TERM, Process.pid)
rescue => e
    p e
end
# Terminated

リファレンスマニュアルに書いてあるが、全てのシグナルに対応するわけではない。例えば SIGSEGV を送ると例外すら出ずRubyがコアダンプして異常終了する。

Interrupt

SIGINT シグナルを捕捉していないときに SIGINT シグナルを受け取ると発生します。 SIGINT 以外のシグナルを受信したときに発生する例外については SignalException を参照してください。

こちらはコード実行を Ctrl+C で中断したりすれば出るのでよく見かける。

interrupt.rb
begin
    Process.kill(:INT, Process.pid)
rescue => e
    p e
end
# Traceback (most recent call last):
#   1: from interrupt.rb:2:in `<main>'
# interrupt.rb:2:in ``': Interrupt

SystemExit

Ruby インタプリタを終了させるときに発生します。

例外自身のページよりも終了処理のページのほうが説明が詳しい。

関数 Kernel.#exitKernel.#abort 、メインスレッドに対する Thread.kill などは SystemExit 例外を発生させます

ということは、スクリプトの途中で exit しても捕捉すれば終了を取り消せる。

逆に、例外として異常終了させることはできなさそう。

SystemStackError

システムスタックがあふれたときに発生します。

典型的には、メソッド呼び出しを無限再帰させてしまった場合に発生します。

再帰の練習などしていれば見るはず。

system_stack_error.rb
def factorial(n)
    return 1 if n == 0

    n * factorial(n - 1)
end

begin
    p factorial(-1)
rescue => e
    p e
end
# Traceback (most recent call last):
#   10080: from system_stack_error.rb:8:in `<main>'
#   10079: from system_stack_error.rb:4:in `factorial'
#   10078: from system_stack_error.rb:4:in `factorial'
#   10077: from system_stack_error.rb:4:in `factorial'
#   10076: from system_stack_error.rb:4:in `factorial'
#   10075: from system_stack_error.rb:4:in `factorial'
#   10074: from system_stack_error.rb:4:in `factorial'
#   10073: from system_stack_error.rb:4:in `factorial'
#    ... 10068 levels...
#       4: from system_stack_error.rb:4:in `factorial'
#       3: from system_stack_error.rb:4:in `factorial'
#       2: from system_stack_error.rb:4:in `factorial'
#       1: from system_stack_error.rb:4:in `factorial'
# system_stack_error.rb:4:in `factorial': stack level too deep (SystemStackError)

fatal

インタプリタ内部で致命的なエラーが起こったときに発生します。

致命的なエラーとは、例えば以下のような状態です。

  • スレッドのデッドロックが発生した
  • -x オプションや -C オプションで指定されたディレクトリに移動できなかった
  • -i オプション付きで起動されたが、 パーミッションなどの関係でファイルを変更できなかった

通常の手段では、 Ruby プログラムからは fatal クラスにはアクセスできません。

小文字で始まっているけれどもクラス。モジュール全体を探しても小文字始まりはこれしか無い。普通にコードに fatal と書いても、変数かメソッドと判断されて例外を参照できない。

ary = ObjectSpace.each_object(Module).select { |mdl| /^[a-z]/ =~ mdl.to_s }
e = ary.first
raise e, 'test'

致命的なエラーとして挙げられているデッドロックを起こしてみる。(参考:The Backyard - DeadLockInRuby

fatal.rb
begin
    q = Queue.new
    Thread.new { q.deq }.join
rescue => e
    p e
end
# Traceback (most recent call last):
#   1: from fatal.rb:3:in `<main>'
# fatal.rb:3:in `join': No live threads left. Deadlock? (fatal)
# 2 threads, 2 sleeps current:0x00007fffcf775500 main thread:0x00007fffcf3b7470
# * #<Thread:0x00007fffcf3e72c8 sleep_forever>
#    rb_thread_t:0x00007fffcf3b7470 native:0x00007f9e3def0700 int:0
#    fatal.rb:3:in `join'
#    fatal.rb:3:in `<main>'
# * #<Thread:0x00007fffcf7ac188@fatal.rb:3 sleep_forever>
#    rb_thread_t:0x00007fffcf775500 native:0x00007f9e39f90700 int:0
#     depended by: tb_thread_id:0x00007fffcf3b7470
#    fatal.rb:3:in `pop'
#    fatal.rb:3:in `block in <main>'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker環境でAtCoder〜Ruby編

AtCoderのRuby環境って特殊だよね?

AtCoderのRubyのバージョンは2.3.3です。ちょっと古いですね。AtCoderの時だけrbenvを使って2.3.3に戻してもいいですけど、Dockerを使ってもいいなぁとぼんやり思っているここの私!
というわけで、DockerでRubyのAtCoder環境を整えてみました。

Dockerfileを書いてみた

Dockerfile
FROM ruby:2.3.3
ENV LANG="C.UTF-8"

ENV USER yourname
ENV HOME /home/${USER}

RUN useradd -m ${USER}
RUN gpasswd -a ${USER} sudo
RUN echo "${USER}:password" | chpasswd

RUN gem install at_coder_friends

USER ${USER}
WORKDIR ${HOME}

CMD ["/bin/bash"]

途中でインストールしているgemの at_coder_friends というのは、 AtCoderツールを自作した話@nejiko96 さんが作ってくださったAtCoderのテストツールです。テストだけではなく、回答のスケルトンも作ってくれたり、このツールから回答フォームにsubmitできたりとものすごく便利です。現在はRubyとC++に対応している模様です。
Usageなどが書かれたGitHubのリポジトリはこちら> AtCoderFriends

で、ビルドして実行してAtCoderを楽しみましょう!

docker build --tag at-coder .
docker run --rm -it -v path/to/atcoder_problems:/home/yourname at-coder

参考URL

https://qiita.com/Riliumph/items/3b09e0804d7a04dff85b
https://qiita.com/nejiko96/items/0cd23ac2c033864ef341

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

文系大学生がアプリ開発よりも機械学習の方が自学自走しやすいと考える理由について

はじめに

最近の風潮として、アプリの開発を一人で自学自走は頑張れば可能と言う考えが蔓延っていて、機械学習はなんか難しそうみたいな考え方があるように見受けられます。確かにアプリの開発を一人で自学自走する人はとてもすごいと思いますが、自分はアプリ開発を一人でやりきるのはとても難しく、また機械学習の自学自走はそれに比べると難しくないと考えています

自分の感覚で言うと
・アプリ開発自走→天才じゃないと無理ゲー
・機械学習自走→頑張れば可能
という感じです(これは自分の感覚なので、個人差はあると思います)。

その理由について述べていきたいと思います。

自分のそれぞれの経験について

まずその前に自分の(お前の経歴なんか興味ねえよって人はすっ飛ばして見て下さい)

アプリ開発

・実際に去年の3月にtechcampさんの方に通って勉強→3ヶ月ほどやるもそこで挫折
・今年の3月にpreogate→railsチュートリアルの流れで勉強をするも、railsチュートリアルで挫折

機械学習

・去年の末から初めて今のところ挫折なし
詳しくは以下を参照
https://qiita.com/HayatoYamaguchi/items/1c20595c5e6dac4530dc

こんな感じでやっています。確かに機械学習の方が本腰を入れて行なっていたという面だったり実際に一度アプリ開発でプログラミングを学んだ後だからというものもあるかもしれませんが、それを加味しても自分は機械学習の方が自学自走しやすいと考える理由をいかに述べたいと思います。

1、参照するページが多く、さらに自分がどこまで反映させたのかがわかりにくい

プログラミング初心者あるあるとして、まずは写生をしますがこの写生内容は往々にして「,」と「.」のミスなどの文字やスペルミスや、括弧やendの位置などのミスが起こりやすいです。その中でエラーメッセージを見てみると、そのメッセージが起きたサイトに言ってもエラーの根本的な原因はそのページではないなどのことが往々に発生します(例えばビューファイルのエラーなのに実際はコントローラーの中の変数の受け取りに問題があるなど)。そのためエラーの発掘が自分では困難になり、挫折しやすいです。一方機械学習の場合は最初はクラス分けを行わなければjupyter notebookなどの一つのファイルで完結します。そのため、エラーの行を見ればその行や周辺を直せば大体のエラーは解決します。

2、アプリ開発には必要な言語や概念が多すぎる

機械学習においてプログラミング面で必要なのはpythonやそれに付随する各種ライブラリ(numpy,pandas,sklearnなど)のみです。
しかし、アプリ開発を学ぼうとすると、例えばrailsだったらrailsチュートリアルを完遂して自分でポートフォリオを作ろうとすると、

・htmlcss
・ruby
・javascript
・SQL
・Git

などが必要で、データベースとの連携方法やさらにテストの実装についても理解も必要なことがあります。そのため何もやったことがない初心者が始めようとすると、「なんかごちゃごちゃしてるけど今は何をやればいいんだっけ、、、」という状態になりやすいです。

ここまで見ると機械学習の方がはるかに簡単そう、、、って思いそうなので、一応機械学習の方が難しい点も述べたいと思います。

機械学習の方が難しい点について

ある程度の数学や統計が必要である

アプリ開発にはほぼ一切数学の知識は求められません。
一方機械学習ではそうはいかず、ある程度アルゴリズムの中身を理解するとなると、大学基礎レベルの数学は必要になります。そのため数学アレルギーの人には厳しいです。

論理的な式がより求められる

機械学習ではモデルを作る前にデータの変形などをする必要があります。そのため、「この条件の時にここを変形する」などの少し論理的なコードを書く力が必要です。例えばこれは自分の昔の記事から引っ張ってきたものですが、条件を指定して削除するなどの少し複雑なものになっています。これは簡単な例ですが、論理的な式を求められることが多いです。」

Xmat = Xmat.drop(Xmat[(Xmat['TotalSF']>5) & (Xmat['SalePrice']<12.5)].index)

終わりに

これはあくまでもサンプル数1の考え方ですが、実際にこのような考え方を投稿している人がいなかったので投稿させてもらいました。何をやるかを考えている人には参考にしてもらえると嬉しいです。

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

VisualStudioCode でRubyの開発環境を作る

VisualStudioCode でRubyの開発環境を作る

利用環境

2019/10/12現在の最新バージョンを用いている。

rubyinstaller-devkit-2.6.5-1-x64
VisualStudioCode Ver1.39.1
Visual Studio Code Ruby Extension

参考

基本的な手順はここのブログで提供されている情報のとおりである。
https://impsbl.hatenablog.jp/entry/RubyInVSCode1
バージョンの違い等もあるので、取得した画面ショットとともに、ここに整理する。

手順

1.VisualStudioCodeのインストール手順は省略する。
2.ruby with Devkitのインストール
2019/10/12現在の最新バージョン rubyinstaller-devkit-2.6.5-1-x64
以下のとおり

image.png
image.png
image.png
image.png
image.png

3.VisualStudioCodeへアドインのインストール
 Visual Studio Code Ruby Extension
 コンソールで、rubyが使えることを確認する。
image.png
image.png

表示 - 拡張機能
image.png
image.png

image.png

4.Rubyの必要環境をgemでインストールする。
注意事項として、install?ruby-debug-ide は、最後にする。(依存関係がある)

gem install rubocop
gem install debase
gem install rcodetools
gem install ruby-lint
gem install reek
gem install faster
gem install debride
gem install ruby-debug-ide

5.Visual Studio Code Ruby Extensionの設定
以下の画面のとおりに設定する。
image.png

Lintについては、以下のとおりjsonを編集する。

{
    "ruby.codeCompletion": "rcodetools",
    "ruby.format": "rubocop",
    "ruby.intellisense": "rubyLocate",
    "ruby.lint": {
        "reek": true,
        "rubocop": true,
        "ruby": true, //Runs ruby -wc
        "fasterer": true,
        "debride": true,
        "ruby-lint": true
    },
    "ruby.locate": {
        "exclude": "{**/@(test|spec|tmp|.*),**/@(test|spec|tmp|.*)/**,**/*_spec.rb}",
        "include": "**/*.rb"
    }
}

動作確認

任意のコードで、デバッグポイントが有効であることを確認する。
image.png

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

order("id ASC") <=> order("id DESC)の切り替え Rails

備忘録及びアウトプットの練習のために、自分が学んだこと・工夫したこと・苦労した事などを書いていきたいと思います。

今回、orderメソッドを使うことにより、テーブルが呼び出したデータのソートができるということで、
画面上のスイッチをクリックするだけで、投稿内容の降順・昇順を切り替えれるようにしてみました。

1.ビューの実装

ビューファイルにスイッチ用のコードを書く

= link_to "↕️", "switch", class: "switch", method: :get

画面上の↕️をクリックしたら、"switch"に移動するようにしています。
個別にcssを適用できるように、"switch"というクラスもつけています。

2.ルーティングの設定

1でswitchというパスを指定したので、対応するルーティングを設定します

get 'switch' => 'tweets/#switch'

パス’switch'からtweetsコントローラのswitchアクションを実行するようにしています

3.コントローラの設定

2で指定したswitchアクションを作ります。

@@order = 1

def index
 if @@order == 1
   @tweets = Tweet.order("id ASC")
 else
   @tweets = Tweet.order("id DESC")
 end
end 

def switch
 @@order *= -1
 redirect_to :action => "index"
end

切り替えに使用するクラス変数@@orderを定義します。

indexアクション内で、@@orderが1かそうでないかで、order("id ASC")かorder("id DESC")かが変わるようなif文を書きます。

次に、@@orderを切り替えるためのswitchアクションを定義します。
switchアクションを実行するたび、
つまりビュー上の↕️をクリックするたびに、クラス変数@@orderに-1をかけるようにします。
redirect_to :action => "index"とすることで、スイッチを押すたびに別画面に遷移することがなくなるようにします。

これで、投稿の順番を変えるスイッチの実装が出来ました。
ルーティングの設定やクラス変数の利用など、色んな項目の復習になりました。
記事の内容は少しづつ更新していこうと思います。

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

ワンクリックでorder("id ASC") <=> order("id DESC)の切り替え

はじめに

備忘録及びアウトプットの練習のために、自分が学んだこと・工夫したこと・苦労した事などを書いていきたいと思います。

今回、orderメソッドを使うことにより、テーブルが呼び出したデータのソートができるということで、
画面上のスイッチをクリックするだけで、投稿内容の降順・昇順を切り替えれるようにしてみました。

1.ビューの実装

ビューファイルにスイッチ用のコードを書く

= link_to "↕️", "switch", class: "switch", method: :get

画面上の↕️をクリックしたら、"switch"に移動するようにしています。
個別にcssを適用できるように、"switch"というクラスもつけています。

2.ルーティングの設定

1でswitchというパスを指定したので、対応するルーティングを設定します

get 'switch' => 'tweets/#switch'

パス’switch'からtweetsコントローラのswitchアクションを実行するようにしています

3.コントローラの設定

2で指定したswitchアクションを作ります。

@@order = 1

def index
 if @@order == 1
   @tweets = Tweet.order("id ASC")
 else
   @tweets = Tweet.order("id DESC")
 end
end 

def switch
 @@order *= -1
 redirect_to :action => "index"
end

切り替えに使用するクラス変数@@orderを定義します。

indexアクション内で、@@orderが1かそうでないかで、order("id ASC")かorder("id DESC")かが変わるようなif文を書きます。

次に、@@orderを切り替えるためのswitchアクションを定義します。
switchアクションを実行するたび、
つまりビュー上の↕️をクリックするたびに、クラス変数@@orderに-1をかけるようにします。
redirect_to :action => "index"とすることで、スイッチを押すたびに別画面に遷移することがなくなるようにします。

終わりに

これで、投稿の順番を変えるスイッチの実装が出来ました。
ルーティングの設定やクラス変数の利用など、色んな項目の復習になりました。
記事の内容は少しづつ更新していこうと思います。

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

Mac OS CatalinaにEmacsをインストールする方法

準備

  • App Storeから最新のXcodeをインストール
  • Homebrewをインストール

Homebrewのホームページにインストール方法が記載されています。

Full Disk Access権限にrubyを追加

  • システム設定を開く
  • Security & Privacyを開く

Screen Shot 2019-10-12 at 11.16.10.png

  • Privacyタブをクリックして、左のメニューからFull Disk Accessをクリックする

Screen Shot 2019-10-12 at 11.17.17.png

  • 左下のロックボタンをクリックして、ロックを解除する

Screen Shot 2019-10-12 at 11.17.17.png

  • 右側の「+」ボタンをクリックする

Screen Shot 2019-10-12 at 11.38.50.png

  • /usr/bin/に移動し、rubyを追加する

Screen Shot 2019-10-12 at 11.43.05.png

Emacsをインストールする

brew install emacs

Emacsのバージョンを確認する

emacs --version
GNU Emacs 26.3
Copyright (C) 2019 Free Software Foundation, Inc.
GNU Emacs comes with ABSOLUTELY NO WARRANTY.
You may redistribute copies of GNU Emacs
under the terms of the GNU General Public License.
For more information about these matters, see the file named COPYING.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

シンボルリテラル

シンボルの生成方法

シンボルのリテラルは、文字列の先頭にコロン「:」を付加する。
また文字列の囲み文字を省略できるため省略するのが一般的となっている。

sample1.rb
foo1 = :"foo1"          #=> :foo1
foo2 = :"#{foo1}foo2"   #=> :foo1foo2
foo3 = :'foo3'          #=> :foo3
foo4 = :foo4            #=> :foo4

#パーセント記法を用いたシンボルの指定も可能。その場合は%似続けてsを指定する。
%s?foo1?               #=> :foo1
%s[foo2]               #=> :foo2

#文字列からシンボルを生成する方法はto_symメソッドを使用する。
v1 = "foo1"           #=> "foo1"
v2 = v1.to_sym        #=> :foo1
v3 = v2.to_s          #=> "foo1"

オブジェクトの同値性と同一性

文字列とシンボルの違い
文字列リテラルでは、毎回新たにオブジェクトを生成するためオブイェクトIDが変わる。
シンボルリテラルでは同じオブジェクトを参照するのでオブジェクトIDは変わらない。

sample2.rb
p "foo1".object_id       #=> 31141068
p "foo1".object_id       #=> 31141044

p :foo1.object_id       #=> 124532
p :foo1.object_id       #=> 124532
p :foo2.object_id       #=> 124628

#シンボルは内部の整数として扱われるので文字列に比べると処理は早くなる。
#単にラベルとして文字の列を扱いたい場合は、文字列よりシンボルのほうが効率が良くなる。

#オブジェクトの同値性と同一性判定
"foo1" == "foo1"         #=> true
"foo1".equal? "foo1"     #=> false

"foo1" == "foo1"         #=> true
"foo1".equal? "foo1"     #=> false

#==と同様の動作をするメソッドにeql?メソッドがあるがこれは型まで比較する。
"foo1".eql? "foo1"      #=> true
1.0 == 1                #=> true
1.0.eql? 1              #=> false
1.0.eql? 1.0            #=> true

破壊的メソッド(※シンボルリテラルではないが説明としてここに記載)

破壊的メソッドを使用すると、他の変数の参照先にも影響する。

sample3.rb
v1 = "foo1"
v2 = v1
p v1.chop            #=> "foo"
p v2                 #=> "foo1"
p v1.chop1           #=> "foo"
p v2                 #=> "foo"

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

webpacker の check_yarn_integrity オプションについて調べてみた

はじめに

Rails アプリで webpacker を利用するケースが増えていると思います。Rails 5.1以上のアプリでは、rails new myapp --webpack というオプションをつける形でも webpacker をインストールできるようになっていますよね。

しかし個人的に、使い方を理解しきれていないことが多いです。そこで理解するためのとっかかりとして、config/webpacker.yml のオプションを調べてみようと思い、その中の check_yarn_integrity というオプションについて手始めに調べてみました。

何をするオプションなのか

webpacker の README を抜粋します。

By default, in development, webpacker runs a yarn integrity check to ensure that all local JavaScript packages are up-to-date.
This is similar to what bundler does currently in Rails, but for JavaScript packages.
If your system is out of date, then Rails will not initialize.
You will be asked to upgrade your local JavaScript packages by running yarn install.

こちらを意訳してみました。

  • development 環境では、ローカルの全ての JavaScript パッケージが最新であるかを yarn integrity check する
  • Rails の bundler がやっていることと似てるが、これは JavaScript 用
  • システムが古くなっていると、Rails は初期化されない
  • yarn install でローカルの JavaScript パッケージをアップグレードするか尋ねられるようになる

実施していること

実際関連しているコード部分を抜粋すると以下になります。

https://github.com/rails/webpacker/blob/8845f37bb038ad0adff813326a7d6a034b9b9a81/lib/webpacker/railtie.rb#L16-L51

config.webpacker.check_yarn_integrity = true にしておけば、rails crails s といった Rails アプリケーションを起動する際の初期化時に、 yarn check --integrity を実行し、エラーが出たら以下のようなエラーメッセージを出力してくれるようです。

========================================
  Your Yarn packages are out of date!
  Please run `yarn install` to update.
========================================


To disable this check, please add `config.webpacker.check_yarn_integrity = false`
to your Rails development config file (config/environments/development.rb).

不要な場合は config.webpacker.check_yarn_integrity = false にすれば OK のようです。
パッケージが古くなっていたらエラーで知らせてくれるということなので、パッケージを最新の状態に保つ手助けになりそうですね。

おわりに

JavaScript のパッケージ管理は複雑で理解が難しいですね。根気よく学習していきたいと思います。

参考

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

Railsのトランザクションと例外処理

Railsのトランザクションと例外処理がよく分からなかったので、調べました。

app/controllers/user_controller.rb
def create
  ActiveRecode::Base.transaction do
    # createではなく、create!にすると保存できなかったときに例外が発生します。
    @user = User.create!(user_params)

  recue => e 
  # バリデーションエラーだけ拾いたい場合は次の行
  # (ActiveRecord:RecodeInvalid => e) 
    # error処理
  end

end

recueはphpでいうとexceptionみたいな感じでした。

参考サイト

rails save! create! update!のバリデーション例外を捕捉する - Qiita

【Rails】例外処理の書き方(begin, rescue, raise,retry, ensure) - Qiita

rubyの例外についてまとめてみた - Qiita

ActiveRecord::Base.transactionで囲うタイミング - Qiita

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

Railsのhas_oneで関連テーブルの保存

has_oneで関連させて保存させる方法につまったので書いておきます。
他のやり方(createを2回使う)もあったのですが、create!しても例外をキャッチできない?感じだったので、こちらの書き方にしました。

app/controllers/users_controller.rb
def create
    @user = User.create(user_params)
    # この行のcreate_articleは状況に応じてprofile部分を読みかえて使います。
    @profile = @user.create_profile(profile_params)
end
app/models/user.rb
has_one :profile
app/models/profile.rb
belongs_to :user

参考サイト
Active Record の関連付け - Rails ガイド

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

ORDER BYを使わずにfind_eachっぽくデータを処理してメモリを節約する方法

ActiveRecord::Batches#find_eachについて

ActiveRecordのfind_eachは、メモリを節約しながら大量のデータを処理するためのメソッドです。
ActiveRecordのリファレンスには以下のようなことが書かれています。

find_each(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil)

find_eachはデータベースのレコードをループ処理します。
大量のレコードが存在するとき、allを用いては、すべてのオブジェクトを一度にインスタンス化するため、メモリ効率がとても悪いです。
一方、find_eachのようなバッチ処理メソッドを使用すると、レコードをバッチで処理するため、メモリ消費が大幅に節約できます。
find_eachは、デフォルトで:batch_sizeが1000なので、1000件ずつのレコードを分割してループします。
https://api.rubyonrails.org/classes/ActiveRecord/Batches.html#method-i-find_each

つまり、10,000件のレコードを取得してcsvに加工したりといった処理を行いたいとき、find_eachを用いれば10,000件のレコードがメモリにのらずに済むということです。
find_eachを用いてActiveRecordが生成するクエリは、以下のようになります。

User.find_each do |user|
    # do_something
end
SELECT  `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1000
SELECT  `users`.* FROM `users` WHERE `users`.`id` > 1000 ORDER BY `users`.`id` ASC LIMIT 1000
SELECT  `users`.* FROM `users` WHERE `users`.`id` > 2000 ORDER BY `users`.`id` ASC LIMIT 1000
SELECT  `users`.* FROM `users` WHERE `users`.`id` > 3000 ORDER BY `users`.`id` ASC LIMIT 1000
...
(usersが全件取得できるまで繰り返す。10,000件存在していれば10)

LIMIT 1000とレコードを限定して取得し、WHERE users.id > 1000のように条件指定することで、漏れ・ダブリなくレコードを取得するようになっています。

LIMITを1000以外にしたい場合は、リファレンスにあるように、find_eah(batch_size: 2000)のように呼び出せば良いです。

しかし、上記を見ると、ORDER BY users.id ASCという意図しないソートがクエリに含まれています。
リファレンスを確認すると、次のような注意書きがあります。

NOTE: It's not possible to set the order. That is automatically set to ascending on the primary key (“id ASC”) to make the batch ordering work. This also means that this method only works when the primary key is orderable (e.g. an integer or string).

どうやら、find_eachは主キーでソートしてからLIMITをかけて分割取得することで、漏れ・ダブリなくデータを処理するといった実装になっているようです。

よって、select(users.id AS user_id)などと一緒に使うとエラーが出ます(idというカラム名である必要がある)。
また、他のカラムでORDERしていると、それは無視されます。

indexによっては、ORDERが入ることでクエリが遅くなってしまうことがあると思います。
ORDERは入れたくないが、分割して処理することでメモリ消費を削減したいとき、どう実現するかを考えてみました。

実現方法

単純ですが、idをすべて取ってきて、each_sliceで任意の数ずつwhereで取ってくることで、ORDERなしでfind_eachっぽく処理するようにしてみました。

ids = User.pluck(:id)
ids.each_slice(1000) do |id|
    User.where(id: id).each do |user|
        # do_somethins
    end
end

これで発行されるクエリは以下のとおりです。

SELECT `users`.`id` FROM `users`
SELECT `users`.* FROM `users` WHERE `users`.`id` IN (1, 2, 3, 4, 5, 6, 7, 8, ..., 1000)
...
(全件取得するまで)

これでORDERなしで、メモリを節約できました。

問題点

find_eachは安全のためにORDER BY users.id ASCしているわけで、例えばクエリ実行のたびに取得順序が変わるような場合は当然これは使用できません。
また、ids = User.pluck(:id)でidを全件取ってくるため、ここでパフォーマンスが落ちることもあります。
結局、find_eachでORDERが入ってもいいように、indexを適切にできるならそうしたほうが良さそうです。

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

【52日目】掲示板にタグを設定しよう①!(seedについて、チェックボックスの作成、タグの保存・表示)

掲示板へのtag付け機能の追加

seedを使ってTagを追加しておく

seedとは

seedとは初期データのことで、システムの運用前に初期データとなるマスターデータ(あらかじめ用意するベースとなるデータ)を投入したり、開発段階で動作確認のために初期データを投入したりします。
他にもユーザー登録機能のある場合に、アドミン権限をユーザーにrollカラムをつける形で付与している場合などには、データベースをリセットする場合に備えて初期データとしてユーザー情報を投入する場合があるようです。

seedファイルはdb/seeds.rbにcreateメソッドやrubyスクリプトによって記述します。
データベースに投入するにはrails db:seedをすれば良いです。

seedファイルを使ったTagの初期データ作成

createメソッドでは、配列 []の中にハッシュ{key: 値}形式で複数のデータを同時に作成することができる。

seeds.rb
Tag.create([
  { name: 'Ruby' },
  { name: 'Ruby on Rails4' },
  { name: 'Ruby on Rails5' },
  { name: 'Python2' },
  { name: 'Python3' },
  { name: 'Django2' },
])

seedsファイルが編集できたらデータを投入する。

docker-compose exec web rails db:seed

フォームにチェックボックスを作成

_form.html.erb
<div class="form-group">
  <span>タグ</span>
  <%= f.collection_check_boxes(:tag_ids, Tag.all, :id, :name) do |tag| %>
    <div class="form-check">
      <%= tag.label class: 'form-check-label' do %>
        <%= tag.check_box class: 'form-check-input' %>
        <%= tag.text %>
      <% end %>
    </div>
  <% end %>
</div>

まず、多対多アソシエーションの設定によって、boardオブジェクトにはtag_idsプロパティが、tagオブジェクトにはboard_idsプロパティが追加されている。
ここでは、boardに登録されているtagの数分のチェックボックスを作成することになるため、フォームヘルパーのcollection.check.boxesメソッドを用いる。

collection_check_boxesメソッドの構造確認

collection_check_boxes(第一引数, :第二引数, 第三引数, :第四引数, :第五引数 ) 

第一引数 データを保存するテーブル(フォームヘルパー内の場合省略可能)
第二引数 データを保存するカラム
第三引数 第二引数のカラムに渡してあげるデータのリスト
     ここで渡したデータの数分のチェックボックスが作成される。
第四引数 チェックボックスのvalueに設定されるプロパティ
第五引数 チェックボックスのラベル名に設定されるプロパティ(text)
=>この第二引数で渡したデータリストの数分、ブロック内が繰り返し実行される。
  ブロック内にはチェックボックスを生成するHTMLが記述されていて、データの数分のチェックボックスが生成される。

<%= tag.label class: 'form-check-label' do %> # タグラベルでチェックボックスとラベル名を覆う
        <%= tag.check_box class: 'form-check-input' %> # チェックボックス自体
        <%= tag.text %> # 第五引数に指定したラベルが表示される

boards_controllerを修正して、チェックボックスを保存できるようにする

ストロングパラメータに対して、チェックボックスのパラメータ(第四引数)に指定したtag_idsを許可すれば保存できるようになる。
ただし、tag_idsは配列の形で複数渡ってくるので、[]をつけて配列であることを明示する必要がある。

boards_controller.rb
def board_params
  params.require(:board).permit(:name, :title, :body, tag_ids: [])
end

掲示板詳細画面で関連づけられたタグを表示する

_detail.html.erb
<% @board.tags.each do |tag| %>
  <span class="badge badge-primary"><%= tag.name %></span>
<% end %>

アソシエーションにおいては、「親モデル.」で、アソシエーション先の子モデルにアクセスできます。
つまり1行目はアソシエーションに設定したboard.tagsをループして、タグの数だけタグ名を表示するようにしている。
2行目では、ブートストラップのバッジのデザインを使用して、掲示板のタイトルの下に表示するようにしている。

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