- 投稿日:2019-10-21T23:50:57+09:00
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 updateWebpackerを除く
Gemfileからwebpackerを除きます。
React表示用のviewを作成
bundle exec rails g controller react-ui::home indextsconfigを作成
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 -Dwebpack記載
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 -Dwebpack.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です。
- 投稿日:2019-10-21T23:04:40+09:00
Rails チュートリアル 第10章 まとめ
editアクション -User編集ページ
GET /users/:id/editdef edit @user = User.find(params[:id]) # GETリクエストのidを使い、ユーザー情報を取得 endViewは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 endbeforeのフォローとして、testにログインしたことを確認するコードを書く
beforeが機能しているかコメントアウトしてチェック
beforeが機能していないとErrorとなるようにテストを書くusers_controller_testtest "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 end2 自分のプロフィールページだけを編集できるように
TDDで
テストはusers_controller_testtest "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_controllerdef 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_testtest "should redirect index when not logged in" do get users_path # users_pathは詳細ページを表す、詳細ページへ移動 assert_redirected_to login_url # beforeアクションでloginページへ enduser_controllerdef index @users = User.all # @usersに注意、User情報を全て取得 endView
<% 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ずつ endIntegrationTest
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へ endIntergrationTest
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
- 投稿日:2019-10-21T22:50:45+09:00
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 4c52b90Dockerの起動
デスクトップにあるクジラのアイコンをクリックすると、起動します。右下のバーにクジラアイコンがあれば、Dockerが起動しています。
Railsアプリ用のディレクトリを作成
例えば現在のディレクトリがrailsというディレクトリだとします。
$ pwd /railsそして、これから作成するアプリ用のディレクトリを以下のように作り、そこに移動します。ここでは、ディレクトリ名をdocker_sample_appとします。
$ mkdir docker_sample_app $ cd docker_sample_app $ pwd /rails/docker_sample_appDockerファイルなどの準備
以下の4つのファイルを作成してください。
$ touch docker-compose.yml Dockerfile Gemfile Gemfile.lockそして、VScode内でファイルを変更していきます。
docker-compose.ymlversion: '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: dbDockerfile.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_HOMEdocker-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.ymldefault: &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にアクセスすると無事、例の画面が出るはずです!!!
これで今回の目標は達成されました!
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.rbconfig.file_watcher = ActiveSupport::EventedFileUpdateCheckerのEventedFileUpdateCheckerのところをFileUpdateCheckerに変更します。
development.rbconfig.file_watcher = ActiveSupport::FileUpdateChecker参考にさせていただいた記事
- 投稿日:2019-10-21T21:32:48+09:00
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
- 一回だけテンプレートファイルを使う時
パターン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:{},{},{}...という記載で追記します。
- 投稿日:2019-10-21T18:43:14+09:00
RSpecのletでcreateした時に気をつけること 〜遅延評価〜
環境 Rails 6.0.0 RSpec 3.9 factory_bot 5.1.1 自分がプチハマりしたので備忘録も兼ねて。
RSpecを書いているとよく見る以下のようなコード。
before do @user = create(:user) endlet(: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と同じタイミング)に実行されます。遅延評価にも、必要のない時まで処理をしなくてすむといったメリットがありますので、適切な方を選べるようにしたいですね。
- 投稿日:2019-10-21T18:26:15+09:00
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 %>
- 投稿日:2019-10-21T17:46:57+09:00
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 :remember4.保存
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の認証
渡されたトークンとダイジェストが一致したらtruedef authenticated?(remember_token) BCrypt::Password.new(remember_digest).is_password?(remember_token) endsessions_helperdef 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 endUserを忘れる
rememberメソッドと対になるdef forget update_attribute(:remember_digest, nil) #remember_digestをnilに endremember(user)の対
def forget(user) user.forget cookies.delete(:user_id) cookies.delete(:remember_token) enddef 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 %>
- 投稿日:2019-10-21T17:42:02+09:00
[Rails]devise、Omniauthを利用したGoogle、facebook認証を実装した
はじめに
久しぶりの更新になります。某スクールを先日卒業したChihaと申します。
某スクールにてチームで某フリマアプリのコピーを作成したため、実装物のコードについて記録を残しつつ、開発チームで共有する目的で当記事を書いています。今回はOmniAuthを利用したGoogle、facebookユーザー認証機能について、私が行った作業を全部まとめて書いていこうと思います。
極力この記事のみで全て実装できるように丁寧な記事にしようと思います。
丁寧な記事が書けるとはとは言ってない対象読者
- SNS認証をアプリケーションに組み込みたい方
- あれこれ色々な記事を確認しながら実装するのに疲れた方
- 某スクールの受講生
後輩受講生が見る可能性があるのでこういう記事書いていいのかな?とも思いますがその辺は考えないそもそもの話、OmniAuthとは?
Google、Facebook、twitter等のSNSアカウントを用いてユーザー登録やログインを行ってくれるgem。
Gemfilegem "omniauth" gem "omniauth-twitter" gem "omniauth-facebook"のように、使いたいSNSによって"omniauth-snsによって決まった名称"のgemを導入する必要があります。
OmniAuthは、複数の外部サービスのアカウント情報を使ってユーザー登録やログインを提供します。OmniAuthはサービスごとにストラテジー(Strategies)を管理する、いわば元締めのgemです。OmniAuthのストラテジーとは、外部サービスごとにOAuth認証に必要な処理が記述されており、Rackミドルウェアとして提供されます。
ということで、やっていきましょう。
開発環境
- 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
にログインし、プロジェクトの選択 > 新しいプロジェクト
導入するアプリに沿ってプロジェクト名を入力し、作成します(今回は記事用のスクリーンショットなのでデフォルト名そのまま)
作成したら以下の画面に遷移するので、左上の三本線(ハンバーガーアイコン)より、ナビゲーションメニューを表示し、APIとサービス > OAuth同意画面 へと遷移し、アプリ名だけ入れて保存を押します。
保存ができたら、認証情報 > 認証情報を作成 > OAuth クライアント IDから、ID取得画面へ移動します。
今回は「ウェブ アプリケーション」を選択してください。
選ぶと色々入力項目が出ますが、承認済みのリダイレクトURIにhttps://localhost:9292/users/auth/google_oauth2/callbackを入れて保存してあげましょう。
保存すると、「クライアントID、クライアントシークレット」の2つが表示されます。
この2つをRailsで使用するので控えておきます。取得できたら、認証を利用するためのAPIを有効にしましょう。
Google+ APIの有効化
左側ナビゲーションメニューからAPIとサービス > ライブラリ へ移動
google+ で検索し、検索結果に出てくるgoogle+ APIを有効にします。
これでGoogleは完了です。
facebook認証
facebook for developersへとアクセスします。
新しいアプリの追加を押してアプリ名とメールアドレスを入力し、アプリIDを作成する。
作成したら、ベーシックへと移動し、「アプリID」「app secret」を控えておきます。
控えたら左側メニューのプロダクトの横にある「+」ボタンからプロダクト追加画面へ。
一番最初に出てくる「Facebookログイン」製品の設定を押すと、左のメニューにFacebookログインの項目が追加されます。
表示されたら、左メニュー「設定」から、OAuthリダイレクトURIを設定します。https://localhost:9292/users/auth/facebook/callback今回はpumaの設定に沿って以下のURIを登録します。問題なければ「変更を保存」して完了です。
何も問題なければ、これでいけると思います。
これで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 string null: false,unique: true encrypted_password string null: false uid string provider string アソシエーションや他テーブルは今回の実装に関係ないので割愛します。
ルーティング
deviseで生成されるomniauth_callbacks_controller.rbを使用するため、コントローラを明示する記述を追加します。
routes.rbRails.application.routes.draw do devise_for :users,controllers: {omniauth_callbacks: "users/omniauth_callbacks"} endController
controllers/users/omniauth_callbacks_controller.rbclass 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 endModel
実際の認証を行う処理部分をモデルに書いています。
user.rbclass 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 endView
認証を行いたいページの適当な箇所に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.ymldevelopment: 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.rbconfig.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以上になります。
今回は保存処理などに関しては書いていません。認証処理と認証完了後のデータの取得を中心に記事を書きました。最後に
スクール卒業から身の回りに積もったあれこれを消化していたら久しぶりの記事更新になりました。まだまだ書きたい項目はあるのでじゃんじゃん更新していこうと思います。
就活も頑張ります。まだまだ粗末な点も多いと思いますが、より良いコード、間違った点などがあればご教授頂けると幸いです。
- 投稿日:2019-10-21T17:35:03+09:00
備忘録:Railsにおける多次元配列のリファクタリングについて(+ jsonでの受渡しについて)
前提
json形式で配列を送りたかったが、配列が別々に2つ存在するため多次元配列(2次元配列)を作る必要があった。
最初
name.rbnames = ["hideki", "takahiro", "miki"] descriptions = ["すごい", "かっこいい", "かわいい"] inventories = [] names.each_with_index do |name, i| inventories.push [name, descriptions[i]] end出力結果
[["hideki", "すごい"], ["takahiro", "かっこいい"], ["miki", "かわいい"]]改善
name.rbnames = ["hideki", "takahiro", "miki"] descriptions = ["すごい", "かっこいい", "かわいい"] inventories = names.zip(descriptions)出力結果
[["hideki", "すごい"], ["takahiro", "かっこいい"], ["miki", "かわいい"]]メモ:受け取り側での処理
上記をjson形式で送る
name.rbrender json: inventoriesループさせる
name.coffeesuccess: (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]をループさせる方法がわからずでして、、どなたかわかる方がいらっしゃいましたら教えて頂けるとうれしみです。。
参考にした記事
- 投稿日:2019-10-21T14:59:20+09:00
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/
- 投稿日:2019-10-21T14:51:55+09:00
Railsで論理削除(soft delete)を実装する
環境
- Rails 5.2.3
- Ruby 2.6.5
- Discard 1.1.0
注意:Gemの選択
(※)非推奨の背景
- ActiveRecordの
delete
やdestroy
をoverrideしているため、開発者が予期しない挙動をする。dependent: :destroy
関連のレコードは削除(物理削除)される。- バグフィックスと、Railsの新しいバージョンへの対応は行うが、新しいfeatureは受け付けない。
Discardの導入
Gemのインストール
Gemfilegem "discard"$ bundle installdb 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 # 削除されたレコードのみ
- 投稿日:2019-10-21T14:51:55+09:00
Railsで論理削除(soft delete)を実装する(discard版)
環境
- Rails 5.2.3
- Ruby 2.6.5
- Discard 1.1.0
Gemの選択に要注意
(※)非推奨の背景
- ActiveRecordの
delete
やdestroy
をoverrideしているため、開発者が予期しない挙動をする。dependent: :destroy
関連のレコードは削除(物理削除)される(※期待動作ではない)。上記に伴い、バグフィックスと、Railsの新しいバージョンへの対応は行うが、新しいfeatureは受け付けていない。
Discardの導入
Gemのインストール
Gemfilegem "discard"$ bundle installdb 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 # 削除されたレコードのみ
- 投稿日:2019-10-21T14:51:55+09:00
Railsで論理削除(soft delete)を実装する(discard gem利用)
環境
- Rails 5.2.3
- Ruby 2.6.5
- Discard 1.1.0
Gemの選択に要注意
(※)非推奨の背景
- ActiveRecordの
delete
やdestroy
をoverrideしているため、開発者が予期しない挙動をする。dependent: :destroy
関連のレコードは削除(物理削除)される(※期待動作ではない)。上記に伴い、バグフィックスと、Railsの新しいバージョンへの対応は行うが、新しいfeatureは受け付けていない。
Discardの導入
Gemのインストール
Gemfilegem "discard"$ bundle installdb 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 # 削除されたレコードのみ
- 投稿日:2019-10-21T14:48:19+09:00
〜誰ワカ〜 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
- 投稿日:2019-10-21T14:44:46+09:00
find, find_by, whereの違いと特徴を丁寧に
勉強会にて
-find
find_by
where
の使い分け
- それぞれどんな時に使うのか
- 取れるデータはどんな形かが理解できていない人が多かったので勉強会用資料として書きます。
※初心者向けですのでわかりやすさ重視を心掛けましたfindメソッド
自動で作られて勝手に連番になってくれる
id
ってありますよね。
このid
を絞り込みの条件にしてデータを取得するこんな感じで作ると
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
の上位版とでもいうべきかも。ただし記述が少し長くなる。
find
はid
のみでしたが、find_by
はid以外も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]] => nilfindで予期しないで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.rbdef index @todos = Todo.where(title: "テスト") endindex.html.erb<h1>ToDo一覧</h1> <table> <% @todos.each do |todo| %> <tr> <th><%= todo.title %></th> </tr> <% end %> </table>
テスト
という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 => falsewhereとfind_byで返ってくるもの違うことに注意
【まとめ】それぞれの使い分けと注意
種類 使い分け 注意 find idで特定の1件取得すればいい場合 idのみ find_by id以外で特定の1件取得すればいい場合 最初の1件のみ & データなしでnil where 複数のデータを取得する場合 取り出し方 & データなしでカラ配列
- 投稿日:2019-10-21T14:41:21+09:00
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-buildreadlineが8にアップデート→
readline7がロードされなくなる→
readline7でビルドしていたrbenvの中のrubyがエラーになるみたい。
こうやってビルドし直せば復活した
RUBY_CONFIGURE_OPTS=--with-readline-dir=`brew --prefix readline` rbenv install 2.5.5
- 投稿日:2019-10-21T13:04:57+09:00
Rails6環境の本番環境でBlocked hostエラーが発生したときの対処法
結論
下記のいずれかでエラーを回避できます。
※ひとまずエラーを回避するための方法として記載しておりますが、これがベストな対処法かは判断し兼ねます。対応は自己責任でお願いいたします。
エラー回避策①
config/environments/development.rb
に下記を記載し、ホワイトリストに許可したいhostを追加する。config/environments/development.rbRails.application.configure do (中略) config.hosts << "<許可したいホスト名>" (中略) end後述するエラーメッセージにしたがった方法です。
今回の私の場合、config.hosts << "thawing-caverns-37676.herokuapp.com"
を記載しました。エラー回避策②
同じく
config/environments/development.rb
に下記を記載し、ホワイトリスト全体をクリアする。
これにより、すべてのホスト名に対するリクエストを通過させることができる。config/environments/development.rbRails.application.configure do (中略) config.hosts.clear (中略) endただし、せっかくRails6で追加された保護機能を無効化してしまうため、推奨は①のような気がします。
発生したエラー
Rails6で開発した環境をherokuへプッシュしてアクセスしようとしたところ、下記のようなエラーが発生した。
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`参考
- 投稿日:2019-10-21T13:01:18+09:00
テーブルを設計する際に必要となる要素を知り、それらの要素をどういった場合に使えばいいのかを理解する
テーブルの構成要素
データベースのテーブルがどのような要素から構成されているかを学習する。
テーブルとエンティティ
エンティティ = テーブルと考えてほとんと差し支えない。
成績管理アプリを作る場合を感えると生徒、科目、成績といったエンティティが存在する。
データベースにはそれらのエンティティに対応した生徒テーブル、科目テーブル、成績テーブルを作成することになる。テーブルの行と列
テーブルは名前の通り表の形式で構成されている。テーブルの行はレコード、列はカラムを言うがそれぞれ表している意味が異なる。
- テーブルの行(レコード)はエンティティの具体的なデータを表す
- テーブルの列(カラム)はエンティティの属性を表す
テーブルの行(レコード)
レコードとはエンティティの具体的なデータである。例で以下のような生徒テーブルのレコードを考えてみる。
id name 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 1 山田太郎 taro@example.com 2 鈴木次郎 jiro@example.com 外部キー
外部キーは関連する他のテーブルのレコードの主キーを値として持つカラムである。外部キーは他のテーブルのレコードとの関係性を表すために用いる。
主キーの説明であげた生徒テーブルには2名の生徒がいる。主キーとなるカラムはidであったが。。。
id name 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:stringRailsでは、カラムに対して
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 scoreRailsでは、マイグレーションファイルで外部キーとなるカラムを追加するときに
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 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 end1つのカラムに対するインデックスを設定してみる
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制約を用いる
- 値に重複がないように設定するには一意性制約を用いる
- キーの存在を保証する時には主キー制約、外部キー制約を用いる
- 検索する際に使うカラムにはインデックスを設定する
- 投稿日:2019-10-21T12:41:30+09:00
フレームワークと言語の関係性は、インスタント食品と料理の違いと言える
はじめに
- フレームワークとはいったいなんなのか?
- 言語との違いや関係性はなんなのか?
- アプリの開発力を高めるためには フレームワークをたくさん勉強するべき?
初心者が勉強を進めていくと一度はぶつかるこの疑問について。
たまにある、
「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のようなフレームワークは、インスタント食品に該当します。例えばインスタント食品であるカップラーメンは
- お湯を沸かす
- お湯を注いで数分待つ
の手順を踏襲するだけで、我々は
- お湯を注いだときカップラーメン内部で何が起こっているか
- カップ麺にはどのような具材が入っているか
- 調味法はどうするのか
- 麺はどのように作られたのか
を「食べる側は一切気にする必要がなく」美味しく食べることができます。
Railsフレームワークの良いところの一つは、「決まりきった方法に従って実装を行うのであれば、開発者側が内部構造を深く理解する必要がない」ことです。なぜこのようなことが可能かというと「Railsの設計者が内部で、開発者側が深く実装を意識しなくて済むよう親切に」設計をしてくださっているからです。
言語はオーダーメイドの料理
Ruby言語そのものは生の食材に該当します。
生の食材で料理をしようと思うと、例えば肉じゃがを作るときに
- 何の具材を入れるか
- 調味料の割合
- 具材の切り方
- 煮込む時間
- 盛り付け方
などを逐一考慮しなければならず相応の手間がかかります。
加えてその過程で包丁や火の扱い方、栄養素についての知識が必要とされるかもしれません。Ruby言語そのものは「多様なことができる」故に、「用途によって深い基礎知識を持って逐一考慮し開発者側が設計を行う」必要があります。
Railsの開発力をあげようと思ったら、Rails内部でどのような実装が行われているかを理解する必要がありますが、それはRailsライブラリ内のRubyコードを読むことに他なりません。
当然Ruby言語の文法知識が要求されます。料理に例え直すと、「食材であるジャガイモ本来の性質を理解し、具材の切り方や煮込み時間などを考慮する」ことでより美味しい肉じゃがを作れる、といったところでしょうか。
また、インスタント食品と生の食材に上下関係が無いように、フレームワークと言語に上下関係はありません。
あくまで言語の用途特化機能群がフレームワークです。
まとめ
言語とフレームワークの関係性や違いについて解説しました。
アプリ開発の経験も浅くて慣れていない最初は、もちろんフレームワークをガンガン使ってアプリ開発の経験と学びを積みましょう。そこからさらにアプリ開発のスキルを深めていきたい場合にはぜひ言語に対する理解を深めていくのがいいかと思われます。
参考
この記事は「CodeShip」内での実際の質疑応答や指導・アドバイスの一部を基に作成しています。
- 投稿日:2019-10-21T12:29:31+09:00
Rails チュートリアル 第8章 まとめ
login-logout情報をrails serverとブラウザに一時的に保存する
session情報を取り扱うコントローラを作成
Sessionクラスを作っていないので、Userコントローラnewアクションのように、
@session = Session.new
とはできない
form_forはこうした事態にも対応できるsession.new.html.erbform_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 enduser情報が引数で渡されると、その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) endtest用のデータを作りたい時に使うファイルがある
テスト用のサンプルデータセットをfixtureに書くと、これがテストのデータベースの中にサンプルデータとして入る
password_digestはハッシュ値であり、そのハッシュ値とbscryptをかけて平文をハッシュ値に変換したものを照合する必要がある
ここで作ったmichaelは上のsetupで呼び出すことができるtest/fixtures/users.ymlmichael: 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はハッシュ変換をライトに行うという指示 endlog-out
def log_out session.delete(:user_id) # keyを指定すると、該当するvalueを削除する @current_user = nil end
- 投稿日:2019-10-21T00:39:37+09:00
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 usersusers_controllerdef 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) endusers/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/routesresources :users
こんな感じになりましたでしょうか。
この段階で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: :jobhas_manyを書き加えることで
user.jobs.build
やuser.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.rbdef 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終わり
チェックボックスに入れてuser登録するとこんな感じになりましたでしょうか?
accepts_nested_attributes_forを使ってみたり、f.collection_check_boxesをつかってみたりしましたが、いまいち挙動がつかめず気持ち悪かったので、きちんと自分で書いてみました。
リファクタリングできたり、fatコントローラである点は置いておいてください。
参考になればうれしいです。