20191021のRailsに関する記事は21件です。

Webpackerを使わずにRails,React,Typescriptアプリのベースを作る

はじめに

仕事でReactを使うようになったので勉強がてら自分でReact,Typescript,Railsを使ったアプリを勉強していましたが

意外と最初の設定でつまずいたのでまとめます。

Rails new

bundle init
#Gemfileにてrailsのコメントアウトを外す
bundle install --path vendor/bundle
bundle exec rails new .
bundle install
bundle update

Webpackerを除く

Gemfileからwebpackerを除きます。

React表示用のviewを作成

bundle exec rails g controller react-ui::home index

tsconfigを作成

typescriptを使うためにはtsconfigを作成しなくてはいけない

railsのルートディレクトリにtsconfig.jsonファイルを作成して中身を以下の様にする

{
  "compilerOptions": {
    "strictNullChecks": true,
    "noUnusedLocals": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "outDir": "./dist/",
    "sourceMap": true,
    "noImplicitAny": false,
    "lib": ["dom", "ES2017"],
    "module": "commonjs",
    "target": "es5",
    "jsx": "react",
    "baseUrl": ".",
    "paths": { "import-png": ["types/import-jpg"] },
    "typeRoots": ["types", "node_modules/@types"]
  }
}

npm install webpack -D

webpackを使える様にする

npm install webpack -D
npm install webpack-cli -D
npm isntall webpack-dev-server -D

webpack記載

railsのルートディレクトリにwebpack.config.jsファイルを作成する。

webpack.config.jsを以下の様に記載する

npm install typescript
npm install html-webpack-plugin -D
npm install webpack-manifest-plugin -D
npm install ts-loader style-loader css-loader file-loader url-loader -D

不要なエラーが発生するので vendor/bundle/ruby/2.6.0/gems 以下のwebpackerのフォルダを削除します。

const HtmlWebpackPlugin = require("html-webpack-plugin")
const ManifestPlugin = require("webpack-manifest-plugin")
const path = require('path');

module.exports = {
    mode: "development",
    entry: {
        home: `${__dirname}/app/webpack/entry/home`
    },

    output: {
        path: `${__dirname}/public/packs`,
        publicPath: `${__dirname}/app/webpack`,
        filename: "[name].js"
    },

    module: {
        rules: [{
                test: /\.(tsx|ts)$/,
                loader: "ts-loader"
            },
            {
                test: /\.css/,
                use: [
                    "style-loader",
                    {
                        loader: "css-loader",
                        options: { url: true }
                    }
                ]
            },
            {
                test: /\.(jpg|png)$/,
                loader: "file-loader?name=/public/[name].[ext]"
            },
            { test: /\.(eot|svg|woff|ttf|gif)$/, loader: "url-loader" }
        ]
    },

    watchOptions: {
        poll: 500
    },

    resolve: {
        extensions: [".ts", ".tsx", ".js"]
    },

    plugins: [
        new HtmlWebpackPlugin({
            template: `${__dirname}/app/webpack/index.html`,
            filename: "index.html"
        }),
        new ManifestPlugin({
            fileName: "manifest.json",
            publicPath: "/packs/",
            writeToFileEmit: true
        })
    ],
    devServer: {
        publicPath: "/packs/",
        historyApiFallback: true,
        inline: true,
        hot: true,
        port: 3035,
        contentBase: "/packs/"
    }
};

app/webpack以下にindex.htmlを作成して以下の様に記載します。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>

</body>

</html>

package.jsonにscriptを追加

package.jsonが作成されているはずなのでそのファイルを開きscriptを以下の様に設定する

"scripts": {
    "start": "webpack-dev-server --hot --inline --contentbase public/packs/  --mode development"
},

これで npm start をコマンドを打ったときにwebpack-dev-serverが起動してくれる

reactを使えるようにする

npm install react -D
npm install react-dom -D

webpack.config.jsのentryからjavascriptを読み込むので

${__dirname}/app/webpack/entry/home

のフォルダにindex.tsxを作成します。

そして中身を以下の様にします。

import * as React from "react";
import { render } from "react-dom";

render(<div>Home</div>, document.getElementById("root"));

Rails側で読み込むロジック追加

webpackerを使わないのでjavascript_pack_tagは使えません。

ですので

react_ui/home/index.html.erbを以下の様にします。

<div id="root"></div>

<% if Rails.env == "development" %>
<script src="http://localhost:3035/packs/home.js"></script>
<% else %>
<script src="/packs/home.js"></script>
<% end %>

config/routes.rbに

root to: "react_ui/home#index"

を追加

layouts/application.html.erbの

    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

を削除します。

動作確認

bundle exec rails s
npm start

を実行してlocalhost:3000にアクセスすれはHomeが表示されるはずです。

あとは好きにreactを書いていけばOKです。

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

Rails チュートリアル 第10章 まとめ

editアクション -User編集ページ
GET /users/:id/edit

def edit
  @user = User.find(params[:id]) # GETリクエストのidを使い、ユーザー情報を取得
end

ViewはUserコントローラのnewとほぼ同じ
コード上の違いがないのに、newはcreateアクションを発行し、editはupdateアクションを発行する?

newはインスタンスを新しく作っていて型だけだが、editはfindを使ってデータを取得しているのでidが入っている。それで判断している

privateメソッド
このメソッド以下のメソッドは全てprivateになる
このコントローラ内でだけ使って外からのアクセスで書き換えられたくないという時に使う
paramsを精査するとかいう場所で使う

updateアクション -編集内容をDBに保存

def update
   @user = User.find(params[:id]) # 入力された内容を取得し、@userに代入
    if @user.update_attributes(user_params) #引数には更新したいカラム名
      flash[:success] = "Profile updated"
      redirect_to @user
   else
      render "edit"
   end
end

認可
ログインしていない人がページにアクセスできてしまう問題を解決
→ 1 権限のないユーザーがアクセスしたらログインするように促す

beforeフィルター
何かを実行する前に、指定した機能を追加してください
→ 編集ページに行く前に、ログインしてください
before_action :logged_in_user, only: [:edit, :update]

def logged_in_user
      unless logged_in? #もしログインしていなかったら
        flash[:danger] = "Please log in." #コメント表示
        redirect_to login_url # loginページに
      end
    end

beforeのフォローとして、testにログインしたことを確認するコードを書く

beforeが機能しているかコメントアウトしてチェック
beforeが機能していないとErrorとなるようにテストを書く

users_controller_test
  test "should redirect edit when not logged in" do
    get edit_user_path(@user) # loginせずにページにアクセス
    assert_not flash.empty? # flashが出る
    assert_redirected_to login_url # loginページへ
  end

  test "should redirect update when not logged in" do
    patch user_path(@user), params: { user: { name: @user.name,
                                              email: @user.email } }
# patchリクエストがブラウザ経由ではなく直接送りつけられた場合
    assert_not flash.empty?
    assert_redirected_to login_url
  end

2 自分のプロフィールページだけを編集できるように
TDDで
テストは

users_controller_test
 test "should redirect edit when logged in as wrong user" do
    log_in_as(@other_user) #  ログイン
    get edit_user_path(@user) # 違う人の編集ページに行こうとする
    assert flash.empty? # flash
    assert_redirected_to root_url # topページに
  end
 # ...(略)

正しいユーザーかどうかチェックする機能を実装

users_controller
def correct_user
      # GET /users/:id/edit
      # PATCH /users/:id どちらかから送られてきたリクエストをparamsに格納
      @user = User.find(params[:id])
      # currentは第8章で実装、今ログインしているユーザーを示す
      redirect_to(root_url) unless @user == current_user
end

indexアクション - User一覧ページ

TDD

user_controller_test
test "should redirect index when not logged in" do
    get users_path # users_pathは詳細ページを表す、詳細ページへ移動
    assert_redirected_to login_url # beforeアクションでloginページへ
end
user_controller
def index
  @users = User.all # @usersに注意、User情報を全て取得
end

View

<% provide(:title, 'All users') %>
<h1>All users</h1>

<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 50 %>
      <%= link_to user.name, user %> # リンクのuserでユーザー詳細に
# どこでやったか
    </li>
  <% end %>
</ul>

サンプルユーザーの生成 Fake

Pagenation
install
Viewではページネーションしたいものを囲む

<%= will_paginate @user %> #引数にUserの集合@userを渡すことで、Userを対象に指定
:

<%= will_paginate %>

Controllerでは

def index
    @users = User.paginate(page: params[:page]) 
# parameterにpageが格納されるようになるので、それを@usersに保存
# 初期では30ずつ
end

IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "index including pagination" do
    log_in_as(@user) # login
    get users_path # indexページにGETリクエスト
    assert_template 'users/index' # indexページに
    assert_select 'div.pagination' # paginationが機能するか
    User.paginate(page: 1).each do |user| #それぞれのリンクが正しく貼られているか
      assert_select 'a[href=?]', user_path(user), text: user.name
    end
  end

リファクタリング  -後でまとめる 32
renderを使って、UserオブジェクトのPartialを作り、それをrender @userで呼び出す

destroyアクション -削除
管理権限を持った人ならアカウントを削除できる

admin
add_column :users, :admin, :boolean, default: false
の,を忘れず

index.htmlのrender @userが_user.html.erbを呼ぶので

_user.html.erb
<% if current_user.admin? && !current_user?(user) %>
# admin なら以下のリンクが見える
    | <%= link_to "delete", user_path(user), method: :delete,
                                  data: { confirm: "You sure?" } %>
# method:で指定しているのでDELETEリクエストが/users/:idに送られる
# 
<% end %>

を書き加える
すると、index -> delete という流れが作れる

def destroy
# 上でDELETE /user/:idに送られたparameterを使い、Userを探し、destroy
    User.find(params[:id]).destroy
    flash[:success] = "User deleted"
    redirect_to users_url # indexへ
end

IntergrationTest
indexのテストに追加している

# adminであれば、deleteが見えているはずという意味。なんでifじゃない?
 unless user == @admin
        assert_select 'a[href=?]', user_path(user), text: 'delete'
 end
...
 delete user_path(@non_admin) #adminなので、non_adminを削除する

...
 test "index as non-admin" do
    log_in_as(@non_admin) #loginしてみる
    get users_path # indexページに行く
    assert_select 'a', text: 'delete', count: 0 #しかし、削除されているので行けない
 end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker-compose+Rails+Mysqlでサーバー起動まで

はじめに

Dockerを使って開発演習が出来たことに感動したので、その嬉しさの勢いと、アウトプットとして書きます。また、Dockerの仕組みが分からなくても、以下に従っていけば、とりあえず構築できます。

環境

  • Docker for Windows
  • VScode
  • Windows10 Pro
  • Mysql

Windowsを使ってますが、基本的にmacの方も、DockerとMysqlがインストールされていれば大丈夫です。

Dockerがインストールされているかの確認

docker --version 
Docker version 18.09.1, build 4c52b90 

Dockerの起動

デスクトップにあるクジラのアイコンをクリックすると、起動します。右下のバーにクジラアイコンがあれば、Dockerが起動しています。

images.png

Railsアプリ用のディレクトリを作成

例えば現在のディレクトリがrailsというディレクトリだとします。

$ pwd 
/rails 

そして、これから作成するアプリ用のディレクトリを以下のように作り、そこに移動します。ここでは、ディレクトリ名をdocker_sample_appとします。

$ mkdir docker_sample_app 
$ cd docker_sample_app 
$ pwd 
  /rails/docker_sample_app

Dockerファイルなどの準備

以下の4つのファイルを作成してください。

$ touch docker-compose.yml Dockerfile Gemfile Gemfile.lock 

そして、VScode内でファイルを変更していきます。

docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7
    ports:
      - "3306:3306"
    restart: always
    environment:
      - MYSQL_DATABASE=app_name_db
      - MYSQL_ROOT_PASSWORD=password
    volumes:
      - ./data:/var/lib/mysql:rw
    command: --innodb_use_native_aio=0
  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/app_name
    ports:
      - "3000:3000"
    depends_on:
      - db
    environment:
      DB_HOST: db
Dockerfile.
FROM ruby:2.5.3
ENV LANG C.UTF-8
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
RUN gem install bundler
WORKDIR /tmp
ADD Gemfile Gemfile
ADD Gemfile.lock Gemfile.lock
RUN bundle install
ENV APP_HOME /app_name
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME
ADD . $APP_HOME

docker-compose.ymlは上から9, 18行目のapp_nameを、Docker.fileは下から4行目の箇所を、先ほど作成したディレクトリ名に変えてください。(ここではdocker_sample_app)

docker-compose.yml内にportが二つあります。db側はmysql workbenchからアクセスするためのポートで、web側はブラウザでlocalhostでアクセスするためのものです。
正直workbenchは今回のサーバ起動までという目的には含まれてないので、気にしなくて大丈夫です。

Gemfile.
source 'https://rubygems.org' 
gem 'rails' 

Gemfile.lockは空で問題ないです。

Railsアプリの作成

ターミナルで、このコマンドをうってください。

$ docker-compose run web rails new . -d mysql --skip-bundle 

途中で

Overwrite /docker_sample_app/Gemfile? (enter "h" for help) [Ynaqdhm] 

と聞かれます。そこは、「y」とタイプしてください。 すると、ディレクトリ内にrails newした時と同じようなファイルが作成されているのが分かると思います。 次に、プロジェクトをビルドします。

$ docker-compose build 

このコマンドでDockerfileの内容が実行されます。 おめでとうございます。あと、もうすこしです。

サーバーを立ち上げる

以下のコマンドをうつことで、サーバーを立ち上げれます。

$ docker-compose up -d

この状態でブラウザに行って、localhostにアクセスしようとすると、

Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2 "No such file or directory") 

といったエラーが出ると思います。これは、まだdatabaseファイルを設定していないからです。なので、変更してあげます。

database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password
  host: <%= ENV['DB_HOST'] %>
development:
  <<: *default
  database: app_name_development
test:
  <<: *default
  database: app_name_test

今回は、本番環境を用意する必要ではないので、Productionは消しました。先ほどと同じように、app_nameをディレクトリ名(docker_sample_app_developmentのように)に変更してください。  

そしたら、データベースファイルをいじったので、サーバーを再起動します。

$ docker-compose down
$ docker-compose up -d

そして、ブラウザをリロードすると、違うエラーが出ます。

Unknown database 'app_name_development'

これは、「データベースを作成してください」というエラーなので、言われるがままにデータベースを作りましょう。

docker-compose exec web rails db:create

ブラウザに戻ってください。localhost:3000にアクセスすると無事、例の画面が出るはずです!!!

20190427151753.png

これで今回の目標は達成されました!

docker-composeのコマンドについて

rails c, rails db:createなどのコマンドがありますが、docker-composeを使うときは、以下のコマンドを必ずその前に付けなければなりません。

docker-compose exec web 

なので、例えば、データベースを作りたい時は

docker-compose exec web rails db:create

となります!

ちなみにrails sコマンドは使いません。
docker-compose up -dでサーバが起動するからです。

[番外編]

ルーティングファイルなどを変更したらサーバーを再起動させますが、viewsファイルに関しては変更してもサーバの再起動は、必要ないのですが、反映されないことが分かりました。その場合は、development.rbファイルを変更する必要があることが分かりました。

development.rb
config.file_watcher = ActiveSupport::EventedFileUpdateChecker

のEventedFileUpdateCheckerのところをFileUpdateCheckerに変更します。

development.rb
config.file_watcher = ActiveSupport::FileUpdateChecker

参考にさせていただいた記事

dockerでrails5環境構築

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

Ruby on Rails [render]について

Ruby on Rails [render]について

Ruby on Rails学習中、renderメソッドに関して、色々調べたので備忘録の意味も含め、まとめました。

そもそもrenderメソッドとは

-renderメソッドとは:コントローラーファイル内定義したアクションにて、呼び出すViewファイルを指定するメソッド
-いつ使う?:テンプレートファイルを使用する時
*この時使用するテンプレートファイル名は「_」から記載します。
複数回、同じHTMLを呼び出す時に使えます。

renderメソッドの使い方

下記を仮置きとして設定します。
[コントローラ]
def index
@tweets = Tweet.all.order("created_at DESC")
end
[テンプレートファイル名]
_tweet.html.erb

  1. 一回だけテンプレートファイルを使う時

パターンA
[index viewファイル]
<%= render partial:'tweet', locals:{tweet(部分テンプレート内で使用する変数),tweet} %>

パターンB*超特例
[index viewファイル]
<%= render @tweet %>
*条件(下記の時のみ使用可能)
 テンプレートファイル名 = 部分テンプレート内で使用する変数名

2.複数回テンプレートファイルを使う時
パターンA
[index viewファイル]
<% @tweets(コントローラ内インスタンス変数).each do |tweet| %>
<%= render partial:'tweet', locals:{tweet(部分テンプレート内で使用する変数),tweet} %>
<% end %>

パターンB
[index viewファイル]
<%= render partial:'tweet', collection @tweets %>
*パターンAの短縮系

パターンC*超特例
[index viewファイル]
<%= render @tweets %>
*条件(下記の時のみ使用可能)
 テンプレートファイル名 = 部分テンプレート内で使用する変数名

ちなみに。。

変数を複数設定する場合は、locals:{},{},{}...という記載で追記します。

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

RSpecのletでcreateした時に気をつけること 〜遅延評価〜

環境
Rails 6.0.0
RSpec 3.9
factory_bot 5.1.1

自分がプチハマりしたので備忘録も兼ねて。

RSpecを書いているとよく見る以下のようなコード。

before do
  @user = create(:user)
end
let(:user) { create(:user) }

この2つはほぼ同じ事をしています。
factoryファイルを元にDBにユーザ情報を作成、前者は@user、後者はuserという変数に代入をします。

ここだけ見ていると、letを使った方が1行で書けるし、letだけ使っていれば問題なさそうな気もします。
しかし注意しなければいけないことがあります。

以下のように、代入した変数を使わずにActive Recordにアクセスする場合、letだとエラーが発生します。

User.find 1

=> ActiveRecord::RecordNotFound: Couldn't find User with 'id'=1

これは、letが遅延評価される(代入した変数が参照されたタイミングで評価される)ために起こります。
User.find user.id ならエラーになりません。

遅延評価されたくない場合はletが使えないのかというと、そういうわけではありません。
遅延評価をしないlet!が用意されています。
let!(:user) { create(:user) } とすれば、テストを行う前(before doと同じタイミング)に実行されます。

遅延評価にも、必要のない時まで処理をしなくてすむといったメリットがありますので、適切な方を選べるようにしたいですね。

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

Railsのcheck_boxにデフォルトでチェックを付ける方法

環境

  • Rails 5.2.3
  • Ruby 2.6.5

失敗例

check_box{ checked: true }をつけるとチェックは付きますが、チェックを外してもtrueがサーバに送られてしまいます。

<%= f.check_box :published, { checked: true }, true, false %>

成功例

controllerでnewする時にセットします。

def new
  @book = Book.new(published: true)
end

check_box{ checked: true }は付けない。

<%= f.check_box :published, {}, true, false %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails チュートリアル 第9章 まとめ

論理の流れが全くイメージ出来ていないので、要復習
Cookiesはブラウザのみで保存する

1 remember_digestをハッシュ化させた値を保存しておく場所を作成
rails generate migration add_remember_digest_to_users remember_digest:string
rails db:migrate
2 ランダムな文字列を作成
SecureRandom.urlsafe_base64

3 has_secure_passwordがやってくれていたことを自分で実装する必要がある
u.passwordに代入できるが、データベースに保存しないという機能(仮想的な属性にアクセス)
今はu.rememberとすることはできない
仮想的な属性にアクセスする部分の実装(setterとgetter)

def remember= (token)
   @remember = token
end
def remember
   @remember
end

よく使うのでRubyで実装されており、この一行に短縮できる

attr_accessor :remember

4.保存

def remember
# Userがログイン状態を保持したいと思い、チェックボックスにチェック(remember me)し、
# ログインが成功した時に呼び出されるメソッド
    self.remember_token = User.new_token
# 新しいトークンを発行し、このメソッドを呼び出したオブジェクト(self)のremember_tokenに入れる
# remember_tokenは次のリクエストが来るまでしか有効ではない
    update_attribute(:remember_digest, User.digest(remember_token))
# new_tokenは開発側が作った値なので、これまでのようにvaliをかける必要がないので
# update_attribute()を使う
# update_attribute(name,value) = (属性名・カラム?,値・更新後の)
# remember_tokenがハッシュ化された値が、DBに保存
 end

認証

bcryptの認証
渡されたトークンとダイジェストが一致したらtrue

def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
sessions_helper
def remember(user)
    user.remember # DBへremember_digestを保存
    cookies.permanent.signed[:user_id] = user.id
# userのidも暗号化してcookiesに入れる
# 文法はあまり考えないでいい
    cookies.permanent[:remember_token] = user.remember_token
# remember_tokenをユーザーのブラウザに入れる
end

if user_id = session[:user_id] 
# user_idというローカル変数にsessionの値を代入
# 代入されたローカル変数の値を条件式で評価、比較演算子(==)ではない
# 評価されるので数字かnilが返る、数字ならtrue
      @current_user ||= User.find_by(id: user_id) # ここまでは同じ
elsif (user_id = cookies.signed[:user_id])
# 復号化されて実際のuser_idになる
      user = User.find_by(id: user_id)
      # 数字が返ってきたら、user_idをfind_byで取得し、Userオブジェクトに引っ掛ける
      if user && user.authenticated?(cookies[:remember_token])
      #Userのcookiesにあるremember_tokenとremember_digestを照合
        log_in user
        @current_user = user
      end
end

Userを忘れる
rememberメソッドと対になる

def forget
    update_attribute(:remember_digest, nil) #remember_digestをnilに
end

remember(user)の対

def forget(user)
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
end
def log_out
    forget(current_user) #上のメソッドを呼んでいる
    session.delete(:user_id)
    @current_user = nil
end 

連続ログアウト問題
二つのデバイスでログインした状態で、片方をログアウトさせると、もう片方がログアウトする時にはlog_outメソッドのforget(current_user)のcurrent_userがいないのでnilが渡されErrorとなる

解決策として、log_outメソッドを呼び出す時、nilじゃないことを確認する
destroyアクションで
log_out if logged_in?
ログアウトするできるのはログインしている時だけ
2回目以降のログアウトはログインしていないので、log_outが実行されず、redirectだけ実行

チェックボックスの設置

<%= f.label :remember_me, class: "checkbox inline" do %>
<%= f.check_box :remember_me %> # checkboxを作る
<span>Remember me on this computer</span>
<% end %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]devise、Omniauthを利用したGoogle、facebook認証を実装した

はじめに

久しぶりの更新になります。某スクールを先日卒業したChihaと申します。
某スクールにてチームで某フリマアプリのコピーを作成したため、実装物のコードについて記録を残しつつ、開発チームで共有する目的で当記事を書いています。

今回はOmniAuthを利用したGoogle、facebookユーザー認証機能について、私が行った作業を全部まとめて書いていこうと思います。
極力この記事のみで全て実装できるように丁寧な記事にしようと思います。
丁寧な記事が書けるとはとは言ってない

対象読者

  • SNS認証をアプリケーションに組み込みたい方
  • あれこれ色々な記事を確認しながら実装するのに疲れた方
  • 某スクールの受講生

後輩受講生が見る可能性があるのでこういう記事書いていいのかな?とも思いますがその辺は考えない

そもそもの話、OmniAuthとは?

Google、Facebook、twitter等のSNSアカウントを用いてユーザー登録やログインを行ってくれるgem。

Gemfile
gem "omniauth"
gem "omniauth-twitter"
gem "omniauth-facebook"

のように、使いたいSNSによって"omniauth-snsによって決まった名称"のgemを導入する必要があります。

OmniAuthは、複数の外部サービスのアカウント情報を使ってユーザー登録やログインを提供します。OmniAuthはサービスごとにストラテジー(Strategies)を管理する、いわば元締めのgemです。OmniAuthのストラテジーとは、外部サービスごとにOAuth認証に必要な処理が記述されており、Rackミドルウェアとして提供されます。

出典:RailsでSNS認証機能を実装しよう~定番gem「OmniAuth」活用法

ということで、やっていきましょう。

開発環境

  • Ruby on Rails 5.2.2
  • Ruby 2.5.1
  • haml
  • gem devise
  • gem omniauth

事前準備

使いたいSNSのAPI取得が必要です。
今回はgoogleとfacebookですね。
また、注意事項が数点あります。

導入にあたっての注意事項

この辺でハマったから記事書いたまである

google認証の注意事項

OAuthクライアントID(Railsに設定するAPIキーのようなもの)の取得に際して、承認済みのリダイレクトURIを登録する必要がありますが、きちんとしたドメインを設定したURLでないと登録ができません。
※ローカル環境のURLは普通に登録できます。できないのはdevelopment環境での〇〇.〇〇.〇〇のような、数値のみのURLとなるドメイン等になります。
そのため、今回はローカル環境のみの話になります。本番実装するならドメインを取得して登録するなどの対応が必要です。

facebook認証の注意事項

こいつも曲者です。
OAuthクライアントID(Railsに設定するAPIキーのようなもの)の取得に際して、承認済みのリダイレクトURIを登録する必要がありますが、SSL通信を行うURLのみ登録することができます。
つまり、httpsから始まるURLでの登録が必要であり、httpで始まるURLでは登録ができません。
※Railsを普通にセッティングして開発してた場合、ローカルサーバーを普通に$rails sすると、httpから始まるパスになります。
そのため、ローカルでもSSL通信となるような設定の変更が必要になります。

注意点まとめ

  • google認証では、本番環境ではきちんとしたドメインの取得が必要な場合がある
  • facebook認証では、httpsから始まるパスによるSSL通信をするURLを登録する必要がある

今回の記事ではローカルでの開発に絞った話をするため、ローカルでのSSL通信化のみ必要となります。

開発環境でのSSL通信化

Rails5 + pumaのローカル環境でSSL/HTTPSを有効にするを参考にしました。

SSL証明書の作成

今回はローカルで動くようにするだけなので適当な証明書をアプリのディレクトリ内に適当に作ります。

ターミナル
ssl証明書を置くディレクトリ $ openssl genrsa 2048 > server.key
ssl証明書を置くディレクトリ $ openssl req -new -key server.key > server.csr #色々入力を求められますが、全部適当で大丈夫です。
ssl証明書を置くディレクトリ $ openssl x509 -days 3650 -req -signkey server.key < server.csr > server.crt

作れたらpumaの設定を弄ります。

pumaの設定変更

以下のコードを書き足します。

puma.rb
~~中略~~
if ENV.fetch('RAILS_ENV') { 'development' } == 'development'
  ssl_bind 'localhost', '9292', {
    key: 'tmp/server.key',
    cert: 'tmp/server.crt'
  }
end

ssl_bindの後にはURLにしたい番号やらを書くといいです。
今回の場合は生成されるのはhttps://localhost:9292となります。

起動時の注意

SSL通信が可能なサーバーを起動する場合、$ rails sではなく、

ターミナル
$ bundle exec puma -C config/puma.rb

で起動します。
以後はこの起動コマンド及びURLにて作業することになります。

続いて認証のためのAPI取得などなど

google認証

まずはクライアントID及びクライアントシークレットを取得します。

クライアントIDの取得

Google Developer Console
にログインし、プロジェクトの選択 > 新しいプロジェクト
スクリーンショット 2019-10-18 18.24.13.png
スクリーンショット 2019-10-18 18.26.50.png
導入するアプリに沿ってプロジェクト名を入力し、作成します(今回は記事用のスクリーンショットなのでデフォルト名そのまま)
スクリーンショット 2019-10-18 18.29.49.png
作成したら以下の画面に遷移するので、左上の三本線(ハンバーガーアイコン)より、ナビゲーションメニューを表示し、APIとサービス > OAuth同意画面 へと遷移し、アプリ名だけ入れて保存を押します。
スクリーンショット 2019-10-18 18.38.27.png
スクリーンショット 2019-10-18 19.15.23.png
スクリーンショット 2019-10-18 19.19.30.png

保存ができたら、認証情報 > 認証情報を作成 > OAuth クライアント IDから、ID取得画面へ移動します。

スクリーンショット 2019-10-18 18.45.11.png
スクリーンショット 2019-10-18 18.46.54.png
スクリーンショット 2019-10-18 18.49.44.png
スクリーンショット 2019-10-18 19.11.13.png

今回は「ウェブ アプリケーション」を選択してください。
スクリーンショット 2019-10-18 19.25.22.png
選ぶと色々入力項目が出ますが、承認済みのリダイレクトURIに

https://localhost:9292/users/auth/google_oauth2/callback

を入れて保存してあげましょう。
保存すると、「クライアントID、クライアントシークレット」の2つが表示されます。
この2つをRailsで使用するので控えておきます。

取得できたら、認証を利用するためのAPIを有効にしましょう。

Google+ APIの有効化

左側ナビゲーションメニューからAPIとサービス > ライブラリ へ移動
スクリーンショット 2019-10-18 20.31.07.png

google+ で検索し、検索結果に出てくるgoogle+ APIを有効にします。
スクリーンショット 2019-10-18 20.34.12.png
スクリーンショット 2019-10-18 20.35.26.png
スクリーンショット 2019-10-18 20.37.01.png

これでGoogleは完了です。

facebook認証

facebook for developersへとアクセスします。
新しいアプリの追加を押してアプリ名とメールアドレスを入力し、アプリIDを作成する。
スクリーンショット 2019-10-18 20.43.26.png
スクリーンショット 2019-10-18 20.45.20.png
作成したら、ベーシックへと移動し、「アプリID」「app secret」を控えておきます。
スクリーンショット 2019-10-18 20.56.30.png
スクリーンショット 2019-10-21 14.31.28.png
控えたら左側メニューのプロダクトの横にある「+」ボタンからプロダクト追加画面へ。
スクリーンショット 2019-10-21 15.05.05.png

一番最初に出てくる「Facebookログイン」製品の設定を押すと、左のメニューにFacebookログインの項目が追加されます。
スクリーンショット 2019-10-21 15.07.42.png
表示されたら、左メニュー「設定」から、OAuthリダイレクトURIを設定します。

https://localhost:9292/users/auth/facebook/callback

今回はpumaの設定に沿って以下のURIを登録します。問題なければ「変更を保存」して完了です。

スクリーンショット 2019-10-21 15.35.47.png

何も問題なければ、これでいけると思います。

これでSNS側での設定は完了しました。続いてRails側のコード。

機能実装

今回はdeviseのomniauth_callbackを利用します。

テーブル

devise経由で作成したusersテーブルにuid、providerの2項目を追加します。(ログイン時に認証するために保存が必要になります)

ターミナル
$ rails g migrate 適当な名前

して適当なmigrateファイルを作成し、uidとproviderカラムを追加する記述をします。
uidは数字だけではないのでstring型にしましょう。integer型だと保存できません。

migrateファイル
class AddOmniauthToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :provider, :string
    add_column :users, :uid, :string
  end
end

書いたら

ターミナル
$ rails db:migrate

して、テーブルの準備は完了です。
最終的なテーブル構成はこんな感じに。

テーブル構成

Column Type Options
nickname string null: false
email string null: false,unique: true
encrypted_password string null: false
uid string
provider string

アソシエーションや他テーブルは今回の実装に関係ないので割愛します。

ルーティング

deviseで生成されるomniauth_callbacks_controller.rbを使用するため、コントローラを明示する記述を追加します。

routes.rb
Rails.application.routes.draw do
  devise_for :users,controllers: {omniauth_callbacks: "users/omniauth_callbacks"}
end

Controller

controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  #facebookとgoogle_oauth2の2つを定義
  def facebook
    callback_from :facebook
  end

  def google_oauth2
    callback_from :google
  end

  private

  def callback_from(provider)
    provider = provider.to_s #プロバイダを定義
    @user = User.from_omniauth(request.env['omniauth.auth']) #モデルでSNSにリクエストするメソッド(from_omniauth)を使用し、レスポンスを@userに代入
    if @user.persisted? #@userがすでに存在したらログイン処理、存在しなかったら残りの登録処理へ移行
      sign_in @user
      redirect_to root_path
    else
      #今回は複数ページに渡る登録項目があるため、情報をsessionに保存し、他のページにも持ち越せるように
      #この辺りの値は用途に合わせてアレンジしてください。
      session[:password] = @user.password
      session[:password_confirmation] = @user.password
      session[:provider] = @user.provider
      session[:uid] = @user.uid
      redirect_to registration_signup_index_path
    end
  end

end

Model

実際の認証を行う処理部分をモデルに書いています。

user.rb
class User < ApplicationRecord
  # :omniauthableの記述を追加するのを忘れないように
  devise :database_authenticatable, :registerable,
          :recoverable, :rememberable, :validatable, :omniauthable
  ~~中略~~
  # sns認証後、ユーザーの有無に応じて挙動を変更する
  def self.from_omniauth(auth)
    # uidとproviderでユーザーを検索
    user = User.find_by(uid: auth.uid, provider: auth.provider)
    if user
      #SNSを使って登録したユーザーがいたらそのユーザーを返す
      return user
    else
      #いなかった場合はnewします。
      new_user = User.new(
        email: auth.info.email,
        nickname: auth.info.name,
        uid: auth.uid,
        provider: auth.provider,
        #パスワードにnull制約があるためFakerで適当に作ったものを突っ込んでいます
        password: Faker::Internet.password(min_length: 8,max_length: 128)
      )
      return new_user
    end
  end
end

View

認証を行いたいページの適当な箇所にcallbackのリンクを仕込むだけです。

new.html.haml
#見やすさのためにclassや他の記述は省いています。
= link_to user_facebook_omniauth_authorize_path do
  = 'Facebookで登録する'
= link_to user_google_oauth2_omniauth_authorize_path do
  = 'Googleで登録する'

クライアントIDの設定

secret.ymlに取得したクライアントID・クライアントシークレットを記載します。

secret.yml
development:
  google_client_id: <%= ENV["GOOGLE_CLIENT_ID"] %>
  google_client_secret: <%= ENV["GOOGLE_CLIENT_SECRET"] %>

  facebook_client_id: <%= ENV["FACEBOOK_CLIENT_ID"] %>
  facebook_client_secret: <%= ENV["FACEBOOK_CLIENT_SECRET"] %>

devise.rbに、設定したクライアントID及びシークレットを読み込む記述を追加します。適当にファイル末尾に以下を記載。

devise.rb
config.omniauth :facebook,Rails.application.secrets.FACEBOOK_CLIENT_ID,Rails.application.secrets.FACEBOOK_CLIENT_SECRET
config.omniauth :google_oauth2,Rails.application.secrets.GOOGLE_CLIENT_ID,Rails.application.secrets.GOOGLE_CLIENT_SECRET

以上になります。
今回は保存処理などに関しては書いていません。認証処理と認証完了後のデータの取得を中心に記事を書きました。

最後に

スクール卒業から身の回りに積もったあれこれを消化していたら久しぶりの記事更新になりました。まだまだ書きたい項目はあるのでじゃんじゃん更新していこうと思います。
就活も頑張ります。

まだまだ粗末な点も多いと思いますが、より良いコード、間違った点などがあればご教授頂けると幸いです。

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

備忘録:Railsにおける多次元配列のリファクタリングについて(+ jsonでの受渡しについて)

前提

json形式で配列を送りたかったが、配列が別々に2つ存在するため多次元配列(2次元配列)を作る必要があった。

最初

name.rb
names = ["hideki", "takahiro", "miki"]
descriptions = ["すごい", "かっこいい", "かわいい"]

inventories = []

names.each_with_index do |name, i|
  inventories.push [name, descriptions[i]]
end

出力結果

 [["hideki", "すごい"], ["takahiro", "かっこいい"], ["miki", "かわいい"]]

改善

name.rb
names = ["hideki", "takahiro", "miki"]
descriptions = ["すごい", "かっこいい", "かわいい"]

inventories = names.zip(descriptions)

出力結果

 [["hideki", "すごい"], ["takahiro", "かっこいい"], ["miki", "かわいい"]]

メモ:受け取り側での処理

上記をjson形式で送る

name.rb
render json: inventories

ループさせる

name.coffee
success: (json) ->
  html = ""
  for i of json
    html += "<div class='name'>#{json[i][0]}</div><div class='description'>#{json[i][1]}</div>"
  $(".names").html(html)

上記の[0][1]をループさせる方法がわからずでして、、どなたかわかる方がいらっしゃいましたら教えて頂けるとうれしみです。。

参考にした記事

Rubyの配列でごにょごにょするときzipとinjectとevalが便利すぎる件

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

rails server コマンドでエラーが出た話

目的

  • サーバをスタートするコマンド$ rails serverを実行した時のエラー解決法を知る。

エラー内容

$ MacBook-miriwo:test_app admin$ rails s
MacBook-miriwo:test_app admin$ rails s
=> Booting Puma
=> Rails 6.0.0 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
Traceback (most recent call last):
    80: from bin/rails:3:in `<main>'
    79: from bin/rails:3:in `load'
    78: from /Users/admin/Documents/test/test_app/bin/spring:15:in `<top (required)>'
    77: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
・
・
・
MacBook-miriwo:test_app admin$ rails webpacker:install
Yarn not installed. Please download and install Yarn from https://yarnpkg.com/lang/en/docs/install/

解決方法

  • コマンド$ rails webpacker:installを実行しようとしたところでエラーがでた。
$ rails webpacker:install
Yarn not installed. Please download and install Yarn from https://yarnpkg.com/lang/en/docs/install/
  • yarnが入っていないためエラーが出たようなのでコマンドbrew install yarnを実行して入れた。
$ brew install yarn
==> Installing dependencies for yarn: node
==> Installing yarn dependency: node
==> Downloading https://homebrew.bintray.com/bottles/node-12.12.0.high_sierra.bottle.tar.gz
==> Downloading from https://akamai.bintray.com/0f/0f35e88be5a84c808dba472d053af25639b300c095392f63e85d9ae94cf12b20
・
・
・
  • コマンド$ rails webpacker:installを実行した。
$ rails webpacker:install
RAILS_ENV=development environment is not defined in config/webpacker.yml, falling back to production environment
      create  config/webpacker.yml
Copying webpack core config
      create  config/webpack
      create  config/webpack/development.js
      create  config/webpack/environment.js
・
・
・
  • コマンド$ rails serverを実行したところ、正常にローカルサーバが起動した。
$ rails s
=> Booting Puma
=> Rails 6.0.0 application starting in development 
=> Run `rails server --help` for more startup options
Puma starting in single mode...
* Version 3.12.1 (ruby 2.6.4-p104), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://localhost:3000
・
・
・

付録

  • コマンド$ rails serverを実行した時のエラーを記載する。
$ rails s
=> Booting Puma
=> Rails 6.0.0 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
Traceback (most recent call last):
    80: from bin/rails:3:in `<main>'
    79: from bin/rails:3:in `load'
    78: from /Users/admin/Documents/test/test_app/bin/spring:15:in `<top (required)>'
    77: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
    76: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
    75: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/binstub.rb:11:in `<top (required)>'
    74: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/binstub.rb:11:in `load'
    73: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/spring-2.1.0/bin/spring:49:in `<top (required)>'
    72: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client.rb:30:in `run'
    71: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/command.rb:7:in `call'
    70: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/rails.rb:28:in `call'
    69: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/rails.rb:28:in `load'
    68: from /Users/admin/Documents/test/test_app/bin/rails:9:in `<top (required)>'
    67: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activesupport-6.0.0/lib/active_support/dependencies.rb:325:in `require'
    66: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activesupport-6.0.0/lib/active_support/dependencies.rb:291:in `load_dependency'
    65: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activesupport-6.0.0/lib/active_support/dependencies.rb:325:in `block in require'
    64: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:30:in `require'
    63: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require_with_bootsnap_lfi'
    62: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register'
    61: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `block in require_with_bootsnap_lfi'
    60: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require'
    59: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/commands.rb:18:in `<main>'
    58: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/command.rb:46:in `invoke'
    57: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/command/base.rb:65:in `perform'
    56: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/thor-0.20.3/lib/thor.rb:387:in `dispatch'
    55: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/thor-0.20.3/lib/thor/invocation.rb:126:in `invoke_command'
    54: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/thor-0.20.3/lib/thor/command.rb:27:in `run'
    53: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/commands/server/server_command.rb:138:in `perform'
    52: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/commands/server/server_command.rb:138:in `tap'
    51: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/commands/server/server_command.rb:147:in `block in perform'
    50: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/commands/server/server_command.rb:37:in `start'
    49: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/commands/server/server_command.rb:77:in `log_to_stdout'
    48: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/rack-2.0.7/lib/rack/server.rb:354:in `wrapped_app'
    47: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/rack-2.0.7/lib/rack/server.rb:219:in `app'
    46: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/rack-2.0.7/lib/rack/server.rb:319:in `build_app_and_options_from_config'
    45: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/rack-2.0.7/lib/rack/builder.rb:40:in `parse_file'
    44: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/rack-2.0.7/lib/rack/builder.rb:49:in `new_from_string'
    43: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/rack-2.0.7/lib/rack/builder.rb:49:in `eval'
    42: from config.ru:in `<main>'
    41: from config.ru:in `new'
    40: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/rack-2.0.7/lib/rack/builder.rb:55:in `initialize'
    39: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/rack-2.0.7/lib/rack/builder.rb:55:in `instance_eval'
    38: from config.ru:3:in `block in <main>'
    37: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:48:in `require_relative'
    36: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activesupport-6.0.0/lib/active_support/dependencies.rb:325:in `require'
    35: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activesupport-6.0.0/lib/active_support/dependencies.rb:291:in `load_dependency'
    34: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activesupport-6.0.0/lib/active_support/dependencies.rb:325:in `block in require'
    33: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/zeitwerk-2.2.0/lib/zeitwerk/kernel.rb:23:in `require'
    32: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:30:in `require'
    31: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require_with_bootsnap_lfi'
    30: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register'
    29: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `block in require_with_bootsnap_lfi'
    28: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require'
    27: from /Users/admin/Documents/test/test_app/config/environment.rb:5:in `<main>'
    26: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/application.rb:363:in `initialize!'
    25: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/initializable.rb:60:in `run_initializers'
    24: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/tsort.rb:205:in `tsort_each'
    23: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/tsort.rb:226:in `tsort_each'
    22: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/tsort.rb:347:in `each_strongly_connected_component'
    21: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/tsort.rb:347:in `call'
    20: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/tsort.rb:347:in `each'
    19: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/tsort.rb:349:in `block in each_strongly_connected_component'
    18: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/tsort.rb:431:in `each_strongly_connected_component_from'
    17: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/tsort.rb:350:in `block (2 levels) in each_strongly_connected_component'
    16: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/tsort.rb:228:in `block in tsort_each'
    15: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/initializable.rb:61:in `block in run_initializers'
    14: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/initializable.rb:32:in `run'
    13: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/initializable.rb:32:in `instance_exec'
    12: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/railtie.rb:84:in `block in <class:Engine>'
    11: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker.rb:27:in `bootstrap'
    10: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/commands.rb:14:in `bootstrap'
     9: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/manifest.rb:18:in `refresh'
     8: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/manifest.rb:83:in `load'
     7: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/configuration.rb:47:in `public_manifest_path'
     6: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/configuration.rb:43:in `public_output_path'
     5: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/configuration.rb:39:in `public_path'
     4: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/configuration.rb:80:in `fetch'
     3: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/configuration.rb:84:in `data'
     2: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/configuration.rb:88:in `load'
     1: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/configuration.rb:88:in `read'
/Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/configuration.rb:88:in `read': No such file or directory @ rb_sysopen - /Users/admin/Documents/test/test_app/config/webpacker.yml (Errno::ENOENT)
    79: from bin/rails:3:in `<main>'
    78: from bin/rails:3:in `load'
    77: from /Users/admin/Documents/test/test_app/bin/spring:15:in `<top (required)>'
    76: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
    75: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
    74: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/binstub.rb:11:in `<top (required)>'
    73: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/binstub.rb:11:in `load'
    72: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/spring-2.1.0/bin/spring:49:in `<top (required)>'
    71: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client.rb:30:in `run'
    70: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/command.rb:7:in `call'
    69: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/rails.rb:28:in `call'
    68: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/spring-2.1.0/lib/spring/client/rails.rb:28:in `load'
    67: from /Users/admin/Documents/test/test_app/bin/rails:9:in `<top (required)>'
    66: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activesupport-6.0.0/lib/active_support/dependencies.rb:325:in `require'
    65: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activesupport-6.0.0/lib/active_support/dependencies.rb:291:in `load_dependency'
    64: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activesupport-6.0.0/lib/active_support/dependencies.rb:325:in `block in require'
    63: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:30:in `require'
    62: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require_with_bootsnap_lfi'
    61: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register'
    60: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `block in require_with_bootsnap_lfi'
    59: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require'
    58: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/commands.rb:18:in `<main>'
    57: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/command.rb:46:in `invoke'
    56: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/command/base.rb:65:in `perform'
    55: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/thor-0.20.3/lib/thor.rb:387:in `dispatch'
    54: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/thor-0.20.3/lib/thor/invocation.rb:126:in `invoke_command'
    53: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/thor-0.20.3/lib/thor/command.rb:27:in `run'
    52: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/commands/server/server_command.rb:138:in `perform'
    51: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/commands/server/server_command.rb:138:in `tap'
    50: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/commands/server/server_command.rb:147:in `block in perform'
    49: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/commands/server/server_command.rb:37:in `start'
    48: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/commands/server/server_command.rb:77:in `log_to_stdout'
    47: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/rack-2.0.7/lib/rack/server.rb:354:in `wrapped_app'
    46: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/rack-2.0.7/lib/rack/server.rb:219:in `app'
    45: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/rack-2.0.7/lib/rack/server.rb:319:in `build_app_and_options_from_config'
    44: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/rack-2.0.7/lib/rack/builder.rb:40:in `parse_file'
    43: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/rack-2.0.7/lib/rack/builder.rb:49:in `new_from_string'
    42: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/rack-2.0.7/lib/rack/builder.rb:49:in `eval'
    41: from config.ru:in `<main>'
    40: from config.ru:in `new'
    39: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/rack-2.0.7/lib/rack/builder.rb:55:in `initialize'
    38: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/rack-2.0.7/lib/rack/builder.rb:55:in `instance_eval'
    37: from config.ru:3:in `block in <main>'
    36: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:48:in `require_relative'
    35: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activesupport-6.0.0/lib/active_support/dependencies.rb:325:in `require'
    34: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activesupport-6.0.0/lib/active_support/dependencies.rb:291:in `load_dependency'
    33: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activesupport-6.0.0/lib/active_support/dependencies.rb:325:in `block in require'
    32: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/zeitwerk-2.2.0/lib/zeitwerk/kernel.rb:23:in `require'
    31: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:30:in `require'
    30: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require_with_bootsnap_lfi'
    29: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register'
    28: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `block in require_with_bootsnap_lfi'
    27: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require'
    26: from /Users/admin/Documents/test/test_app/config/environment.rb:5:in `<main>'
    25: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/application.rb:363:in `initialize!'
    24: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/initializable.rb:60:in `run_initializers'
    23: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/tsort.rb:205:in `tsort_each'
    22: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/tsort.rb:226:in `tsort_each'
    21: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/tsort.rb:347:in `each_strongly_connected_component'
    20: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/tsort.rb:347:in `call'
    19: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/tsort.rb:347:in `each'
    18: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/tsort.rb:349:in `block in each_strongly_connected_component'
    17: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/tsort.rb:431:in `each_strongly_connected_component_from'
    16: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/tsort.rb:350:in `block (2 levels) in each_strongly_connected_component'
    15: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/2.6.0/tsort.rb:228:in `block in tsort_each'
    14: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/initializable.rb:61:in `block in run_initializers'
    13: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/initializable.rb:32:in `run'
    12: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/railties-6.0.0/lib/rails/initializable.rb:32:in `instance_exec'
    11: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/railtie.rb:84:in `block in <class:Engine>'
    10: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker.rb:27:in `bootstrap'
     9: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/commands.rb:14:in `bootstrap'
     8: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/manifest.rb:18:in `refresh'
     7: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/manifest.rb:83:in `load'
     6: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/configuration.rb:47:in `public_manifest_path'
     5: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/configuration.rb:43:in `public_output_path'
     4: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/configuration.rb:39:in `public_path'
     3: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/configuration.rb:80:in `fetch'
     2: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/configuration.rb:84:in `data'
     1: from /Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/configuration.rb:87:in `load'
/Users/admin/.anyenv/envs/rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/webpacker-4.0.7/lib/webpacker/configuration.rb:91:in `rescue in load': Webpacker configuration file not found /Users/admin/Documents/test/test_app/config/webpacker.yml. Please run rails webpacker:install Error: No such file or directory @ rb_sysopen - /Users/admin/Documents/test/test_app/config/webpacker.yml (RuntimeError)
MacBook-miriwo:test_app admin$ rails webpacker:install
Yarn not installed. Please download and install Yarn from https://yarnpkg.com/lang/en/docs/install/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsで論理削除(soft delete)を実装する

環境

  • Rails 5.2.3
  • Ruby 2.6.5
  • Discard 1.1.0

注意:Gemの選択

  • paranoiaは、自らのREAMDEで「新規にプロジェクトに導入するのは非推奨(※)」としている。
  • 同READMEで推奨されているdiscardを使う。

(※)非推奨の背景

  • ActiveRecordのdeletedestroyをoverrideしているため、開発者が予期しない挙動をする。
  • dependent: :destroy関連のレコードは削除(物理削除)される。
  • バグフィックスと、Railsの新しいバージョンへの対応は行うが、新しいfeatureは受け付けない。

Discardの導入

Gemのインストール

Gemfile
gem "discard"
$ bundle install

db migration

$ rails generate migration add_discarded_at_to_posts discarded_at:datetime:index

以下のようなファイルが生成される。

class AddDiscardedAtToCatalogs < ActiveRecord::Migration[5.2]
  def change
    add_column :catalogs, :discarded_at, :datetime
    add_index :catalogs, :discarded_at
  end
end
$ rails db:migrate

モデルに定義追加

class Post < ApplicationRecord
  include Discard::Model
end

使い方

削除

destroyの代わりに、discardを使う。

@post.discard

コマンド実行例

# 削除
post.discard       # => true
# 確認
post.discarded?    # => true

# 強制削除。既に削除済の場合は、exceptionが発生する。
post.discard!      # => true
post.discard!      # Discard::RecordNotDiscarded: Failed to discard the record

# 削除したレコードを元に戻す
post.undiscard     # => true
post.undiscard!    # => Discard::RecordNotUndiscarded: Failed to undiscard the record
post.discarded_at  # => nil

# 削除した日時を確認
post.discarded_at  # => Mon, 21 Oct 2019 14:34:41 JST +09:00

# 削除されたレコード一覧
Post.discarded     # => [#<Post:0x00007fc04dbe3010 ...]
# 削除されていないレコード一覧
Post.kept          # => []

default_scopeの導入について

デフォルトでは、Post.allは削除されたレコードも含めて返す。
この挙動を変えて削除されていないものだけ返すようにするには、default_scope -> { kept }を設定する。

class Post < ApplicationRecord
  include Discard::Model
  default_scope -> { kept }
end

Post.all                       # 削除さけていないレコードのみ
Post.with_discarded            # 全てのレコード
Post.with_discarded.discarded  # 削除されたレコードのみ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsで論理削除(soft delete)を実装する(discard版)

環境

  • Rails 5.2.3
  • Ruby 2.6.5
  • Discard 1.1.0

Gemの選択に要注意

  • paranoiaは、自らのREAMDEで「新規にプロジェクトに導入するのは非推奨(※)」としている。
  • 同READMEで推奨されているdiscardを使う。

(※)非推奨の背景

  • ActiveRecordのdeletedestroyをoverrideしているため、開発者が予期しない挙動をする。
  • dependent: :destroy関連のレコードは削除(物理削除)される(※期待動作ではない)。

上記に伴い、バグフィックスと、Railsの新しいバージョンへの対応は行うが、新しいfeatureは受け付けていない。

Discardの導入

Gemのインストール

Gemfile
gem "discard"
$ bundle install

db migration

$ rails generate migration add_discarded_at_to_posts discarded_at:datetime:index

以下のようなファイルが生成される。

class AddDiscardedAtToCatalogs < ActiveRecord::Migration[5.2]
  def change
    add_column :catalogs, :discarded_at, :datetime
    add_index :catalogs, :discarded_at
  end
end
$ rails db:migrate

モデルに定義追加

class Post < ApplicationRecord
  include Discard::Model
end

使い方

削除

destroyの代わりに、discardを使う。

@post.discard

コマンド実行例

# 削除
post.discard       # => true
# 確認
post.discarded?    # => true

# 強制削除。既に削除済の場合は、exceptionが発生する。
post.discard!      # => true
post.discard!      # Discard::RecordNotDiscarded: Failed to discard the record

# 削除したレコードを元に戻す
post.undiscard     # => true
post.undiscard!    # => Discard::RecordNotUndiscarded: Failed to undiscard the record
post.discarded_at  # => nil

# 削除した日時を確認
post.discarded_at  # => Mon, 21 Oct 2019 14:34:41 JST +09:00

# 削除されたレコード一覧
Post.discarded     # => [#<Post:0x00007fc04dbe3010 ...]
# 削除されていないレコード一覧
Post.kept          # => []

default_scopeの導入について

デフォルトでは、Post.allは削除されたレコードも含めて返す。
この挙動を変えて削除されていないものだけ返すようにするには、default_scope -> { kept }を設定する。

class Post < ApplicationRecord
  include Discard::Model
  default_scope -> { kept }
end

Post.all                       # 削除さけていないレコードのみ
Post.with_discarded            # 全てのレコード
Post.with_discarded.discarded  # 削除されたレコードのみ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsで論理削除(soft delete)を実装する(discard gem利用)

環境

  • Rails 5.2.3
  • Ruby 2.6.5
  • Discard 1.1.0

Gemの選択に要注意

  • paranoiaは、自らのREAMDEで「新規にプロジェクトに導入するのは非推奨(※)」としている。
  • 同READMEで推奨されているdiscardを使う。

(※)非推奨の背景

  • ActiveRecordのdeletedestroyをoverrideしているため、開発者が予期しない挙動をする。
  • dependent: :destroy関連のレコードは削除(物理削除)される(※期待動作ではない)。

上記に伴い、バグフィックスと、Railsの新しいバージョンへの対応は行うが、新しいfeatureは受け付けていない。

Discardの導入

Gemのインストール

Gemfile
gem "discard"
$ bundle install

db migration

$ rails generate migration add_discarded_at_to_posts discarded_at:datetime:index

以下のようなファイルが生成される。

class AddDiscardedAtToCatalogs < ActiveRecord::Migration[5.2]
  def change
    add_column :catalogs, :discarded_at, :datetime
    add_index :catalogs, :discarded_at
  end
end
$ rails db:migrate

モデルに定義追加

class Post < ApplicationRecord
  include Discard::Model
end

使い方

削除

destroyの代わりに、discardを使う。

@post.discard

コマンド実行例

# 削除
post.discard       # => true
# 確認
post.discarded?    # => true

# 強制削除。既に削除済の場合は、exceptionが発生する。
post.discard!      # => true
post.discard!      # Discard::RecordNotDiscarded: Failed to discard the record

# 削除したレコードを元に戻す
post.undiscard     # => true
post.undiscard!    # => Discard::RecordNotUndiscarded: Failed to undiscard the record
post.discarded_at  # => nil

# 削除した日時を確認
post.discarded_at  # => Mon, 21 Oct 2019 14:34:41 JST +09:00

# 削除されたレコード一覧
Post.discarded     # => [#<Post:0x00007fc04dbe3010 ...]
# 削除されていないレコード一覧
Post.kept          # => []

default_scopeの導入について

デフォルトでは、Post.allは削除されたレコードも含めて返す。
この挙動を変えて削除されていないものだけ返すようにするには、default_scope -> { kept }を設定する。

class Post < ApplicationRecord
  include Discard::Model
  default_scope -> { kept }
end

Post.all                       # 削除さけていないレコードのみ
Post.with_discarded            # 全てのレコード
Post.with_discarded.discarded  # 削除されたレコードのみ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

〜誰ワカ〜 MVC ルーティング・コントローラー・ビューの基本関係(ついでにモデル)

「MVC」 (コントローラー、ビュー + モデル)の関係・流れの基本(基礎編)

Ruby on Railsにおいて、画面を表示させるまでに知っておくべきこと(2)

こんにちは。 〜誰ワカ〜 Ruby on Rails攻略 のコムリンです。

このページでは、Ruby on Rails攻略において必須だけど必須じゃない!?「MVC」についてです。

「MVC」は、Ruby on Railsの基礎、基本的な概念です。が、
でも、知っていて損はありませんが、知らなくても問題ないかと思います。

(学習しているうちにいつの間にか分かるので)

初学者にはなかなか飲み込む事が難しいと思うし、実際に手を動かさないとイメージが湧かないからです。

なので、とりあえず的な感じでさらっと行きましょう!

MVCとは、
「モデル」「ビュー」「コントローラー」の略。

モデル(Model) はデータを管理するところ。
ビュー(View) は表示画面を管理するところ。
コントローラー(Controller) はモデルとビューを繋げたり処理したりするところ。

です!!

こういう関係があるから、複雑な処理が必要なアプリやサイトがうまく動くのです!!!
それをわざわざ「MVC」なんていうかっこいい名前をつけるから・・・なんか難しく感じちゃいません!?w
なんかすごいシステムの仕組みかと思ってましたが、よくよく考えたらしごく単純明快な概念でした。

難しい説明がないことが売りの〜誰ワカ〜なので、めちゃめちゃ簡単なイメージでお伝えしました。

↓Ruby on Rails の基本的な流れ↓
https://qiita.com/comlin_memo/items/617a6e5bbe96b55c57cf

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

find, find_by, whereの違いと特徴を丁寧に

勉強会にて
- find find_by whereの使い分け
- それぞれどんな時に使うのか
- 取れるデータはどんな形か

が理解できていない人が多かったので勉強会用資料として書きます。
※初心者向けですのでわかりやすさ重視を心掛けました

findメソッド

自動で作られて勝手に連番になってくれるidってありますよね。
このidを絞り込みの条件にしてデータを取得する

todo2.gif

こんな感じで作ると

id title created_at updated_at
1 ああああ 2019-10-18 04:05:36.776003 2019-10-18 04:05:36.776003
2 買い物 2019-10-18 04:05:43.090180 2019-10-18 04:05:43.090180
3 帰宅 2019-10-18 04:05:51.098753 2019-10-18 04:05:51.098753

こんな感じでデータベースに登録されます。
で、その最初の列のidを使って検索します。

2.5.3 :001 > Todo.find(1)
  Todo Load (0.2ms)  SELECT  "todos".* FROM "todos" WHERE "todos"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
 => #<Todo id: 1, title: "ああああ", created_at: "2019-10-18 04:05:36", updated_at: "2019-10-18 04:05:36">

データが1件取れます。同じidは存在しないので1件しか取れないのが当たり前ですが一応。

試しにidが10を条件にして探してみる。

2.5.3 :002 > Todo.find(10)
Todo Load (0.1ms)  SELECT  "todos".* FROM "todos" WHERE "todos"."id" = ? LIMIT ?  [["id", 10], ["LIMIT", 1]]
Traceback (most recent call last):
        1: from (irb):2
ActiveRecord::RecordNotFound (Couldn't find Todo with 'id'=10)

そんなデータないです!
というエラーが出る。これ注意です。whereではエラーは出ません。find_byではnilです。

whereで存在しないidを条件に検索してみる

2.5.3 :003 > Todo.where(id: 10)
  Todo Load (0.1ms)  SELECT  "todos".* FROM "todos" WHERE "todos"."id" = ? LIMIT ?  [["id", 10], ["LIMIT", 11]]
 => #<ActiveRecord::Relation []> 

カラの配列[]が取得されてます。
同じようなことをしても動作が少し違います。

find_byメソッド

一概には言えないですけどfindの上位版とでもいうべきかも。ただし記述が少し長くなる。

  • findidのみでしたが、find_byid以外もOK
  • もちろんidでの検索もできる
  • 条件を複数設定することが可能
  • 取得できるデータが最初に見つかった1件(超重要!!)
id title created_at updated_at
1 ああああ 2019-10-18 04:05:36.776003 2019-10-18 04:05:36.776003
2 買い物 2019-10-18 04:05:43.090180 2019-10-18 04:05:43.090180
3 帰宅 2019-10-18 04:05:51.098753 2019-10-18 04:05:51.098753
4 ああああ 2019-10-18 04:05:56.098753 2019-10-18 04:05:56.098753

例えばさっきのデータに同じtitleのデータを追加します。

2.5.3 :001 > Todo.find_by(title: "ああああ")
  Todo Load (0.1ms)  SELECT  "todos".* FROM "todos" WHERE "todos"."title" = ? LIMIT ?  [["title", "ああああ"], ["LIMIT", 1]]
 => #<Todo id: 1, title: "ああああ", created_at: "2019-10-18 04:05:36", updated_at: "2019-10-18 09:17:15"> 

1件目のデータを取得しています。

あと、データがない場合にエラーでなくて nil です。

2.5.3 :004 > Todo.find_by(title: "いいいい")                                                                    
  Todo Load (0.1ms)  SELECT  "todos".* FROM "todos" WHERE "todos"."title" = ? LIMIT ?  [["title", "いいいい"], ["LIMIT", 1]]
 => nil 

findで予期しないでnilが返ってくるとよろしくない場合あり、書くにしても少し長くなるなどfindの使い道はあります。間違いも減りますので基本的にはidで検索するときはfindでいく方がいいと思います。

whereメソッド

前述の2つは似てましたがwhereは少し違います。

  • 該当データをすべて取得 ※一件でないです
  • 取得した件数が一件でも配列(取り出し方注意)

普通やらないですが、試しにidが1の場合を検索して変数に入れます。

2.5.3 :006 > todo = Todo.where(id: 1)                                                                             
  Todo Load (0.2ms)  SELECT  "todos".* FROM "todos" WHERE "todos"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 11]]
 => #<ActiveRecord::Relation [#<Todo id: 1, title: "ああああ", created_at: "2019-10-18 04:05:36", updated_at: "2019-10-18 09:17:15">]> 

[#<Todo id: 1, title: "テスト", created_at: "2019-10-18 04:05:36", updated_at: "2019-10-18 09:17:15">]

配列で囲われていますね。→ []
というわけでTodo.where(id: 1).idみたいに取り出すことできません。
findとfind_byは

2.5.3 :007 > Todo.find(1).title
  Todo Load (0.1ms)  SELECT  "todos".* FROM "todos" WHERE "todos"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
 => "ああああ"

みたいに取り出せます。

whereなら

2.5.3 :009 > todo = Todo.where(id: 1)                                                                    
  Todo Load (0.1ms)  SELECT  "todos".* FROM "todos" WHERE "todos"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 11]]
 => #<ActiveRecord::Relation [#<Todo id: 1, title: "ああああ", created_at: "2019-10-18 04:05:36", updated_at: "2019-10-18 09:17:15">]> 
2.5.3 :010 > todo[0].id
  Todo Load (0.3ms)  SELECT "todos".* FROM "todos" WHERE "todos"."id" = ?  [["id", 1]]
 => 1 

こんな感じで取り出します。全て配列で取得するので何番目かを指定しないといけないわけです。

今回取得できたのが1件なので分かりにくいかもしれませんので複数件取得できる条件で試してみます。
Todo.where(title: "ああああ")は2件ヒットします

2.5.3 :010 > todo = Todo.where(title: "ああああ")                                                            
  Todo Load (0.1ms)  SELECT  "todos".* FROM "todos" WHERE "todos"."title" = ? LIMIT ?  [["title", "テスト"], ["LIMIT", 11]]
 => #<ActiveRecord::Relation [#<Todo id: 1, title: "ああああ", created_at: "2019-10-18 04:05:36", updated_at: "2019-10-18 09:17:15">, #<Todo id: 5, title: "ああああ", created_at: "2019-10-21 04:04:05", updated_at: "2019-10-21 04:04:05">]>

ちょっと整理すると

#<ActiveRecord::Relation 
[
#<Todo id: 1, title: "ああああ", created_at: "2019-10-18 04:05:36", updated_at: "2019-10-18 09:17:15">,
#<Todo id: 5, title: "ああああ", created_at: "2019-10-21 04:04:05", updated_at: "2019-10-21 04:04:05">
]>

こんなデータ取れてます。
Todo.where(title: "ああああ").titleとするとidが1とidが5にデータ2つあるからどっちかわからないわけです。

というわけで最初に
todo = Todo.where(id: 1)としてデータを取得した後に
todo[0].idとして配列の0番目のidを取り出しました。

ついでにさっきのtitleが「ああああ」の例なら
todo = Todo.where(title: "ああああ")としてデータを取得した後に
todo[0].title  

結果:"テスト"

todo[0].created_atなら

結果:Fri, 18 Oct 2019 04:05:36 UTC +00:00

こんな感じで取れます。(Datetimeなのでこんな形式)

検索条件に一致しなかった場合

先ほどfindのところで説明しましたが、エラーでもnilでもなくカラ配列です。

2.5.3 :003 > Todo.where(id: 10)
  Todo Load (0.1ms)  SELECT  "todos".* FROM "todos" WHERE "todos"."id" = ? LIMIT ?  [["id", 10], ["LIMIT", 11]]
 => #<ActiveRecord::Relation []> 

whereの取り出し方

2.5.3 :027 > todos = Todo.where(title: "ああああ")                                                                  
  Todo Load (0.1ms)  SELECT  "todos".* FROM "todos" WHERE "todos"."title" = ? LIMIT ?  [["title", "テスト"], ["LIMIT", 11]]
 => #<ActiveRecord::Relation [#<Todo id: 1, title: "ああああ", created_at: "2019-10-18 04:05:36", updated_at: "2019-10-18 09:17:15">, #<Todo id: 5, title: "ああああ", created_at: "2019-10-21 04:04:05", updated_at: "2019-10-21 04:04:05">]> 
2.5.3 :028 > todos.each do |todo|
2.5.3 :029 >     puts todo.title
2.5.3 :030?>   end
テスト
テスト

todos.each do |todo|をつかって取り出しています。
todosに配列形式で入っているので1つの要素ずつtodoという変数に代入しています。

繰り返し1回目の変数todo
#<Todo id: 1, title: "ああああ", created_at: "2019-10-18 04:05:36", updated_at: "2019-10-18 09:17:15">

繰り返し2回目の変数todo
#<Todo id: 5, title: "ああああ", created_at: "2019-10-21 04:04:05", updated_at: "2019-10-21 04:04:05">

このように1つずつの要素がtodoに入っていて、1回目、2回目の要素の中にはtitleが一つしかありません。
なのでtodo.titleとすればデータを特定できるので取り出すことができます。

ついでにRailsで書くと

たぶんRailsで使うと思いますので、参考程度に。

todos_controller.rb
def index
  @todos = Todo.where(title: "テスト")
end
index.html.erb
<h1>ToDo一覧</h1>
<table>
  <% @todos.each do |todo| %>
    <tr>
      <th><%= todo.title %></th>
    </tr>
  <% end %>
</table>

image.png

テストというtitleを検索して表示している例です。

余談:エラーに気が付きにくいので注意

※ 少し難しいので省いてOKです。

whereだとデータなくてもエラーでもnilでもないので注意必要です。
条件に使いたいからデータあるかどうかで条件式書こうとか言っているとエラー起こるかもしれません。

2.5.3 :015 > todo = Todo.where(title: "いいいい")                                                      
  Todo Load (0.1ms)  SELECT  "todos".* FROM "todos" WHERE "todos"."title" = ? LIMIT ?  [["title", "いいいい"], ["LIMIT", 11]]
 => #<ActiveRecord::Relation []> 
2.5.3 :016 > !!todo
 => true
2.5.3 :017 > todo = Todo.find_by(title: "いいいい")                                                               
  Todo Load (0.1ms)  SELECT  "todos".* FROM "todos" WHERE "todos"."title" = ? LIMIT ?  [["title", "いいいい"], ["LIMIT", 1]]
 => nil 
2.5.3 :018 > !!todo
 => false 

whereとfind_byで返ってくるもの違うことに注意

【まとめ】それぞれの使い分けと注意

種類 使い分け 注意
find idで特定の1件取得すればいい場合 idのみ
find_by id以外で特定の1件取得すればいい場合 最初の1件のみ & データなしでnil
where 複数のデータを取得する場合 取り出し方 & データなしでカラ配列
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

realineのエラー

要約

  • railsプロジェクトでrails sのときにreadlineのエラーが出た
  • image not foundエラー
  • rbenv使っている
  • readlineのバージョンが上がったのが原因
  • rbenvからビルドしなおせば直った

エラー

/nishizaki/.rbenv/versions/2.5.5/lib/ruby/2.5.0/x86_64-darwin18/readline.bundle, 9): Library not loaded: /usr/local/opt/readline/lib/libreadline.7.dylib (LoadError)
  Referenced from: /Users/nishizaki/.rbenv/versions/2.5.5/lib/ruby/2.5.0/x86_64-darwin18/readline.bundle
  Reason: image not found - /Users/nishizaki/.rbenv/versions/2.5.5/lib/ruby/2.5.0/x86_64-darwin18/readline.bundle

背景

rbenvでrubyの2.6.4をインストールした後に、2.5.5に戻したらエラー

rbenvを最新バージョンリストに更新するために

brew update && brew upgrade ruby-build

readlineが8にアップデート→
readline7がロードされなくなる→
readline7でビルドしていたrbenvの中のrubyがエラーになる

みたい。

こうやってビルドし直せば復活した

RUBY_CONFIGURE_OPTS=--with-readline-dir=`brew --prefix readline` rbenv install 2.5.5

https://github.com/guard/guard/wiki/Add-Readline-support-to-Ruby-on-Mac-OS-X#option-4-build-ruby-with-gnu-readline-using-rbenv-ruby_build-and-homebrew

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

Rails6環境の本番環境でBlocked hostエラーが発生したときの対処法

結論

下記のいずれかでエラーを回避できます。

※ひとまずエラーを回避するための方法として記載しておりますが、これがベストな対処法かは判断し兼ねます。対応は自己責任でお願いいたします。

エラー回避策①

config/environments/development.rbに下記を記載し、ホワイトリストに許可したいhostを追加する。

config/environments/development.rb
Rails.application.configure do
(中略)
  config.hosts << "<許可したいホスト名>"
(中略)
end

後述するエラーメッセージにしたがった方法です。
今回の私の場合、config.hosts << "thawing-caverns-37676.herokuapp.com"を記載しました。

エラー回避策②

同じくconfig/environments/development.rbに下記を記載し、ホワイトリスト全体をクリアする。
これにより、すべてのホスト名に対するリクエストを通過させることができる。

config/environments/development.rb
Rails.application.configure do
(中略)
  config.hosts.clear
(中略)
end

ただし、せっかくRails6で追加された保護機能を無効化してしまうため、推奨は①のような気がします。

発生したエラー

Rails6で開発した環境をherokuへプッシュしてアクセスしようとしたところ、下記のようなエラーが発生した。

スクリーンショット 2019-10-21 12.15.27.png

Blocked host: thawing-caverns-37676.herokuapp.com
To allow requests to thawing-caverns-37676.herokuapp.com, add the following to your environment configuration:
config.hosts << "thawing-caverns-37676.herokuapp.com"

※thawing-caverns-37676.herokuapp.comはherokuのアプリ名

原因

調べてみると、Rails6へのアップデート時の変更点の一つである
DNSリバインディング攻撃からの保護
という機能が原因のようです(Railsガイド〜Ruby on Rails 6.0 リリースノート〜参照)。

上記機能のPull Requestによると、攻撃を保護するためのActionDispatch::HostAuthorizationという新しいミドルウェアが導入されたことにより、許可するホストは自分で設定しなくてはならなくなったようです。
(デフォルトでは0.0.0.0、::、およびlocalhostからのリクエストを許可)

まとめ

対処法として、config/environments/development.rbに下記のいずれかを記載する。

対処法① ホワイトリストに許可したいhostを追加

config.hosts << "<許可したいホスト名>"

対処法② ホワイトリスト全体をクリア

`config.hosts.clear`

参考

https://www.fngtps.com/2019/rails6-blocked-host/

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

テーブルを設計する際に必要となる要素を知り、それらの要素をどういった場合に使えばいいのかを理解する

テーブルの構成要素

データベースのテーブルがどのような要素から構成されているかを学習する。

テーブルとエンティティ

エンティティ = テーブルと考えてほとんと差し支えない。
成績管理アプリを作る場合を感えると生徒、科目、成績といったエンティティが存在する。
データベースにはそれらのエンティティに対応した生徒テーブル、科目テーブル、成績テーブルを作成することになる。

テーブルの行と列

テーブルは名前の通り表の形式で構成されている。テーブルの行はレコード、列はカラムを言うがそれぞれ表している意味が異なる。

  • テーブルの行(レコード)はエンティティの具体的なデータを表す
  • テーブルの列(カラム)はエンティティの属性を表す

テーブルの行(レコード)

レコードとはエンティティの具体的なデータである。例で以下のような生徒テーブルのレコードを考えてみる。

id name email
1 山田太郎 taro@example.com
2 鈴木次郎 jiro@example.com

idが1である1行目は山田太郎さんという生徒のデータを管理している。idが2である2行目は鈴木次郎さんという生徒のデータである。
このようにレコーそはそのテーブルの表す具体的なデータ(山田太郎さん、鈴木次郎さん)を表している。

テーブルの列(カラム)

カラムとはエンティティの属性である。上記の表の例だと生徒テーブルにはid, name, email (それぞれ識別子、名前、メールアドレスの意味)という3つの属性を持っているということになる。

テーブル同士の関連性

エンティティ間には関係性のある場合がある。「エンティティ = テーブル」と考えて良いので、テーブル同士にも関係性がある場合がある。この関係性がリレーションにあたる。
例えば、生徒を成績の間には関係性がある。生徒は必ず成績を持っており、成績も必ず生徒に紐づいている(Aさんは70点、Bさんは90点など)。このような場合、生徒テーブルを成績テーブルの間にはリレーションがある。

データを識別するための特殊な属性値

属性の中にはキーと呼ばれる特殊なデータが存在する。キーは同じテーブルのレコード同士を識別するためのデータである。多くの場合、idをいう名前のつく属性がキーとなる。

キーの役割

エンティティの属性であるカラムの中にはキーと呼ばれる特殊なデータが存在する。キーの役割はレコードを識別することである。

キー
テーブルにおけるキーとはレコードを識別するための特別なカラムのことを指す。キーは識別子であるので同じテーブル内の他のレコードとは絶対に被らないように設定する。

キーの種類

キーには以下の2種類がある。

  • 主キー
  • 外部キー

主キー

主キーはあるテーブルのなかで他のレコードとの区別をつける識別子となるカラムである。そのため、同じ主キーの値を持つレコードがテーブル内に存在してはならない。
以下の生徒テーブルのidカラムが主キーになる。この時、鈴木次郎さんのレコードのidが1であってはならない。

id name email
1 山田太郎 taro@example.com
2 鈴木次郎 jiro@example.com

外部キー

外部キーは関連する他のテーブルのレコードの主キーを値として持つカラムである。外部キーは他のテーブルのレコードとの関係性を表すために用いる。
主キーの説明であげた生徒テーブルには2名の生徒がいる。主キーとなるカラムはidであったが。。。

id name email
1 山田太郎 taro@example.com
2 鈴木次郎 jiro@example.com

生徒テーブルと関係性を持つテーブルとして成績テーブルがあると仮定する。成績テーブルにはそれぞれの生徒に対応する成績が保存されている。

id score student_id
1 70 2
2 90 1

成績テーブルのidは主キーです。その他にstudent_idという属性が存在する。成績テーブルでは、このstudent_idは外部キーに当たる。これはその成績をとった生徒のレコードの主キーと対応している。
つまり成績テーブルのidが1であるレコードは生徒テーブルのidが2であるレコードと対応しており、このことから 「鈴木次郎さんは70点である」 ことが分かる。

制約で安全なテーブルを設計する

テーブルのカラムに対して制約をかけることで不正なデータや予期せぬデータが保存されることを防ぐことができる。

制約とは

制約とは特定のデータの保存を許さないためのバリデーションである。例えば同じメールアドレスのユーザーを登録できないようにする、名前のデータが空のユーザーを保存できないようにするといったことができるようになる。

制約の種類

  • NOT NULL制約
  • 一異性制約
  • 主キー制約
  • 外部キー制約

この4つの制約の挙動を具体的に確認するために実際に実装してみることにする。そこで学習するためのサンプルアプリを作成する。

● 以下の手順で 「DataBaseDesignSample」という名前のRailsアプリケーションを作成する

1. アプリケーションの作成以下のコマンドを順々に実行する。

ターミナル
$ cd #ホームディレクトリに移動
$ rails _5.0.7.2_ new DataBaseDesignSample -d mysql #mysqlでRailsアプリケーションを作成
$ cd DataBaseDesignSample
$ bundle exec rake db:create #DBの作成

2. userモデルを作成

ターミナル
$ rails g model user

ここから4つの制約を説明しつつ実際に実装してみる。

NOT NULL制約

NOT NULL制約はカラムに設定する制約である。 NOT NULL制約を設定すると、そのカラムの値にはNULL (空の値) を入れることができなくなる。絶対に値があるカラムに対して使う制約である。

NOT NULL制約
NOT NULL制約はテーブルの属性値にNULL (空の値) が入ることを許さない制約である。例えば、 usersテーブルのnameというカラムに NOT NULL制約を設定すると、 nameが空(nil)レコードは保存できなくなる。
実際にNOT NULL制約の挙動を確認してみる。

usersテーブルにNOT NULL制約を付けたnameカラムを作成する

Railsでは、マイグレーションファイルでカラムを追加するときにnull: falseと記述することでNOT NULL制約を設定することができる。

マイグレーションファイル
class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :name, null: false
      t.timestamps null: false
    end
  end
end

記述ができたらターミナルでマイグレーションを実行

ターミナル
$ bundle exec rake db:migrate

マイグレーションが実行されてusersテーブルが作成されたら、rails cを使い実際の挙動を確認してみる。

ターミナル
$ rails c
irb(main):001:0> User.create(name: "keita") //=> ユーザーが作成される
irb(main):002:0> User.create(name: nil) //=> エラー

このようにNOT NULL制約が設定されたカラムがnilであるとエラーが発生する。

一意性制約

一意性制約はカラムに設定する制約である。一意性とはユニークで他とは違う意味である。一意性制約を設定したカラムには同じ値をできなくなる。例にあげるとAさんのemailが「test@gmail.com」だった場合、他にemailが「test@gmail.com」のレコードを保存できなくなる。

● 一意性制約
一意性制約はテーブル内で重複するデータを禁止する制約である。
emailカラムに対して一意性制約を設定すると同じemailのレコードは保存できなくなる。
実際に一意性制約の挙動を確認してみる

usersテーブルに一意性制約を付けたemailカラムを作成する

emailカラムを作成するためのマイグレーションファイルを作成する。

ターミナル
$ rails g migration AddEmailToUsers email:string

Railsでは、カラムに対してadd_indexメソッドを用いることで一意性制約を付けることができる。
一異性制約

add_index :テーブル名, :カラム名, unique: true

生成されたマイグレーションファイルを以下のように編集してemailカラムに一異性制約を設定する。

マイグレーションファイル
class AddEmailToUsers < ActiveRecord::Migration
  def change
    add_column :users, :email, :string
    add_index :users, :email, unique: true
  end
end

記述したらターミナルでマイグレーションを実行

ターミナル
$ bundle exec rake db:migrate

実際の挙動をrails cで確認してみる。

ターミナル
$ rails c
irb(main):001:0> User.create(name: "taro", email: "taro@yamada.com") //=> ユーザーが作成される
irb(main):002:0> User.create(name: "yamada", email: "taro@yamada.com") //=> エラー

2回目のUser.createでエラーが起きる。これは1回目のUser.createと2回目のUser.createで同じemailでユーザーを作成していることで、一異性制約に引っかかってしまったためである。
このように一異性制約を設定したカラムの値は、唯一の値でなくてはいけない。

主キー制約

主キー制約とは、レコードが必ず主キーを持っていなくてはいけないことを保証するための制約である。

主キー制約
主キー制約は、主キーである属性値が必ず存在してかつ重複していないことを保証する制約である。主キーに対してNOT NULL制約と一意性制約を両方設定するのと同義になる。
Railsでテーブルを作成する際、主キー制約は元々実装されている。Railsでは主キーはidカラムとして自動で作成される。つまり、idカラムの値は重複しないようにできている。

外部キー制約

外部キー制約とは、外部キー制約とは、外部キーに対応するレコードが必ず存在することを保証する制約である。例えばstudent_idが3のレコードを保存するためにはstudentsテーブルにidが3のレコードが存在してなくてはならない。

外部キー制約
外部キー制約は、外部キーの対応するレコードが必ず存在しなくてはいけないという制約である。外部キーのカラムに値があっても、その値を主キーとして持つ他のテーブルのレコードがなければいけない。
実際に外部キー制約の挙動を確認してみる。

外部キー制約を実装してみる

usersテーブルの外部キーを持つためのscoreテーブルを作成する。このscoresテーブルはユーザーの成績を保存するためのテーブルである。そのため、scoresテーブルのレコードはuser_idという外部キーのカラムを持ち、どのユーザーの得点なのかがわかるようにする。

ターミナル
$ rails g model score

Railsでは、マイグレーションファイルで外部キーとなるカラムを追加するときにforeign_key: trueと記述することで外部キー制約を設定することができる。
では、生成されたマイグレーションファイルを以下のように編集してuserとのアソシエーションに外部キー制約を設定する。

マイグレーションファイル
class CreateScores < ActiveRecord::Migration
  def change
    create_table :scores do |t|
      t.string :name
      t.integer :score
      t.references :user, foreign_key: true
      t.timestamps null: false
    end
  end
end

記述ができたらターミナルでマイグレーションを実行する。

ターミナル
$ bundle exec rake db:migrate

マイグレーションを実行するとscoreテーブルにはuser_idというカラムが作成されている。このuser_idカラムは外部キーであり、外部キー制約が設定されている。
rails cで挙動を確認してみよう。usersテーブルが以下のような状態と仮定して説明する。

id name email
1 山田太郎 taro@example.com
2 鈴木次郎 jiro@example.com
ターミナル
$ rails c
irb(main):001:0> Score.create(name: "English", score: 80, user_id: 2) //レコードが生成される
irb(main):002:0> Score.create(name: "Math", score: 90, user_id: 4) //エラー

3行目では、 user_idに4を指定している。しかし、 usersテーブルにはidが4のユーザーは存在しませんから外部キー制約によってエラーが発生する。このように外部キー制約は関連先のテーブルに存在する主キーのみしか外部キーに指定することができない。

インデックスでデータの検索を高速化する

サービスでよく起きるテーブル操作の中でレコードの検索がある。例えばusersテーブル内で検索が頻繁に行われるカラムにインデックスを設定することで検索の高速化を図ることができる。

インデックスとは

インデックスはデータベースの機能の一つで、テーブル内のデータ検索を高速化することができる。インデックスはカラムに対して設定することができ、設定したカラムでの検索が高速になる。
※ インデックスを設定することを、「インデックスを貼る」と言う。

インデックス
インデックスとはテーブル内のデータの検索を高速にするための仕組みである。インデックスはカラムに対して設定する。インデックスをカラムに設定するとそのカラムで検索をした場合に検索速度が向上する。

インデックスのデメリット

インデックスで速度が上がるからといってすべてのカラムにインデックスを設定してはならない。インデックスには以下の2つのデメリットがある。

  • データを保存・更新する速度が遅くなる
  • データベースの容量を使う

データを保存・更新する速度が遅くなる

データを保存する際に、設定されているインデックスの数だけ追加でデータを作成する。インデックスを設定するカラムが増えるだけ保存するデータが増え、処理の速度が遅くなる。

データベースの容量を使う

インデックスはそのカラムで検索しやすいための特別なデータを保存するために検索速度が向上する仕組みです。そのため、インデックスを多く設定すればその分、データが必要になり容量が圧迫される。

1つのカラムに対するインデックス

テーブル内の1つのカラムにインデックスを貼る場合は、そのカラムで検索した場合に検索速度が向上する。
インデックスはmigrationファイル内で以下のように記述することで設定することができる。

migrationファイル
class AddIndexToテーブル名 < ActiveRecord::Migration
  def change
    add_index :テーブル名、 :カラム名
  end
end

1つのカラムに対するインデックスを設定してみる

DataBaseDesignSampleアプリケーションを使ってインデックスを実践してみる。 scoreテーブルに対してインデックスを貼るためのマイグレーションファイルを作成する。

ターミナル
$ rails g migration AddIndexToScores

記述したらターミナルでマイグレーションを実行する。問題なく実行できたらscoresテーブルのnameカラムに対してインデックスが設定できている。
以下のような検索の場合、検索速度が向上する。

__nameカラムによる検索

Score.where(name: '山田太郎')

複数のカラムに対するインデックス

インデックスは1つのカラムだけではなく、複数のカラムにも設定ができる。例えば、ユーザーを姓と名で検索するシステムを作っていることを想定しよう。SQLは以下のようになる。

姓と名によるユーザー検索

SELECT *
FROM users
WHERE family_name = '山田' AND first_name = '太郎'

このように検索時に2つのカラムを使う場合が多い時に複数カラムに対してインデックスを設定する。
複数のカラムにインデックスを設定するためには、migrationファイル内で以下のように記述する。

migrationファイル
class AddIndexToテーブル名 < ActiveRecord::Migration
  def change
    add_index :テーブル名, [:カラム名, :カラム名]
  end
end

複数のカラムに対するインデックスを設定してみる

DataBaseDesignSampleアプリケーションを使ってインデックスの設定を実践してみる。usersテーブルに対してインデックスを貼るためにマイグレーションファイルを作成する。

ターミナル
$ rails g migration AddIndexToUsers

作成したマイグレーションファイルを編集してnameカラムとemailカラムの2つに対してインデックスを貼ることにする。

migrationファイル
class AddIndexToUsers
  def change
    add_index :users, [:name, :email]
  end
end

記述ができたらターミナルでマイグレーションを実行する。問題なく実行できたらusersテーブルのnameカラムをemailカラムの2つで検索する場合に対するインデックスが設定できている。
以下のような検索の場合、検索速度が向上する。

nameカラムによる検索

User.where(name: '山田太郎', email: 'taro@mail.com')

※この方法でインデックスを貼るとき、emailカラム単体で検索する場合には検索速度は向上しないので注意すること。

まとめ

  • エンティティをテーブルとして定義する
  • エンティティの持つ属性をカラムとして定義する
  • カラムには主キーを必ず持たせる
  • 他のテーブルのレコードと関連がある場合、外部キーという形で他のテーブルとの関係を保存する
  • カラムの値には制約をつけてデータの正しさを保証する
  • 値が必ず設定されていることを保証する時にはNOT NULL制約を用いる
  • 値に重複がないように設定するには一意性制約を用いる
  • キーの存在を保証する時には主キー制約、外部キー制約を用いる
  • 検索する際に使うカラムにはインデックスを設定する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

フレームワークと言語の関係性は、インスタント食品と料理の違いと言える

はじめに

  • フレームワークとはいったいなんなのか?
  • 言語との違いや関係性はなんなのか?
  • アプリの開発力を高めるためには フレームワークをたくさん勉強するべき?

初心者が勉強を進めていくと一度はぶつかるこの疑問について。

たまにある、
「Ruby on Railsでの開発力を高めたいと思っている。RailsはRubyの”上位の技術”だから、Railsの勉強を集中して行うべき?」
という疑問にも回答すべく、今回はRubyとRailsを例に使って言語とフレームワークの関係性や違いを分かりやすく解説しました。

フレームワークについて

フレームワークと言語に上下関係はない

ちなみにRailsとRubyに技術的な上下関係はありません。
また、ある程度まで上達してなおRailsの開発力を高めたかったら、むしろRuby言語の勉強を優先して行った方がいいと言えます。

フレームワークでこんなものが簡単に作れる

Ruby on Railsを構築するプログラミング言語Rubyは、Webアプリを作る以外にも以下のような様々なことができます。

  • チャットボット
  • スマホアプリ
  • ゲーム
  • Webスクレイピング
  • Webクローリング

Ruby on Railsは「Webアプリケーションを」「決まった流れに従って」構築することに特化したフレームワークです。

フレームワークはインスタント食品

Ruby on RailsとRubyの関係は、料理における「インスタント食品」と「生の食材」との関係に近いと私は考えます。
Railsのようなフレームワークは、インスタント食品に該当します。

例えばインスタント食品であるカップラーメンは

  1. お湯を沸かす
  2. お湯を注いで数分待つ

の手順を踏襲するだけで、我々は

  • お湯を注いだときカップラーメン内部で何が起こっているか
  • カップ麺にはどのような具材が入っているか
  • 調味法はどうするのか
  • 麺はどのように作られたのか

を「食べる側は一切気にする必要がなく」美味しく食べることができます。
Railsフレームワークの良いところの一つは、「決まりきった方法に従って実装を行うのであれば、開発者側が内部構造を深く理解する必要がない」ことです。

なぜこのようなことが可能かというと「Railsの設計者が内部で、開発者側が深く実装を意識しなくて済むよう親切に」設計をしてくださっているからです。

言語はオーダーメイドの料理

Ruby言語そのものは生の食材に該当します。
生の食材で料理をしようと思うと、例えば肉じゃがを作るときに

  • 何の具材を入れるか
  • 調味料の割合
  • 具材の切り方
  • 煮込む時間
  • 盛り付け方

などを逐一考慮しなければならず相応の手間がかかります。
加えてその過程で包丁や火の扱い方、栄養素についての知識が必要とされるかもしれません。

Ruby言語そのものは「多様なことができる」故に、「用途によって深い基礎知識を持って逐一考慮し開発者側が設計を行う」必要があります。

Railsの開発力をあげようと思ったら、Rails内部でどのような実装が行われているかを理解する必要がありますが、それはRailsライブラリ内のRubyコードを読むことに他なりません。
当然Ruby言語の文法知識が要求されます。

料理に例え直すと、「食材であるジャガイモ本来の性質を理解し、具材の切り方や煮込み時間などを考慮する」ことでより美味しい肉じゃがを作れる、といったところでしょうか。

また、インスタント食品と生の食材に上下関係が無いように、フレームワークと言語に上下関係はありません。
あくまで言語の用途特化機能群がフレームワークです。

まとめ

言語とフレームワークの関係性や違いについて解説しました。
アプリ開発の経験も浅くて慣れていない最初は、もちろんフレームワークをガンガン使ってアプリ開発の経験と学びを積みましょう。

そこからさらにアプリ開発のスキルを深めていきたい場合にはぜひ言語に対する理解を深めていくのがいいかと思われます。

参考

この記事は「CodeShip」内での実際の質疑応答や指導・アドバイスの一部を基に作成しています。

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

Rails チュートリアル 第8章 まとめ

login-logout情報をrails serverとブラウザに一時的に保存する

session情報を取り扱うコントローラを作成
Sessionクラスを作っていないので、Userコントローラnewアクションのように、
@session = Session.new
とはできない
form_forはこうした事態にも対応できる

session.new.html.erb
form_for(:session, url: login_path) do |f|
#form_forの引数にシンボルを渡すことで、@userと同様の効果
# オプション引数はPostリクエストを/loginに送るため
#form_forにはcreateアクションに送る機能があるが、ここではurlを指定する必要がある

createアクション

sessions_controller.
def create
  @user = User.find_by(email: params[:session][:email])
#form_forでユーザが入力した内容がparamsに格納されている
#入力内容がすでに登録されている内容と同じかどうかUser.find_by(email: で検索している
  if @user.authenticate(params[:session][:password])
#.auth-は第6章終わりで学んだ、passwordが正しいか認証する
end

しかし、このコードでは登録されていないemailが入力された場合、find_by()は見つからなかった時、nilを返すので@user = nilということになり、  nilにはauthe-メソッドはないので、Errorとなる

そこで

def create
  @user = User.find_by(email: params[:session][:email])

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

end

とする
こうすることで、if文はnilとfalse以外は全てtrueなので、
もし@userにユーザオブジェクトが入っているなら、if文はtrueになるし、
@userにnilが入っているなら、trueとはならないので、elseの方に行く
さらに、@userを左に置くことで、@user.athen-の方は評価されないためErrorにならない

flashが消えないバグ
flash[:]の有効期間はリクエストが発行されるまでで、今まではredirect_toをすぐ実行していたので消えていた

ログイン成功時、ログイン状態の保持
sessionを使えば、serverに一時的に情報を保存することができる
まずその機能の実装
loginだと初めてコードを読んだ人にも分かるように、メソッドを定義する

session_helper.
def log_in(user)
  session[:user_id] = user.id
end

user情報が引数で渡されると、そのuserのidがuser_idというキーの値として保存される

次に、sessionの情報がある時(login状態)、ない時(logout状態)で振る舞いを変える実装
profileページでは他人のプロフィールではなく、自分のプロフィールが表示されないといけないので、自分の情報を引っ張ってくる必要がある
そのため今ログインしている人のユーザーオブジェクトをsession情報から復元しなければならない
すると、今ログインしているユーザのページが反映される

def current_user
   @current_user ||= User.find_by(id: session[:user_id])
end

@current_userの式変形はなんとなく理解

session[:user_id]が保持されているかいないかで振る舞いを変える部分

 def logged_in?
    !current_user.nil?
# loginしていたらtrue、していなかったらfalseを
# current_userにsession情報があれば、nilではないのでfalseになるが
# !でその情報を反転させてtrueを返すようにしている
  end

成功する時のTest(難しい)

def setup
    @user = users(:michael)
# usersメソッドの引数にラベル(micael)をおくと、サンプルデータが手に入る
  end
  .
  .
  .
  test "login with valid information" do
    get login_path #loginページの表示
    post login_path, params: { session: { email:    @user.email,
                                          password: 'password' } }
# 今までのTestなら適当な値でもフォーマットが正しければTestとして機能していたが
# passwordはauthen-を使うので、正しい値、実際の値を使わなければ通らない(?)
    assert_redirected_to @user
# リダイレクト先が正しいかどうかをチェック
    follow_redirect!
# 実際にリダイレクト先に移動
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
# loginへのリンクがないことを確認
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
  end

test用のデータを作りたい時に使うファイルがある
テスト用のサンプルデータセットをfixtureに書くと、これがテストのデータベースの中にサンプルデータとして入る
password_digestはハッシュ値であり、そのハッシュ値とbscryptをかけて平文をハッシュ値に変換したものを照合する必要がある
ここで作ったmichaelは上のsetupで呼び出すことができる

test/fixtures/users.yml
michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>

そのためのメソッドUser.digestを作る

 def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
# BCrypt::Password.create(stringで文字列をハッシュ値に変換
# costはハッシュ変換をライトに行うという指示
  end

log-out

def log_out
    session.delete(:user_id)
# keyを指定すると、該当するvalueを削除する
    @current_user = nil
  end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

accepts_nested_attributes_forとf.collection_check_boxesを使わずに、一つのフォームでモデルと中間テーブルを同時に保存する 

多対多のリレーションを考えたときに、親モデルを作成すると同時に子モデルも作成したいときがあります。ブログにカテゴリーをつけたいときとか。
accepts_nested_attributes_forを使用したやり方がよく紹介されていますが、評判が良くないといった書き込みがあったり、個人的に挙動がわかりにくかったのでaccepts_nested_attributes_forを使用しない方法を記載しました。
ターゲットはあくまでも初心者ですので、ていねいにコメントを多めにしながら書きました。

サンプルとしてuser(ユーザー)とjob(仕事)の多対多の関係を考えます

userモデルとjobモデルが中間テーブルuser_jobで多対多のリレーションを持つとします。

とりあえずuserを作成します。

userをつくりましょう。まずモデルから

$rails g model user name:string
$rails db:migrate

今回は名前だけ持たせます。
コントローラをつくります。userをつくるだけのシンプルなものです。

$rails g controller users
users_controller
def new
    @user = User.new
    @users = User.all
  end

  def create
    User.create(user_params)
    redirect_to new_user_path
  end

  private
  def user_params
    params.require(:user).permit(:name)
  end

users/new.html.erb
<h1>ユーザー登録</h1>
<%= form_for @user do |f| %>
<%= f.label :name %>
<%= f.text_field :name%>
<%= f.submit "登録" %>
<% end %>

<% @users.each do |user| %>
<%= "#{user.id}:#{user.name}" %><br>
<% end %>
config/routes
resources :users

スクリーンショット 2019-10-18 11.34.29.png
こんな感じになりましたでしょうか。
この段階でhttp://www.localhost:3000/users/newにアクセスすればuserの名前を登録するかんたんなwebサイトになります。一郎、次郎、三郎を作成しました。

user登録と同時に中間テーブル(user_jobs)を作成することを考えてみる

とりあえずjobモデルをつくりましょう

名前だけを持たせます。

$rails g model Job name:string
$rails db: migrate

migrateを忘れずに。
jobをいくつかつくりたいのですが、formを作るのが面倒なのでコンソールでjobを作成します。

> Job.create name: "farmer"
> Job.create name: "fisherman"

今回はfarmerとfishermanを作成しました。

中間テーブルuser_jobsを作成する

$rails g model UserJob user:references job:references
$rails db:migrate

user:references job:referencesとすることでuser_idとjob_idを自動生成してくれます。

has_manyとbelongs_toを加える

user.rb
#これを書き加える
has_many :user_jobs
has_many :jobs, through: :user_jobs, source: :job

has_manyを書き加えることでuser.jobs.builduser.user_jobsといったメソッドが使えるようになる。belongs_toは中間テーブル作成時に初めからあるはずです。

ここでユーザー登録と同時にuser_jobsも登録することを考えます。

users/new.html.erb
<h1>ユーザー登録</h1>
<%= form_for @user do |f| %>
  <!--ここはいつも通りの@userのname部分のform-->
  <%= f.label :name %>
  <%= f.text_field :name%>
  <!--ここが中間テーブルuser_jobsを保存させるためのform
  パラメータを扱いやすくするためfields_forでネストさせます-->
  <%= f.fields_for :job do |j| %>
    <!--jobは複数あるのでeachで全て取り出してチェックボックスにします-->
    <% Job.all.each do |job| %>
      <%= j.label job.name %>
      <%= j.check_box job.name,{}, true, false %>
    <% end %>
  <% end %>
  <%= f.submit "登録" %>
<% end %>
<!--この部分は保存したuserとjobを確認しているだけなので分かれば何でもいいです。-->
<% @users.each do |user| %>
  <% if user.jobs.first.nil? %>
    <%= "#{user.id}:#{user.name}" %><br>
  <% else %>
    <%= "#{user.id}:#{user.name}:" %>
    <% user.jobs.each do |job| %>
      <%= "#{job.name}" %>
    <% end %>
    <br>
  <% end %>
<% end %>

f.fields_forをつかって中間テーブル作成用のformをuser作成formにネストしています。
ここでは、あらかじめ登録したjobをチェックボックスで選択してuserに登録するようにします。
formでネストすることによって
params[:user][:job]= {"farmer"=>"true", "fisherman"=>"true"}
のようにチェックボックスでの値が取り出せます。

あとはコントローラで中間テーブルを保存する

users_conntroller.rb
def create
  created_user = User.create(user_params)
  #checkboxで受け取ったパラメータはhashになっているので一つづつ取り出して保存する
  unless params[:user][:job].nil?  #nilガード
    params[:user][:job].each do |key, value|  
      if value == false  #チェックされていない場合はスキップする
        next
      else
        job_id = Job.find_by(name: key).id  #keyで探して保存するだけです
        user_job = UserJob.create(user_id: created_user.id, job_id: job_id)
      end
    end
  end

  redirect_to new_user_path
end

  private
  def user_params
    params.require(:user).permit(:name)
  end

スクリーンショット 2019-10-21 0.27.29.png

終わり

チェックボックスに入れてuser登録するとこんな感じになりましたでしょうか?
accepts_nested_attributes_forを使ってみたり、f.collection_check_boxesをつかってみたりしましたが、いまいち挙動がつかめず気持ち悪かったので、きちんと自分で書いてみました。
リファクタリングできたり、fatコントローラである点は置いておいてください。
参考になればうれしいです。

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