- 投稿日:2020-07-28T23:39:32+09:00
Rails メールアドレス、パスワード+αでログイン認証する
目的
nation_nameプロパティをログインに必要な項目に追加する。
使い方
- カラムとして存在しないプロパティを追加するattr_accessorメソッドを使ってnation_nameプロパティをユーザーモデルに追加する
user.rbclass User < ApplicationRecord #accessor attr_accessor :nation_name
- registration/new.html.erbとsessions/new.html.erbに以下のコードを追記する。
:new.html.erb <div class="field"> <%= f.label :国名 %><br /> <%= f.text_field :nation_name, autofocus: true %> </div>
- パラメーターとして送られるgroup_nameが許可されるようにする
:application.controller class ApplicationController < ActionController::Base before_action :authenticate_user! before_action :configure_permitted_parameters, if: :devise_controller? def configure_permitted_parameters #sign_in時にnation_nameも許可する devise_parameter_sanitizer.permit(:sign_in, keys:[:nation_name]) #sign_up時にnation_nameも許可する devise_parameter_sanitizer.permit(:sign_up, keys:[:nation_name]) #account_update時にnation_nameも許可する devise_parameter_sanitizer.permit(:account_update, keys:[:nation_name]) end end
- ユーザーの新規登録時にネーションIDが保存されるようにする
user.rbclass User < ApplicationRecord #accessor attr_accessor :nation_name #association belongs_to :nation # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :confirmable, authentication_keys: [:email, :nation_name] #validation before_validation :nation_name_to_id, if: :has_nation_name? def self.find_first_by_auth_conditions(warden_conditions) conditions = warden_conditions.dup nation_name = conditions.delete(:nation_name) nation_id = Nation.where(name: nation_name).first email = conditions.delete(:email) || conditions.delete(:unconfirmed_email) # devise認証を、複数項目に対応させる if nation_id && email find_by(nation_id: nation_id, email: email) elsif conditions.has_key?(:confirmation_token) where(conditions).first elsif email where(email: email).first else false end end private def has_nation_name? nation_name.present? end def nation_name_to_id nation = Nation.where(name: nation_name).first_or_create self.nation_id = nation.id end end
- 投稿日:2020-07-28T22:20:24+09:00
自作ログイン機能
事前情報
セッション
「サーバ側に用意された一つのブラウザから連続しておくれらている一連のリクエスト間で「状態」を共有できる仕組み」のことを言う
セッションがなければ、同じユーザから送られた一つ目のリクエストから二つ目のリクエストに情報を受け渡すことができないcontroller#セッションにアクセス session[:user_id] = @user.id #セッションの値の取り出し @user.id = session[:user_id]Cookie
ブラウザとwebサーバの間での情報の受け渡し。
「複数のリクエストの間で共有したい「状態」をブラウザ側に保存する仕組み」Userモデルを作る
rails g model user name:string email:string password_digest:string #同じパスワードから生成すると同じだが、それ自体は無意味な文字列migrationfileclass CreateUsers < ActiveRecord::Migration[5.2] def change create_table :users do |t| t.string :name, null: false t.string :email, null: false t.string :password_digest, null: false t.timestamps t.index :email, unique: true end end endrails db:migrateパスワードをdigestに変換する仕組みを作る
has_secure_passwordを使用し、パスワードをハッシュ化するためbcryptと言うハッシュ関数を提供するgemをインストールする
Gemfilegem 'bcrypt' #記述あるはずなのでコメントアウトをするbundle installmodelclass User < ApplicationRecord has_secure_password end #この一行で「password」「password_confirmation」 カラム が追加されるadminフラグのカラムをuserモデルに追加する
rails g migration add_admin_to_usersmigrationfileclass AddAdminToUsers < ActiveRecord::Migration[5.2] def change add_column :user, :admin, :boolean, default: false, null: false end endrails db:migrateコントローラの作成
rails g controller Admin::Users new edit show index #Adminと言うモジュールの名前空間の中にUsersControllerクラスを定義するルーティングの設定
routesRails.application.routes.draw do namespace :admin do resources :users end root to: 'task#index' resources :tasks endコントローラの中身を定義
controllerclass Admin::UsersController < ApplicationController def index @users = User.all end def show @user = User.find(params[:id]) end def new @user = User.new end def edit @user = User.find(params[:id]) end def create @user = User.new(user_params) if @user.save redirect_to admin_user_url(@user), notice:"ユーザー「#{@user.name}」を登録しました。" else render :new end end def update @user = User.find(params[:id]) if @user.update(user_params) redirect_to admin_user_url(@user), notice:"ユーザー「#{@user.name}」を更新しました。" else render :edit end end def destroy @user = User.find(params[:id]) @user.destroy redirect_to admin_user_url(@user), notice:"ユーザー「#{@user.name}」を削除しました。" end private def user_params params.require(:user).permit(:name, :email, :admin, :password, :password_confirmation) end endモデルの中身を定義
modelclass User < ApplicationRecord has_secure_password validates :name, presence: true validates :email, presence: true, uniqueness: true endビューの中身を定義
indexh1 ユーザー一覧 = link_to '新規登録', new_admin_user_path, class: 'btn btn-primary' .mb-3 table.table.table-hover thead.thead-default tr th= User.human_attribute_name(:name) th= User.human_attribute_name(:email) th= User.human_attribute_name(:admin) th= User.human_attribute_name(:created_at) th= User.human_attribute_name(:updated_at) th tbody - @users.each do |user| tr td= link_to user.name, [:admin, user] td= user.email td= user.admin? ? 'あり' : 'なし' td= user.created_at td= user.updated_at td = link_to '編集', edit_admin_user_path(user), class:'btn btn-primary mr-3' = link_to '削除', [:admin, user], method: :delete, date:{confirm:ユーザー「#{user.name}」を削除します。よろしいですか?”}, class:'btn btn-danger'newh1 ユーザー登録 .navjustify-content-end = link_to '一覧', admin_users_path, class: 'nav-link' = render partial: 'form', locals:{user: @user}edith1 ユーザー編集 .navjustify-content-end = link_to '一覧', admin_users_path, class: 'nav-link' = render partial: 'form', locals:{user: @user}_form- if user.errors.present? ul#error_explanation - user.errors.full_messages.each do |message| li = message = form_with model:[:admin, user], local: true do |f| .form-group = f.label :name, '名前' = f.text_field :name, class: 'form-control' .form-group = f.label :email, 'メールアドレス' = f.text_field :email, class: 'form-control' .form-check = f.label :admin, class: 'form-check-label' do = f.check_box :admin, class: 'form-check-input' | 管理者権限 .form-group = f.label :password, 'パスワード' = f.text_field :password, class: 'form-control' .form-group = f.label :password_confirmation, 'パスワード(確認)' = f.text_field :password_confirmation, class: 'form-control' = f.submit '登録する', class: 'btn btn-primary'showh1 ユーザーの詳細 .nav.justify-content-end = link_to '一覧', admin_users_path, class: 'nav-link' table.table.table-hover tbody tr th= User.human_attribute_name(:id) td= @user.id tr th= User.human_attribute_name(:name) td= @user.name tr th= User.human_attribute_name(:email) td= @user.email tr th= User.human_attribute_name(:admin) td= @user.admin? ? 'あり' : 'なし' tr th= User.human_attribute_name(:created_at) td= @user.created_at tr th= User.human_attribute_name(:updated_at) td= @user.updated_at = link_to '編集', edit_admin_user_path, class:'btn btn-primary mr-3' = link_to '削除', [:admin, user], method: :delete, date:{confirm:ユーザー「#{user.name}」を削除します。よろしいですか?”}, class:'btn btn-danger'ja.ymlja: activerecord: attributes: task: user: name: 名前 email: メールアドレス admin: 管理者権限 password: パスワード password_confirmation: パスワード(確認) created_at: 登録日時 updated_at: 更新日時ログインフォームの表示
rails g controller Sessions newroutesRails.application.routes.draw do get '/login', to:'sessions#new' endnewh1 ログイン = form_with scope: :session, local: true do |f| .form-group = f.label :email, 'メールアドレス' = f.text_field :email, class: 'form-control', id: 'session_email' .form-group = f.label :password, 'パスワード' = f.text_field :password, class: 'form-control', id: 'session_password' = f.submit 'ログインする', class: 'btn btn-primary'ログインの実装
routesRails.application.routes.draw do get '/login', to:'sessions#new' post '/login', to:'sessions#create' endsessions_controllerclass SessionsController < ApplicationController def new end def create user = User.find_by(email: session_params[:email]) #メールアドレスで検索 if user&.authenticate(session_params[:password]) #パスワード認証 見つからなかった場合はnilで返したいから&. session[:user_id] = user.id #セッションにuser_idを格納 redirect_to root_url, notice: 'ログインしました。' else render :new end end private def session_params params.require(:session).permit(:email, :password) end endログイン後、ユーザーを取得したい場合、、
以下でコードで取得できるが、ヘルパーメソッドを定義する方が良い
User.find_by(id: session[:user_id)application_controllerclass ApplicationController < ActionController::Base helper_method :current_user private def current_user @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id] end endログアウトを実装
routesRails.application.routes.draw do get '/login', to:'sessions#new' post '/login', to:'sessions#create' delete '/logout', to:'sessions#destroy' endsessions_controllerclass SessionsController < ApplicationController def new end def create ・・・ end def destroy reset_session redirect_to root_url, notice: 'ログアウトしました。' end private ・・・ endapplicationul.navbar-nav.ml-auto - if current_user li.nav-item= link_to 'タスク一覧', tasks_path, class:'nav-link' li.nav-item= link_to 'ユーザー一覧', admin_users_path, class:'nav-link' li.nav-item= link_to 'ログアウト', logout_path, method: :delete, class:'nav-link' - else li.nav-item= link_to 'ログイン', login_path, class:'nav-link各アクションの実行前に毎回ユーザーがログインしているか調べる
application_controllerclass ApplicationController < ActionController::Base helper_method :current_user before_action :login_required private def current_user @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id] end def login_required redirect_to login_url unless current_user end endログイン画面のときは、「before_action :login_required」がスキップされるように
sessions_controllerclass SessionsController < ApplicationController skip_before_action :login_required endUserとTaskを紐付ける
rails g migration AddUserIdTasksmigrationfileclass AddUserIdToTasks < ActiveRecord::Migration[5.2] def up execute 'DELETE FROM tasks;' #外部キーのついていない今までのtaskが削除される add_reference :tasks, :user, null: false, index: true end def down remove_reference :tasks, :user, index:true end endアソシエーションを定義する
user.rbclass User < ApplicationRecord validates :email, presence: true has_many :tasks endtask.rbclass Task < ApplicationRecord validates :name, presence: true validate :validate_name_not_including_comma belongs_to :user end紐付け後コントローラーの修正
tasks_controllerdef index @tasks = Task.all #を①または②に変更 @tasks = current_user.tasks #① @tasks = Task.where(user_id: current_user.id) #② end def create @task = Task.new(task_param)#を①または②に変更 @task = Task.new(task_param.merge(user_id: current_user.id)) #① @task = current_user.tasks.new(task_params) #② if @task.save redirect_to @task, notice: "タスク「#{@task.name}」を登録しました。" else render :new: end Task.find(params[:id])#を以下に変更 current_user.tasks.find(params[:id]) end管理者だけに管理機能を表示させる
applicationul.navbar-nav.ml-auto - if current_user li.nav-item= link_to 'タスク一覧', tasks_path, class:'nav-link' - if current_user.admin? li.nav-item= link_to 'ユーザー一覧', admin_users_path, class:'nav-link' li.nav-item= link_to 'ログアウト', logout_path, method: :delete, class:'nav-link' - else li.nav-item= link_to 'ログイン', login_path, class:'nav-linkusers_controllerclass Admin::UsersController < ApplicationController before_action :require_admin private def require_admin redirect_to root_url unless current_user.admin? end end
- 投稿日:2020-07-28T21:40:28+09:00
Rails form_for フォームコントロール
- 投稿日:2020-07-28T21:36:50+09:00
RSpecのCapybaraを使ったシステムスペックで`NotImplementedError`が出たときの対処法
システムスペックを書いている際にCapybaraのsend_keysメソッドを使おうとしたら、
NotImplementedError
が発生し、解決に時間がかかってしまったので、対処法を備忘録として記載します。結論から言うと、RSpecでJavaScriptを使う設定をし忘れていたことが原因でした。
エラーの発生
下記のテスト実行時に、
NotImplementedError
が発生しました。Failures: 1) Messages can display messages in real time Failure/Error: find('#message').send_keys(:enter) NotImplementedError: NotImplementedError発生箇所はsend_keysメソッドのようです。
spec/systems/messages_spec.rbrequire 'rails_helper' RSpec.describe 'Messages', type: :system do # 略 it 'can display messages in real time', js: true do log_in(user) visit room_path(room) message = 'Hello' expect{ fill_in 'Write a message', with: message # send_keysメソッドでエラー発生 find('#message').send_keys(:enter) }.to change(Message, :count).by(1) expect(page).to have_content message end対処法
1 JSのドライバの設定を追加する
spec/support/capybara.rbRSpec.configure do |config| config.before(:each, type: :system) do driven_by :rack_test end # 追記 config.before(:each, type: :system, js: true) do driven_by :selenium_chrome_headless end end2 スペックでJSを有効にする
spec/support/capybara.rbrequire 'rails_helper' RSpec.describe 'Messages', type: :system do # 略 # js: true を追記 it 'can display messages in real time', js: true do log_in(user) visit room_path(room) message = 'Hello' expect{ fill_in 'Write a message', with: message find('#message').send_keys(:enter) }.to change(Message, :count).by(1) expect(page).to have_content message end参考
- 投稿日:2020-07-28T20:34:24+09:00
Bootstrap導入手順をまとめてみた(RubyonRails)
初めまして!
弱弱駆け出しエンジニアのてしまと申します。初投稿です!
今回はRubyonRailsでのBootStrapの導入手順についてまとめてみました。
導入自体は簡単です!
もし僕のような初学者でBootstrapに興味がある方は参考になればと思います。
実際に使った例も載せているため後半は長くなっております。内容
①RubyonRailsにBootstrapを導入
②Bootstrapを使ってテーブルとボタンの装飾対象者
・RubyonRailsで簡単なアプリケーションを立ち上げたことがある
・Bootstrapを知らないor使ってみたいと考えている
・Scaffoldを使ったことがある(無くても導入はできます)Bootstrapとは
CSSの「フレームワーク」
通常CSSを書く場合、全てのスタイルを自分で作っていく必要がありますが
このフレームワークにはよく使われるスタイルがあらかじめ定義してあるので
ルールに沿って利用するだけで整ったデザインのページを作成できます。
(引用:https://techacademy.jp/magazine/6270)色々調べているとBootstrapをダウンロードすると出てきましたが
今回、Gemを使ったので特にダウンロードせずに使えました。前提条件
今回はBootstrapの導入と一部使用例をまとめています。
予めscaffoldを使ってtasksテーブルを作成。titleカラム、textカラムを追加しております。
今回使いませんがdeviseも入れてます。では早速本題の導入手順です。
RubyonRailsでBootstrapを使えるようにする
①Gemの導入
まずはGemfileにgemを導入します。
Gemfile.gem 'bootstrap', '~> 4.1.1' gem 'jquery-rails'jqueryは今回使っていませんが、いつか使うと思われるので入ってなければ記述。
ターミナル.bundle installbundle installも忘れずに。
②SCSSファイルにimport
application.cssをapplication.scssに名前を変更。
そして以下の文を追記application.scss@import "bootstrap";一応application.jsにも追記
application.js//= require bootstrap
はい!以上で準備完了です!
これだけでBootstrapが使えるようになります!
肝心の使い方があまり載ってなかったので
実際にコードを入れて装飾してみたいと思います。Bootstrapのコードを実際に入れてみる
では実際にBootstrapを使って装飾をしていきます。
装飾前がこんな感じです。
Scaffoldで生成してるのである程度形は整ってます。
①まずはHPにアクセス
「Bootstrap HP」
https://getbootstrap.com/ヘッダー左2番目のDocumentationをクリックして参考コードを検索しに行きます。
②作りたいCSSを検索
今回はテーブルを作るので「table」で検索
するとサンプル画像とそのコード一覧が出てくるので使いたいものを選びコピペするだけです!③使いたいコードをコピー
一覧は画面の大きさの都合上、割愛してます。
今回は以下の画像サンプルのコードを使って実装していきます。③エディタに貼り付け
先ほどコピーしたコードをエディタの対象ファイルに貼り付けます。
今回はviews/tasks(ご自身のファイル名)/index.html.erb
一旦、一番下などに貼り付けしてしっかり反映するか確かめてみると良いです。
下記はコピーして貼り付けただけです。
このテーブルタグの中身の記述(白テキスト部分)を自分のデータに置き換えていきましょう!<table class="table table-striped"> <thead> <tr> <th scope="col">#</th> <th scope="col">First</th> <th scope="col">Last</th> <th scope="col">Handle</th> </tr> </thead> <tbody> <tr> <th scope="row">1</th> <td>Mark</td> <td>Otto</td> <td>@mdo</td> </tr> <tr> <th scope="row">2</th> <td>Jacob</td> <td>Thornton</td> <td>@fat</td> </tr> <tr> <th scope="row">3</th> <td>Larry</td> <td>the Bird</td> <td>@twitter</td> </tr> </tbody> </table>④記述場所にデータを置き換え(index.html.erbファイル)
貼り付け前のファイルの記述がこちらです。(参考までに)
views/tasks/index.html.erb<p id="notice"><%= notice %></p> <h1>Tasks</h1> <table> <thead> <tr> <th>Title</th> <th>Text</th> <th colspan="3"></th> </tr> </thead> <tbody> <% @tasks.each do |task| %> <tr> | <td><%= task.title %></td> <td><%= task.text %></td> <td><%= link_to 'Show', task %></td> <td><%= link_to 'Edit', edit_task_path(task) %></td> <td><%= link_to 'Destroy', task, method: :delete, data: { confirm: 'Are you sure?' } %></td> </tr> <% end %> </tbody> </table> <br> <%= link_to 'New Task', new_task_path %>Bootstrapのサンプルに余計なテーブルがあるので削除。
先ほどコピペしたコードのテーブルタグの中身を表示したいデータの記述に置き換えます。
バーっと作ったのでインデントなど細かいところはすみません?
変更場所は見比べていただければと思います。
貼り付けがうまくいったら元々あった記述は消しちゃいましょう。
以下が変更後の記述です。<h1>Tasks</h1> <% if user_signed_in?%> <%= link_to "ログアウト", destroy_user_session_path, method: :delete %> <% else %> <%= link_to "新規登録", new_user_registration_path %> <%= link_to "ログイン", new_user_session_path %> <% end %> <%# ----------以下がBootstrapのテーブル---------- %> <table class="table table-striped"> <thead> <tr> <th scope="col">No</th> <th scope="col">Title</th> <th scope="col">Text</th> <th scope="col">Date</th> <th scope="col">Show</th> <th scope="col">Edit</th> <th scope="col">Destroy</th> </tr> </thead> <tbody> <% @tasks.each.with_index do |task, no| %> <tr> <td><%= no++1 %></td> <td><%= task.title %></td> <td><%= task.text %></td> <td><%= task.created_at.strftime('%Y/%m/%d %H:%M') %></td> <td><%= link_to 'Show', task %></td> <td><%= link_to 'Edit', edit_task_path(task) %></td> <td><%= link_to 'Destroy', task, method: :delete, data: { confirm: 'Are you sure?' } %></td> </tr> <% end %> </tbody> </table> <%# ----------以上がBootstrapのテーブル---------- %> <br> <%= link_to 'New Task', new_task_path %>完成図と相違があると困惑するため
deviseで使っている上の部分の記述があります。
気になさらずに。
コメントアウトで区切っている中身のみをご確認ください。⑤ボタンも作成
せっかくなのでボタンも入れてみましょう!
今度は「btn」で検索して(検索方法は色々あると思います)
以上のように色々出てきました。
今回は新規投稿ページにリンクさせたいので画像最下部のaタグのCSSを拝借。まずは以下のリンクの記述を削除し
<%# ----------以上がBootstrapのテーブル---------- %> <br> <%= link_to 'New Task', new_task_path %>以下のように書き換え
※引用元のa href=後ろのリンク先の記述とタグ内の文字を変更しただけ
引用元に合わせてPrefixからURIパターンに書き換えてます。<br> <a href="tasks/new" class="btn btn-primary btn-lg active" role="button" aria-pressed="true">New Task</a>完成画像
HTMLの記述だけでサンプル画像と同じ見た目の装飾ができました!
(CSSのクラス名がBootstrapによって決められているため)
この高さの揃った綺麗なテーブルを作るのが意外と大変•••
きっと初学者の方なら共感いただけるかと思います。Bootstrapを使えば決まったレイアウトにデータを置き換えるだけなので
このように簡単にCSSを作成可能!
アイコンなどもたくさんあったのでFontawsome派の方もぜひみる価値はありそうです!
今後はJSなども色々試してみてみたいと思います!参考資料
初めてのRuby on rails Bootstrap導入編 [Memo for neko]
https://qiita.com/Matteneko3/items/4dae9e55054e4a4affb4参考資料の方の記事を長く書いただけの記事です、、、
画像もつけて結構丁寧に書いたつもりですが
初投稿なのでもし間違っている点、ご意見などございましたら
コメントに残していただけると勉強、励みになります?♂️?
よろしくお願いいたします。
- 投稿日:2020-07-28T18:50:48+09:00
[Rails][Turbolinks] inline Javascript が実行されない 解決法
Javascript周り の取り扱いが難しい
Turbolinks
。起こったこと
ページ毎に任意の
Javascript
を実行したいが
普通に書いたら以下エラー。ページ遷移でも同様。
ChromeDeveloperConsoleRefused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' https: 'unsafe-inline' 'nonce-UiVx2CiP0HHN9jOOSEG43g=='". Note that 'unsafe-inline' is ignored if either a hash or nonce value is present in the source list.
Content-Security-Policy (CSP)
(Cross Site Scripting (XSS) や data injection 攻撃を防ぐための HTTP の仕様)のために
ブラウザでの Javascript 実行が制限されている。https://qiita.com/tearoom6/items/30e3aacaa432860d4b36
解決法
Rails 側で nonce を発行し、Javascript を認証する。
(HTTPヘッダに追加)views/layouts/head.html.slim= csp_meta_tagconfig/initializers/content_security_policy.rbRails.application.config.content_security_policy_nonce_generator = -> (request) do # use the same csp nonce for turbolinks requests if request.env['HTTP_TURBOLINKS_REFERRER'].present? request.env['HTTP_X_TURBOLINKS_NONCE'] else SecureRandom.base64(16) endscript タグに nonce をつける
some_page.html.slim= javascript_tag, nonce: true | console.log('JS here');Turbolinks の
Event
に合わせて、nonce を入れ替える。application.jsdocument.addEventListener('turbolinks:request-start', function(event) { var nonceTag = document.querySelector("meta[name='csp-nonce']"); if (nonceTag) event.data.xhr.setRequestHeader('X-Turbolinks-Nonce', nonceTag.content); }); document.addEventListener('turbolinks:before-cache', function() { Array.prototype.forEach.call(document.querySelectorAll('script[nonce]'), function(element) { if (element.nonce) element.setAttribute('nonce', element.nonce); }); });Turbolinks Event list
https://github.com/turbolinks/turbolinks#full-list-of-events
参考
https://github.com/turbolinks/turbolinks/issues/430
https://discuss.rubyonrails.org/t/turbolinks-broken-by-default-with-a-secure-csp/74790
https://github.com/thredded/thredded/pull/797/commits/beac77f9c7ac0c880afac3302f752157f2945afe
- 投稿日:2020-07-28T18:46:35+09:00
コールバック(モデルの状態を自動的に制御)
イベントの前後に任意の処理を挟むことをコールバックと言う
主に、イベントの前(before)、イベントの後(after)、イベントを挟む(around)のタイミングで書くことができる。※トランザクション
一連の複数の処理によるデータベースの整合性を保つための機能。
コーツバックの一つで例外が発生した場合、ロールバックと言う取り消し作業が発生し、その後のコールバックが実行されない仕組みになっている。
コールバックの種類 使い道 before_validation 検証前の値の正規化 after_validation 検証結果(エラーメッセージ)の加工 before_save, before_create, before_update saveのために裏側で行いたいデータ準備を行う after_save, after_create, after_update そのモデルの状態に応じてほかのモデルの状態をカエルなど、連動した挙動を実現する。検証エラーを出してもユーザーにはどうすることもできない状態異常を防ぐために例外を出す before_destroy 削除してOKかチェックし、ダメなら例外を出すなどして防ぐ after_destroy そのモデルの削除に応じてほかのモデルの状態をかえるといった連動した挙動を実現する
- 投稿日:2020-07-28T18:43:48+09:00
form_withがUndefinedエラーになったので、railsのバージョンをあげたらrails sが立ち上がらなくなった話
railsで入力フォームを作ろうをしたら
undefined method `form_with' for #<#Class:0x00007fdef2555c48:0x00007fdef1211178>
Did you mean? form_tagとエラー。
どうやらform_withはrails5.1以降の機能だったらしい。
なのでrailsのバージョンをあげることに
Gemfilegem 'rails', '5.2.1'そして bundle install
無事にバージョンアップに成功!
しかし rails s したところ
undefined method `halt_callback_chains_on_return_false=' for ActiveSupport:Module (NoMethodError)
と言われる。
どうやらhalt_callback_chains_on_return_falseはRails 4との後方互換のためのメソッドで、定義されていないメソッドだと言われる。
なのでターミナルにてターミナルvi config/initializers/new_framework_defaults.rbconfig/initializers/new_framework_defaults.rbActiveSupport.halt_callback_chains_on_return_false = falseをコメントアウトでok!
無事にrails s 立ち上がりました!
- 投稿日:2020-07-28T18:19:07+09:00
Couldn't find ModelName with 'id'=batch_action
- 投稿日:2020-07-28T16:05:40+09:00
[Rails] テーブルの新規作成するコマンド
rails g model テーブル名 カラム名:型
テーブル名は頭文字は大文字で、単数形にする
カラム名と型は省略可能
- 投稿日:2020-07-28T16:05:40+09:00
[Rails] テーブルの新規作成
rails g model テーブル名 カラム名:型
テーブル名は頭文字は大文字で、単数形にする
- 投稿日:2020-07-28T16:03:26+09:00
[Rails] cloneしたリポジトリをrailsサーバーで起動する
git hubからcloneしたリポジトリをrailsサーバーで起動するまでの道のりを記録しました。
リポジトリをclone
$ git clone https://github.com/*****/*****.git
サーバー起動コマンドを打ってみる
$ bin/rails s
Could not find rake-13.0.1 in any of the sources Run `bundle install` to install missing gems.
エラーが出て起動しません。bundleがないみたい。指定されたコマンドを打ってみます。
$ bundle install
/Users/*****/.anyenv/envs/rbenv/libexec/rbenv-exec: /usr/local/bin/bundle: /System/Library/Frameworks/Ruby.framework/Versions/2.3/usr/bin/ruby: bad interpreter: No such file or directory /Users/*****/.anyenv/envs/rbenv/libexec/rbenv-exec: line 47: /usr/local/bin/bundle: Undefined error: 0
これもまたエラーが出て失敗しています。
エラー解消の旅
teratailとCould not find XXXX in any of the sources エラーの修正方法を参考にさせていただきました。
まずはどのruby, bundleを参照しているか確認。
$ which ruby
/Users/*****/.anyenv/envs/rbenv/shims/ruby
$ which bundle
/Users/*****/.anyenv/envs/rbenv/shims/bundle
.anyenvの中のrubyとbundleを見ているのでこれは大丈夫そう。
そして、bundlerをインストールする。
$ gem install bundler
You don't have write permissionsと言われたのでsudoをつけます。
$ sudo gem install bundler
途中2回PC起動時のパスワードの入力を求められますがbundlerのインストールに成功。満を辞して
$ bin/rails s
=> Booting Puma => Rails 6.0.2.2 application starting in development => Run `rails server --help` for more startup options error Couldn't find an integrity file error Found 1 errors. ======================================== Your Yarn packages are out of date! Please run `yarn install --check-files` to update. ======================================== (以下省略)yarnをinstallしてね、とのこと。コマンドまで教えてくれていたのに癖で
--check-files
オプションを付けずに、$ yarn install
をしてしまいましたが問題ありませんでした。(省略) ✨ Done in 17.93s.
install成功!
そして今度こそ!!
$ bin/rails s
=> Booting Puma => Rails 6.0.2.2 application starting in development => Run `rails server --help` for more startup options Puma starting in single mode... * Version 4.3.3 (ruby 2.6.3-p62), codename: Mysterious Traveller * Min threads: 5, max threads: 5 * Environment: development * Listening on tcp://127.0.0.1:3000 * Listening on tcp://[::1]:3000 Use Ctrl-C to stoprailsサーバー無事起動しました^^
- 投稿日:2020-07-28T14:32:54+09:00
letを使用することで、通るはずのテストが通らないfalse Alarmを生じる可能性がある
先日RSpecのテストコードにレビューを頂いたので、
なるべく多くを学びたいと思い、その内容を咀嚼する過程での気付きを書きたいと思います
元記事
RSpecのEmail一意性テストで"Email has already been taken"問題と回避策 - Qiita
なるべく小さいscope?(contextの中など)を見ただけでテストの内容が把握できるような書き方のほうが、読み手に理解しやすい、想定外の挙動を防ぐことができる。
let
を使って書くことはこれを実現するために有効
let
の遅延評価によって、テストによっては必要のないインスタンス変数を毎回before
で定義するような冗長性を回避できるが、この遅延評価を理解せずに、なんとなく変数代入の感覚で使用していたために以下のようなエラーに遭遇しました
let
を使用することで、通るはずのテストが通らないfalse Alarmを生じる可能性がある
let
とlet!
の使い分けが重要になりそうなケース
- テストを実施する時点と、前提となる条件の間に時間的なずれがある場合
- すでに完了しているものと、現在の比較が必要な場合
- モデルのテストの場合なら、一意性のテスト
遅延評価されるletが、実際にテストの失敗を招いたケース
レビューの内容を参考に、
属性の一意性についてのテストを書いてみた。User modelの属性
username
の一意性についてのテストを以下のうように書くとdescribe "username" do ... context "usernameが重複している場合" do let(:existing_user) { FactoryBot.create(:user, username: "alice") } subject { FactoryBot.build(:user, username: "alice" ) } it { is_expected.to be_invalid } end ... endモデルで
username
属性についてのvalidationが記述されているにもかかわらずclass User < ApplicationRecord validates :username, presence: true, uniqueness: true, length: { maximum: 12 } ... endvalidationがinvalidにならない
1) User#create username usernameが重複している場合 is expected to be invalid Failure/Error: it { is_expected.to be_invalid } expected `#<User id: nil, email: "test_user_5@example.com", created_at: nil, updated_at: nil, username: "alice">.invalid?` to return true, got falseテストの記述を変更する
(実際にレビューコードではこの書き方を提示してくれていました)
let
>>let!
describe "username" do ... context "usernameが重複している場合" do let!(:existing_user) { FactoryBot.create(:user, username: "alice") } #let!に変更 subject { FactoryBot.build(:user, username: "alice" ) } it { is_expected.to be_invalid } end ... endこのようにすると、想定通りテストがパスするようになる。
let
とlet!
の違い
- letは、itやexampleが実行されるまで評価されない
- let!は即座に実行される
it { is_expected.to be_invalid }
の時点で、existing_user
は存在完了していないといけない
英文でいうところlet
は現在完了的な振る舞いをして、let!
は過去完了の状態をつくってくれると考えると個人的にはしっくりきた。参考
- 投稿日:2020-07-28T14:32:54+09:00
letを使用することで、通るはずのテストが通らないFalse Alarmを生じる可能性がある
先日RSpecのテストコードにレビューを頂いたので、
なるべく多くを学びたいと思い、その内容を咀嚼する過程での気付きを書きたいと思います
元記事
RSpecのEmail一意性テストで"Email has already been taken"問題と回避策 - Qiita
なるべく小さいscope?(contextの中など)を見ただけでテストの内容が把握できるような書き方のほうが、読み手に理解しやすい、想定外の挙動を防ぐことができる。
let
を使って書くことはこれを実現するために有効
let
の遅延評価によって、テストによっては必要のないインスタンス変数を毎回before
で定義するような冗長性を回避できるが、この遅延評価を理解せずに、なんとなく変数代入の感覚で使用していたために以下のようなエラーに遭遇しました
let
を使用することで、通るはずのテストが通らないfalse Alarmを生じる可能性がある
let
とlet!
の使い分けが重要になりそうなケース
- テストを実施する時点と、前提となる条件の間に時間的なずれがある場合
- すでに完了しているものと、現在の比較が必要な場合
- モデルのテストの場合なら、一意性のテスト
遅延評価されるletが、実際にテストの失敗を招いたケース
レビューの内容を参考に、
属性の一意性についてのテストを書いてみた。User modelの属性
username
の一意性についてのテストを以下のうように書くとdescribe "username" do ... context "usernameが重複している場合" do let(:existing_user) { FactoryBot.create(:user, username: "alice") } subject { FactoryBot.build(:user, username: "alice" ) } it { is_expected.to be_invalid } end ... endモデルで
username
属性についてのvalidationが記述されているにもかかわらずclass User < ApplicationRecord validates :username, presence: true, uniqueness: true, length: { maximum: 12 } ... endvalidationがinvalidにならない
1) User#create username usernameが重複している場合 is expected to be invalid Failure/Error: it { is_expected.to be_invalid } expected `#<User id: nil, email: "test_user_5@example.com", created_at: nil, updated_at: nil, username: "alice">.invalid?` to return true, got falseテストの記述を変更する
(実際にレビューコードではこの書き方を提示してくれていました)
let
>>let!
describe "username" do ... context "usernameが重複している場合" do let!(:existing_user) { FactoryBot.create(:user, username: "alice") } #let!に変更 subject { FactoryBot.build(:user, username: "alice" ) } it { is_expected.to be_invalid } end ... endこのようにすると、想定通りテストがパスするようになる。
let
とlet!
の違い
- letは、itやexampleが実行されるまで評価されない
- let!は即座に実行される
it { is_expected.to be_invalid }
の時点で、existing_user
は存在完了していないといけない
英文でいうところlet
は現在完了的な振る舞いをして、let!
は過去完了の状態をつくってくれると考えると個人的にはしっくりきた。参考
- 投稿日:2020-07-28T13:01:54+09:00
vue-routerのrouter.goがsafariで機能しない時の対処法
- 投稿日:2020-07-28T12:31:25+09:00
Ruby on Rails から Open Distro for Elasticsearch に接続する
この記事について
Ruby on Rails から Docker 上の Open Distro for Elasticsearch に接続する方法を記載します。
Open Distro for Elasticsearch とは
Open Distro for Elasticsearch は Elasticsearch のディストリビューションで、 Amazon Elasticsearch Service で使われているものです。
Docker
公式のドキュメントを参考に docker-compose で Open Distro for Elasticsearch を立ち上げます。
今回はシングルノードで立ち上げています。docker-compose.ymlversion: "3" services: elasticsearch_open_distro: image: amazon/opendistro-for-elasticsearch:1.9.0 environment: - discovery.type=single-node - cluster.name=elasticsearch - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms512m -Xmx512m" ulimits: memlock: soft: -1 hard: -1 ports: - 9200:9200Ruby on Rails から接続
elasticsearch-rails と elasticsearch-model を使います。
Gemfilegem 'elasticsearch-model' gem 'elasticsearch-rails'素の Elasticsearch と違い Open Distro for Elasticsearch は localhost の場合も https で接続しなければなりません。
よって initializer は以下のように書きます。
localhost の場合はtransport_options
で ssl の verify を false にしています。
ELASTICSEARCH_USER
とELASTICSEARCH_PASSWORD
はデフォルトでは両方 admin を使うと接続することはできます。config/initializers/elasticsearch.rbElasticsearch::Model.client = Elasticsearch::Client.new( host: 'localhost', port: 9200, user: ENV['ELASTICSEARCH_USER'], password: ENV['ELASTICSEARCH_PASSWORD'], scheme: 'https', transport_options: { ssl: { verify: false, }, }, )これで接続することができました。
elasticsearch-rails の Usage の手順で接続を確認することができます。
- 投稿日:2020-07-28T12:07:24+09:00
【初心者向け?】SQL超マスター〜複雑なテーブルにへこたれない編〜【Railsとの比較あり】
はじめに
Rails
を利用していると、普通に使っている分にはどんなSQLが発行されるんだろう?
ということを考えずとも、欲しいレコードが簡単に取得できますよね。Rails
を学び始めた頃からすごいな〜と思っていましたが、今になってもやはり凄いなと思います。ただ1つ、便利だからこそ生じる弊害があります。それは
>>>SQLが書けなくなる<<<
別に、「SQLが書けなくても
ActiveRecord
とかが頑張ってくれるから困らないじゃん」と思っている方もいらっしゃるかもしれません。私もそう思っていました。
しかし、例えRails
であっても、テーブル構造が複雑になってくると、どんなSQLを発行したいのか?
という部分が分からないと、Rails
でどう書いたらデータが取れるのか?というのが分からなくなります。業務中に、そういった場面と直面する機会が最近格段に増え物凄く困った、危機感を持った、というのが私の体験談であり、結果として「やばい、SQL勉強し直そう」と思うきっかけとなりました。
なんとなくでRails
でレコードを取得している方は、この記事で一緒に学び直しましょう!環境
- Docker for Mac 2.3.0.3
- Ruby 2.6.6
- Rails 6.0.3
- MySQL 8.0
- DBeaver 7.1.0
環境構築
- こちらからリポジトリをcloneする
$ docker-compose build
を実行$ docker-compose up -d
を実行$ docker-compose exec web rails db:create
を実行$ docker-compose exec web rails db:migrate
を実行$ docker-compose exec web rails db:seed
を実行- こちらの記事を参考に、
MySQL
とDBeaver
を接続するDBeaver
との接続後、students
テーブルにレコードが 6 件登録されていることを確認できればOK※DBeaver との接続時、MySQL の
root
パスワードを要求されると思います。
パスワードはdocker-compose.yml
のMYSQL_ROOT_PASSWORD
に指定した値を入力してください。(未設定の場合はpassword
が設定されます。)今回使用するテーブル
初級編〜SELECTの基礎を学ぶ〜
※ここからは
DBeaver
のSQLコンソール
を使用していきます。sql_master_development
がデフォルト選択されるよう設定を行って下さい。
universities
テーブルのレコードを全件取得する手始めに
universities
テーブルのレコードを全件取得してみましょう。これは簡単ですね!
universitiesテーブルのレコードを全件取得する
select * from universities;これは
Rails
だと以下のようになります。University.all特定のUniversityのレコードを取得する
では、次は
name
がUniversityA
のレコードを全件取得しましょう。
name
がUniversityA
のレコードを全件取得するselect * from universities where name = 'UniversityA';これは
Rails
だと以下のようなイメージです。University.where(name: 'UniversityA')
where
が出て来たので、find_by
もSQL
で書いてみましょう。
※1件のみ取得したい時はlimit
を使用します。
name
がUniversityA
のレコードを1件取得するselect * from universities where name = 'UniversityA' limit 1;中級編 テーブルを結合して、欲しいレコードを取得する
UniversityA
に紐づいたstudents
レコードを全件取得するテーブル結合について(簡易版)
≈
universities
テーブルとstudents
テーブルはuniversity_students
という中間テーブルによって紐づけられています。そのため、該当レコードを取得するためにはテーブル同士の結合が必要になります。
テーブル結合には内部結合(JOIN/INNER JOIN)
と外部結合(LEFT JOIN)
があります。
- 内部結合・・・あるカラムの値が一致しているレコードを取得し、結合して表示する
- 外部結合・・・あるカラムの値が一致していない場合でもテーブルを結合し、全件表示する
今回のような場合は
内部結合
か外部結合
か考えながら、まずは最終的に欲しい情報であるstudents
テーブルと中間テーブルであるuniversity_students
テーブルを結合して、students
レコードを全件取得してみましょう。※結合の構文は
inner join(left join)
結合するテーブル名
on
結合先テーブル名
.カラム名
=結合するテーブル名
.カラム名
name
がUniversityA
のレコードを1件取得するselect students.* from students inner join university_students on students.id = university_students.student_id;取得できましたか?
これはRails
だと以下のようになります。Student.joins(:university_student)結合したテーブルを使って、
UniversityA
に紐づいたstudents
レコードを全件取得するでは、次は本題である
UniversityA
に紐付いたstudents
レコードを全件取得していきましょう。
先ほどstudents
テーブルとuniversity_students
テーブルを結合したので、今回は追加でuniversity_students
テーブルとuniversities
テーブルを結合します。そして、
universities
レコードのname
カラムがUniversityA
であるものを特定すると、UniversityA
に紐づいたstudents
レコードを全件取得することができます。
StudentA
〜StudentF
まで取得できていたらOKです
UniversityA
に紐づいたstudents
レコードを全件取得するselect students.* from students inner join university_students on students.id = university_students.student_id inner join universities on university_students.university_id = universities.id where universities.name = 'UniversityA';これを
Rails
で書くと以下のようになります。Student.joins(:university).where(universities: { name: 'UniversityA' })この辺りから「
Rails
スゲー!!」という気持ちが大きくなってくるのではないでしょうか?
joins
が:university
のみの記述で良いのは、Model(Student)
でhas_one :university, through: :university_student
を定義しているからです。Railsは関連付けさえ綺麗に定義できれば上記のように記述をどんどん簡略化できるのでよいですね
UniversityA
に所属しているStudentA
のcourse_registrations
レコードを全件取得するこの辺から関連するテーブルが増えてきてごちゃごちゃします。
一つずつ紐解いて考えていきましょう。ここでは
UniversityA
大学に所属しているStudentA
さんのcourse_registration
レコード、つまり履修登録情報を取得します。ここで簡単に仕様を説明します。
course_registrations
レコードは年度毎に作成されます。
StudentA
さんは、2020年度
の時点で2回生です。
そのため、course_registrations
レコードは2レコード作成されています。
students
テーブルとcourse_registrations
テーブルはstudent_course_registrations
という中間テーブルを持っています。
UniversityA
大学に所属しているStudentA
さんのcourse_registration
レコードを全件取得するselect course_registrations.* from course_registrations inner join student_course_registrations on course_registrations.id = student_course_registrations.course_registration_id inner join students on student_course_registrations.student_id = students.id inner join university_students on students.id = university_students.student_id inner join universities on university_students.university_id = universities.id where universities.name = 'UniversityA' and students.name = 'StudentA';これを
Rails
で書くとこうなります。CourseRegistration.joins(student: :university) .where( students: { name: 'StudentA' }, universities: { name: 'UniversityA' } )ここまで書いた私「Railsやばい」
自分で生のSQLを全部書く時間と比較してどうでしょう?
Rails
凄い。さて、まだこれはテーブル結合として「まだ」優しいです。
この時点ではまだ、全て年度の履修を登録した
という情報しか取得できていません。
最初のER図を見た時に少し嫌な予感がした、という方。正しい判断です。
次はある年度
に履修した全ての科目
を取得してみましょう!
UniversityA
に所属しているStudentA
の2020
年度のCourseRegistration(履修登録情報)
に紐付いたsubjects
レコードを全件取得する見出しがカオスになってきました。
ただ、ここまでの知識を活かせばそう難しくないと思います。
subjects
テーブルとcourse_registrations
テーブルにも、例によってsubject_course_registrations
という中間テーブルがいます。
では、今までの知識を活かして取得してみましょう!
UniversityA
に所属しているStudentA
の2020
年度のCourseRegistration(履修登録情報)
に紐付いたsubjects
レコードを全件取得するselect subjects.* from subjects inner join subject_course_registrations on subjects.id = subject_course_registrations.subject_id inner join course_registrations on subject_course_registrations.course_registration_id = course_registrations.id inner join student_course_registrations on course_registrations.id = student_course_registrations.course_registration_id inner join students on student_course_registrations.student_id = students.id inner join university_students on students.id = university_students.student_id inner join universities on university_students.university_id = universities.id where universities.name = 'UniversityA' and students.name = 'StudentA' and course_registrations.year = '2020';これを
Rails
で書くと以下のようになります。Subject.joins(course_registration: { student: :university }) .where( universities: { name: 'UniversityA' }, students: { name: 'StudentA' }, course_registrations: { year: '2020' } )やっぱり
Rails
って凄いですね。。。
UniversityA
に所属するTeacherA
が受け持っている科目基礎英語
を2020年度
に受講する生徒を全件取得する
subjects
レコードを取得したから、今度はどうせteachers
レコードでも取得するんでしょうと思われた方もいるかもしれません。
ですが、恐らくワンパターンすぎて飽きてきたという方もいるでしょう。
私も流石に(ちょっともういいかな……)と思えてきたため、teachers
レコードを追加するのは各自で試してみていただければ、と思います。今度は生徒ではなく、教員側がデータベースに登録された情報を参照したい場合を考えてみます。
表題のようなレコードが欲しい、というケースですが、例えば
教員
が 「今年度の出席簿を作りたいな……」と思った時に、履修対象者を全件取得したい、といった時に起こり得そうですね。
UniversityA
に所属するTeacherA
が受け持っている科目基礎英語
を2020年度
に受講する生徒を全件取得するselect distinct students.* from students inner join student_course_registrations on student_course_registrations.student_id = students.id inner join course_registrations on course_registrations.id = student_course_registrations.course_registration_id inner join subject_course_registrations on subject_course_registrations.course_registration_id = course_registrations.id inner join subjects on subjects.id = subject_course_registrations.subject_id inner join subject_teachers on subject_teachers.subject_id = subjects.id inner join teachers on teachers.id = subject_teachers.teacher_id inner join university_teachers on university_teachers.university_id = teachers.id inner join universities on university_teachers.university_id = universities.id where universities.name = 'UniversityA' and teachers.name = 'TeacherA' and subjects.name = '基礎英語' and course_registrations.year = '2020';これを
Rails
で書くと以下のようになります。Student.joins(course_registrations: { subjects: { teachers: :university } }) .where( course_registrations:{ subjects: { name: '基礎英語' }, teachers: { name: 'TeacherA' }, universities: { name: 'UniversityA' }, course_registrations: { year: '2020' } } )上級(?)編 結合したテーブルを使って合計値を出す
各生徒ごとの総取得単位を取得する
ただレコードをとるだけでは面白くないので、次は生徒の名前と、総取得単位を取得し閲覧したいと思います。
取得するのは以下のようなデータです。
student_name total_credit StudentA 13 StudentB 13 StudentC 8 StudentD 7 ・・・ ・・・ 今回は
生徒
ごとの単位の合計
を出すので、students
テーブルとsubjects
テーブルの情報が必要になります。1人の生徒には年度ごとに
履修登録情報
が紐づいていて、履修登録情報
に各科目
の情報が紐づいている、というのは一度SQLを書いたので問題ないと思います。キモとなるのは、
1人ずつ
単位の合計値をまとめたい
という点だと思います。
情報をまとめたい場合はGROUP BY
を使います。group by (カラム名)また、カラムの合計値を出す時は
SUM関数
を使います。sum(カラム)そして、先程の表を見ると、少しヘッダーの表示がカラム名と異なっていたと思います。
AS
でエイリアスをつける必要がありそうですね。上記のことを踏まえて、SQLを書いてみましょう!
各生徒ごとの総取得単位を取得する
select students.name as student_name, sum(subjects.credit) as sum_credit FROM students inner join student_course_registrations ON student_course_registrations.student_id = students.id inner join course_registrations ON course_registrations.id = student_course_registrations.course_registration_id inner join subject_course_registrations ON subject_course_registrations.course_registration_id = course_registrations.id inner join subjects ON subjects.id = subject_course_registrations.subject_id group by students.nameこれはRailsで書くと以下のようになります。
Student.joins(course_registrations: :subjects) .group('students.name') .sum(:credit)書く量が全然違いますよね。
ただ、これを書こうと思った時に、結局どんな感じのSQLが発行されて欲しいのか?というところがわからないと、「???」となると思います。(私は毎回そうなっていました)また、「上みたいな場合だと
Ruby
でなんとかできそうだから、map
とかeach_with_object
使ってなんとかしちゃお」と私は思いがちだったのですが、純粋に値が必要なだけであればデータベースから直接取得できるので、Ruby
でゴリ押すのではなく、いい感じのSQL
を発行して必要な値を取得する、というのも必要なスキルだなと思いました最後に
親子関係のあるテーブルならまだ良いのですが、親子孫曽孫……のような構成のテーブルがあった時に、「親から曽孫ってどうやってとるんだ!?!?」と混乱することが多かったのですが、「どのテーブルのレコードが主人公になっているのか?」を意識しながら書いていくとそんなに複雑ではないということが分かったと思います。
また、Railsでレコードを取得する時も少し混乱してしまいがちですが、SQLのテーブル結合を意識するとシンプルに書いていくことができるので、
こんなSQLが発行されて欲しい!
というのを意識しながら書いていくと良いですねRailsでシンプルに書いていこうと思うと、Modelに定義する
関連付け
が大事だという話を少ししました。テーブルが複雑になればなるほど、この関連付け
の定義も難しくなります(ここ最近私が頭を抱えているところです。)次はRailsで条件付きの
has_one
・has_many
の定義の仕方を学べるようなQiitaが書けたらと思っています
- 投稿日:2020-07-28T11:25:11+09:00
ActiveRecordが接続しているDBの設定情報を確認するコマンド
今回のユースケース
DBクライアントツールにDBの設定情報を入力するために、ActiveRecordが接続しているDBの設定情報を一覧で取得したい。
動作環境
- Rails 6.0.3.2
- Microsoft SQL Server 2014
手順
- rails cでコンソールを起動する
- console内でActiveRecord::Base.connection_configを実行する
$ rails c Loading development environment (Rails 6.0.3.2) [1] pry(main)> ActiveRecord::Base.connection_config => {:adapter=>"sqlserver", :encoding=>"utf8", :username=>"hoge", :password=>"fuga", :tds_version=>7.4, :host=>"127.0.0.1", :database=>"piyo", :timeout=>15000, :port=>1433}※デフォルトのdevelopment環境以外の環境でDBの設定情報を確認したい場合は、rails c -e 環境名で環境を指定してコンソールを立ち上げて、console内でActiveRecord::Base.connection_configを実行することで確認が出来る。
$ rails c -e test Loading test environment (Rails 6.0.3.2) [1] pry(main)> ActiveRecord::Base.connection_config => {:adapter=>"sqlserver", :encoding=>"utf8", :username=>"hoge", :password=>"fuga", :tds_version=>7.4, :host=>"127.0.0.1", :database=>"piyo_test", :timeout=>5000}参照
https://api.rubyonrails.org/classes/ActiveRecord/ConnectionHandling.html#method-i-connection_config
感想
環境変数で設定している値も一覧で確認できたので便利でした。
- 投稿日:2020-07-28T10:41:20+09:00
Docker 初心者がまとめてみた
プログラミング初心者がDocekrについてまとめてみた
どこまでのまとめ?
Dockerについて〜コンテナの作成
コンテナ・イメージについて
Dockerの便利なところ
コマンド一発でコンテナを何度でも生成できる
- 一度Dockerfileを作成してしまえば、簡単に用意できる。
コンテナを共有できる
- Dockerfile(環境構築のレシピ)の中身はソースコードだから他の人たちと共有できる。Dockerfileを共有するためのDockerhubというサービスがあり、他の人が生成したコンテナをいつでも使える。
コンテナとは
コンテナとはDockerによって作成されるゲストOSのことで、 Dockerイメージを元に作成される仮想環境の実行部分。
イメージとは
通常パソコンにOSをインストールする時に使用されるものがイメージと呼ばれる。
アプリケーション開発にはUbuntu、RubyやPHPなど、実行するためにはOSが必ず必要になってくる。 『 OS が必要 = イメージが必要 』となり、結果 Docker の使用にあたって「Dockerイメージ」が必要となってくる。イメージはコンテナを作成するためのテンプレートとなってくれる。
つまりDockerコンテはDockerイメージから生成される。ビルドをするとは
DockerfileからDockerコンテナの元となるイメージをつくることを、Dockerイメージをビルドすると呼ばれている。
Dockfileとは
環境構築のレシピのこと。どうやって環境を作成していくかを記したファイル。
DockerfileはベースとなるDockerイメージ(OS)をFROMで定義できる。
Dockerfileはあくまでもイメージを構築するための手順を記述したファイルで、Dockerfile自身がDockerイメージになるわけではない!!!qiita.rbFROM ruby:2.6Dockerfileでイメージをビルドする際、まず最初にFROMで指定されたイメージをDockerHubというレジストリからダウンロードしてから実行される。
Dockerhub
Dockerfile(環境構築のレシピ)の中身はソースコードだから他の人たちと共有できる。Dockerfileを共有するためのDockerhubというサービスがあり、他の人が生成したコンテナをいつでも使える。
ここまでの流れ
Dockerイメージを構築するためのDockerfileを作成し、ビルドをすることによってイメージが作成されて、Dockerコンテナを実行していく。
コマンド/Dockerfileの中身
DockerfileFROM ruby:2.6 # install package to docker container RUN apt-get update -qq && apt-get install -y build-essential libpq-dev \ && apt-get install apt-transport-https \ && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ && apt-get update && apt-get install -y yarn \ && curl -sL https://deb.nodesource.com/setup_10.x | bash - \ && apt-get install -y nodejs \ && mkdir /アプリ名 WORKDIR /FANTRA COPY Gemfile /アプリ名/Gemfile COPY Gemfile.lock /アプリ名/Gemfile.lock COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000[FROM]
使用するイメージとバージョンを指定。
Dockerfileでイメージをビルドする際、まず最初にFROMで指定されたイメージをダウンロードしてから実行される。[RUN]
Dockerイメージビルド時に、Dockerコンテナ内で実行するコマンドを定義
ここではrails6に必要なツールをインストール。[WORKDIR]
作業ディレクトリを設定
[COPY]
Dockerを動作させているホストマシン上のファイルやディレクトリをDockerコンテナ内にコピーする
左側がローカルで、右側がコンテナ内。[ENTRYPOINT]
一番最初に実行するコマンド。
ENTRYPOINTはCMDと同じくコンテナ内で実行するプロセスを指定する。[EXPOSE]
コンテナがlistenするport番号
[CMD]
CMDはコンテナ起動時に1度実行される。RUNでアプリケーションの更新や配置、CMDはアプリケーションそのものを動作させる
Dockerfileやその他必要なファイルができればビルドをする
rails6で開発したかったら、以下のURLを参考にして必要フィルを作成していける。
https://qiita.com/nsy_13/items/9fbc929f173984c30b5ddocker image buildコマンドでDockerイメージを作成
qiita.rbdocker image build -t イメージ名 Dockerfile配置ディレクトリのパス-tオプションでイメージ名を指定する。
ここでイメージ名を指定しないと、ハッシュ値で管理することになって手間が増えてしまう。
カレントディレクトリがDockerfile配置ディレクトリであれば、最後の引数は「.」(カレントディレクトリ)docker image buildでは必ずDockerfileを与える必要があるから、ディレクトリにDockerfileが存在しないとちゃんと実行できない。
イメージがbuildで作成されたら
作成したイメージをdocker container runコマンドを利用してコンテナを実行できる。
注意なのがrunコマンドは実行とコンテナを作成するから、作る必要のないコンテナを実行する時は --rmをつけてやる。コンテナのライフサイクル
Dockerコンテナは実行中・停止・破棄という3つの状態のいずれかに分類される。これをDockerコンテナのライフサイクルと呼びます。docker container runで起動された直後は実行中に当てはまる。
実行中
docker container runで指定されたDockerイメージをもとにコンテナが作成され、DockerfileのCMDやENTRYPOINTで定義されているアプリケーションの実行を開始する。このアプリケーションが実行中なら、Dockerコンテナは実行中にあるということ。
停止
実行中のコンテナはユーザーが明示的にコンテナを停止するか、コンテナで実行されているアプリケーションが正常・異常を問わず終了した場合に自動的に停止。
破棄
頻繁にコンテナの実行・停止を繰り返すような環境ではディスクの要領を専有していくことになるため、不要なコンテナは破棄した方がいい。
コンテナは明示的に破棄しない限り残り続けてしまう。
- 投稿日:2020-07-28T10:05:15+09:00
RailsでIPベースでBasic認証をかける
社内システムでかつ、外部公開をする場合に社内なら認証なし+社外からは認証かけたい場合のIPベースでの解決方法
RailsでのBasic認証
https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic.html
まずは公式参照ほぼこの内容なので、条件をローカルネットワークなら認証必要に変える。
ちなみにこの公式の内容だと通常のログイン認証と併用できたりする。管理者アクセスとかに使えるかも。application_controller.rbclass ApplicationController < ActionController::Base protect_from_forgery with: :exception http_basic_authenticate_with name: "user", password: "pass" endIPベースの判定
CIDRベースで指定したい。
IPAddr
かnetaddr
でいけるみたい。今回はnetaddr
の方を使う。github
https://github.com/dspinhirne/netaddr-rb
readmeに使い方はないので、テストケースを参照。サブネットで判定
https://github.com/dspinhirne/netaddr-rb/blob/master/test/ipv4net_test.rb
ここにいろいろあるテストケースから抜粋def test_contains net = NetAddr::IPv4Net.parse("1.0.0.8/29") ip1 = NetAddr::IPv4.parse("1.0.0.15") ip2 = NetAddr::IPv4.parse("1.0.0.16") ip3 = NetAddr::IPv4.parse("1.0.0.7") assert_equal(true, net.contains(ip1)) assert_equal(false, net.contains(ip2)) assert_equal(false, net.contains(ip3)) endこのへんですね。
実際に書くとdef is_local_access? return NetAddr::IPv4Net.parse('192.168.1.0/24').contains(NetAddr::IPv4.parse(request.ip)) endこんな感じでしょうか。
これで192.168.1.0/24のネットワークが判定できる。
(nginxとかリバースプロキシ挟んでるときはrequest.ip
かrequest.remote_ip
、nginxの設定によるかも)もうちょっとやるならlocalリクエストも判定しておいたほうがいい。
request.local?
application_controller
application_controller.rbrequire 'netaddr' class ApplicationController < ActionController::Base protect_from_forgery with: :exception http_basic_authenticate_with name: "user", password: "pass", unless: : is_local_access? private def is_local_access? return NetAddr::IPv4Net.parse('192.168.1.0/24').contains(NetAddr::IPv4.parse(request.ip)) end end
- 投稿日:2020-07-28T08:24:57+09:00
nuxt.js × Rails CSVアップロードフォームを作る
はじめに
新卒エンジニアの@yhorikawaです
nuxt.jsとRailsを使ってCSVアップロードフォームを作るのに苦戦したので、記事として残します。csvファイル
1,hoge,100 2,foo,200 3,bar,300このようなcsvが存在していて、一行ずつ保存したいと仮定します。
フロント側
<template> <div class="container"> <input @change="handleChange" type="file" name="file"> <button @click="handleSubmit" type="submit">保存</button> </div> </template> <script> export default { data: { file: null, }, methods: { handleChange(e) { this.file = e.target.files[0]; }, handleSubmit() { let formData = new FormData(); formData.append('file', this.file); this.$axios .$post('csvFileUploadUrl', formData) .then(response => { // 成功した時の処理 }).catch(err => { // エラーの時の処理 }); }, }, }; </script>ファイルをpostできなくて苦戦していたのですが、
FormDataを利用することで簡単にファイルアップロードをすることができました!Rails側
require 'csv' def csv_upload CSV.foreach(params[:file]) do |row| # 保存処理 end endおわりに
今回はファイルアップロードフォームを作成しました。
単純に実装できそうだと思って作り始めたのですが、意外と難しかったので記録として残しておこうと思います。
- 投稿日:2020-07-28T07:12:24+09:00
Railsでbootstrap-iconsを簡単に使う
rails(webpacker)でbootstrap-iconsを使う方法
現時点では
v1.0.0-alpha5
が最新なので今後変わると思います。npm
npm install bootstrap-icons
yarn
yarn add bootstrap-icons
次に
app/helpers/application_helper.rb
に以下を追記します。module ApplicationHelper ... # 追加 def icon(icon, options = {}) file = File.read("node_modules/bootstrap-icons/icons/#{icon}.svg") doc = Nokogiri::HTML::DocumentFragment.parse file svg = doc.at_css 'svg' if options[:class].present? svg['class'] += " " + options[:class] end doc.to_html.html_safe end endviewはslimを使用しているので
=icon("hdd", class: "text-gray")で表示されます。
- 投稿日:2020-07-28T01:17:10+09:00
rails devise 導入時のエラー
devise gemを導入時にdb:migrateでエラー発生
以下の手順で導入
Gemfilegem 'devise'をGemfileに追加。
その後、gemをインストール$ bundle install依存するファイルを作成
$ rails g devise:install以下の英文が表示される。
=============================================================================== Depending on your application's configuration some manual setup may be required: 1. Ensure you have defined default url options in your environments files. Here is an example of default_url_options appropriate for a development environment in config/environments/development.rb: config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } In production, :host should be set to the actual host of your application. * Required for all applications. * 2. Ensure you have defined root_url to *something* in your config/routes.rb. For example: root to: "home#index" * Not required for API-only Applications * 3. Ensure you have flash messages in app/views/layouts/application.html.erb. For example: <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p> * Not required for API-only Applications * 4. You can copy Devise views (for customization) to your app by running: rails g devise:views * Not required * ===============================================================================上記の内容は今回のエラーとは関係ないので無視で!
ユーザーモデルの作成
$ rails g devise userマイグレーションファイルが作成されているので、ここで追加したいカラムがあればマイグレーションファイルに記述しておく。
今回の問題点
$ rails db:migrateマイグレーションファイルをマイグレートしようとすると以下のエラーがでてくる。
Takanori:movie_share_app taka$ rails db:migrate == 20200727124253 CreatePosts: migrating ====================================== -- create_table(:posts) -> 0.0137s == 20200727124253 CreatePosts: migrated (0.0138s) ============================= == 20200727141052 DeviseCreateUsers: migrating ================================ -- create_table(:users) -> 0.0182s -- add_index(:users, :email, {:unique=>true}) rails aborted! StandardError: An error has occurred, all later migrations canceled: Mysql2::Error: Specified key was too long; max key length is 767 bytes /Users/taka/projects/movie_share_app/db/migrate/20200727141052_devise_create_users.rb:39:in `change' /Users/taka/projects/movie_share_app/bin/rails:9:in `<top (required)>' /Users/taka/projects/movie_share_app/bin/spring:15:in `<top (required)>' bin/rails:3:in `load' bin/rails:3:in `<main>' Caused by: ActiveRecord::StatementInvalid: Mysql2::Error: Specified key was too long; max key length is 767 bytes /Users/taka/projects/movie_share_app/db/migrate/20200727141052_devise_create_users.rb:39:in `change' /Users/taka/projects/movie_share_app/bin/rails:9:in `<top (required)>' /Users/taka/projects/movie_share_app/bin/spring:15:in `<top (required)>' bin/rails:3:in `load' bin/rails:3:in `<main>' Caused by: Mysql2::Error: Specified key was too long; max key length is 767 bytes /Users/taka/projects/movie_share_app/db/migrate/20200727141052_devise_create_users.rb:39:in `change' /Users/taka/projects/movie_share_app/bin/rails:9:in `<top (required)>' /Users/taka/projects/movie_share_app/bin/spring:15:in `<top (required)>' bin/rails:3:in `load' bin/rails:3:in `<main>' Tasks: TOP => db:migrate (See full trace by running task with --trace)上記のエラーだと、
指定されているキーが長すぎるということを言っているらしいです。
キーの最大長は767バイトということです。
原因としては、DBのcharsetがutf8mb4であったことが原因です。DBのcharset確認方法
mysql -u root -p
上記でmysqlにログイン
以下の記述でdatabaseの文字コード確認
mysql> show create database [database_nameを記入、 例: test_development] ; ← #セミコロン忘れ注意!以下のようにでてくる。
+-----------------------------+-----------------------------------------------------------------------------------------+ | Database | Create Database | +-----------------------------+-----------------------------------------------------------------------------------------+ | test_development | CREATE DATABASE `test_development` /*!40100 DEFAULT CHARACTER SET utf8mb4 ← **ここがデフォルトのcharset** */ | +-----------------------------+-----------------------------------------------------------------------------------------+
Rails6以降のverではデフォルトでcharsetはutf8mb4になる。それ以前だと、utf-8がデフォルトだった。
utf8は1文字が3バイト。対して、utf8mb4は1文字が4バイトとなります。
したがって、varchar(255)カラム上では
utf> 255 * 3 = 765byte
utf8mb4> 255 * 4 = 1020byte
こえちゃってますね。対処方法
1.DBのcharsetをutf-8に変更
以下の記事を参考に!
MySQLの文字コードをutf8mb4に変更2.varcharの最大長を191にする
デフォルトでvarchar(255)となっているものを、varchar(191)とする。
変更方法は、migrationファイルで行います。
migrateする前に各カラムにlimit: 191を追記する*_devise_create_users.rb# frozen_string_literal: true class DeviseCreateUsers < ActiveRecord::Migration[6.0] def change create_table :users do |t| ## Database authenticatable t.string :email, null: false, default: "", limit: 191 #limitを記述 t.string :encrypted_password, null: false, default: "", limit: 191 #limitを記述 ## Recoverable t.string :reset_password_token, limit: 191 #limitを記述 t.datetime :reset_password_sent_at ## Rememberable t.datetime :remember_created_at ## Trackable # t.integer :sign_in_count, default: 0, null: false # t.datetime :current_sign_in_at # t.datetime :last_sign_in_at # t.string :current_sign_in_ip # t.string :last_sign_in_ip ## Confirmable # t.string :confirmation_token # t.datetime :confirmed_at # t.datetime :confirmation_sent_at # t.string :unconfirmed_email # Only if using reconfirmable ## Lockable # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts # t.string :unlock_token # Only if unlock strategy is :email or :both # t.datetime :locked_at t.timestamps null: false end add_index :users, :email, unique: true add_index :users, :reset_password_token, unique: true # add_index :users, :confirmation_token, unique: true # add_index :users, :unlock_token, unique: true end endもう一つ方法があるのですが今回は割愛します。
これでrails db:migrateができるようになります。
- 投稿日:2020-07-28T00:55:38+09:00
renderメソッドとredirect_toの違い
※この記事は私の勘違いが含まれています。正確な内容はコメント欄でご指摘頂いている通りです。
ツイッターのようなツイートアプリを作成中なのですが、表題の通りrenderとredirect_toの違いってなんや?
と思い私なりの結論が出たのでアウトプットのため、記事にしています。結論
- renderは同じコントローラー内のファイルしか基本読み込まないため、同じコントローラーのビューファイルを表示されることに向いている。
- redirect_toはurlを指定できるので、別のコントローラーのindexのビューファイルを表示させたい場合に向いている。
この結論に至った経緯は以下の通りです。
まずは以下のコードを見てください。app/controllers/users_controller.rbclass UsersController < ApplicationController def edit end def update if current_user.update(user_params) redirect_to root_path else render :edit end end private def user_params params.require(:user).permit(:name, :email) end endこれはユーザー管理用のusersコントローラーの記述です。
updete
アクションで、ユーザーアカウントの編集ができた場合はroot_path(messageコントローラーのindexアクション)
へリダイレクトする。
できなかった場合はrender :edit
で再度編集画面(users
コントローラーのedit
のビューファイル)へ戻すというものです。では
redirect
をrender
に変えた場合どうなるのか考えてみました。def update if current_user.update(user_params) render template : "messages/index" else render :editこれで同じになったやろ!
と思ったらエラーになりました。
原因はmessage/index
のビューファイルは部分テンプレートを使用していたからでした。app/views/messages/index.html.erb<div class="wrapper"> <div class="side-ber"> <%= render "side_bar" %> </div> <div class="chat"> <%= render "main_chat" %> </div> </div>ここでmessagesフォルダ内にある部分テンプレート
side_bar
,main_chat
を読み込むという記述があります。
試しに部分テンプレートをapp/views/users
フォルダの直下に入れたら正常に表示されました。以上のことからわかったこと
users
コントローラーからmessages/index
のビューファイルだけを呼び出した。- ビューファイル内に部分テンプレートを呼び出すコードがあったため
users
のビューファイル内で部分テンプレートを呼び出そうとしたが部分テンプレートがないためエラーになった。まとめ
- 同じコントローラーのビューファイルを指定する場合は
render
の方が早い- redirect_toはリクエストがルーティングに戻されるので別コントローラーのビューファイルを表示させる時に使う。