- 投稿日:2020-07-03T22:34:10+09:00
メッセージ送信の非同期化
フォームが送信されたら、イベントが発火するようにしよう
この記述の解説をします。
$(**)には、formのクラス名を記述します。
.on(**,にはイベント名を記述します。
e.preventDefaulでは、非同期通信を行う為にデフォルトのイベントを止めています。イベントが発火したときにAjaxを使用して、messages#createが動くようしましょう
この記述の中のthisは、イベントの発火元であるFormの情報が入っています。
$(this).attr('action');は、Form情報のパスを取得しています。messagesコントローラーの#createアクションでメッセージを保存し、respond_toを使用してJSON形式のリクエストに対してのレスポンスを返せるようにしましょう
if @message.save
リクエストで送られてきた情報を保存している
respond_to do |format|
format.json
json方式で返しているその他アウトプット
クラス名MessageFieldにappend(html)でHTMLを追加している$(".submit-btn").prop('disabled', false);
送信ボタンを一度押すとリロードしないと押せなくなるが
prop('disabled', false);を送信ボタンクラスに記述する事によりロードせずに投稿ができる様になる非同期に失敗した場合の処理
- 投稿日:2020-07-03T19:24:26+09:00
VueでRailsのactive_storageに保存した画像を表示する
Railsアプリ内でVueを使用し、SPAを実装しました。
その中で、VueからRailsのactive_storageを使おうと思いましたがそれっぽい方法を見つけられず、自分でとりあえず実装してみたので残します。
プログラミング初心者かつVue自体も初めてなので間違いもだいぶあると思います。見つけたら指摘していただいたら嬉しいです。version
Rails 6.0.3.2
ruby 2.6.3
@vue/cli 4.4.1active_storageに保存するまでは
https://qiita.com/ozin/items/5ec81a4b126b8ebf7a96
この記事でやりました。このあとvue側に表示するまでを書きます。Vueでは単一ファイルコンポーネントで実装しています。
ライブラリのインストール方法は省略します。モデル
diary.rbhas_one_attached :image # 省略コントローラ
diaries_controller.rbdef index diaries = Diary.all diaries_array = [] diaries.each do |d| hash = d.attributes # インスタンスをハッシュにする if d.image.attached? # imageのパスを取得 image = Rails.application.routes.url_helpers.rails_blob_path(d.image) hash["image"] = image # ハッシュに新しいkeyと値を追加 else hash["image"] = '' end diaries_array << hash # 配列にハッシュを要素として追加 end render json: diaries_array endaxiosでリクエストを送る
diareis.vue// 省略 <script> export default { data: function() { return { diaries: '' } }, mounted() { let that = this; axios.get('/APIなどのパス/').then(function(response) { that.diaries = response.data; } } } <script>バインドして表示
diaries.vue<template> <div> <div v-for='diary in diaries' v-bind:key='diary.id' > <div v-if='diary.image'> <img v-bind:src="diary.image"> </div> </div> </div> </template>これで表示できました。
指摘などありましたらお願いします!
- 投稿日:2020-07-03T19:18:31+09:00
完成したRailsアプリの立ち上げ方
Terminalbundle install --path vendor/bundle rails db:migrate rails s
- 投稿日:2020-07-03T19:16:17+09:00
Rails5でECサイトを作る① ~アプリ構成、各種gem準備、Model・Routing作成~
はじめに
先日、プログラミングスクールにてチーム実装の課題があり、ECサイトを作りました。
自分の担当したところ以外がどのようなコードで動いているのか、復習も兼ねて作ってみたいと思います。何のお店のECサイトにするか迷いましたが、Googleフォトのアルバムにパンの写真がたくさん入っていたので、架空のパン屋さんということにします。
コードソース
https://github.com/Sn16799/bakeryFUMIZUKI
環境
Rails 5.2.4.2
Ruby 2.7.1
Centos7アプリの概要
・ECサイト(ユーザがサイト内で買い物できる)
・ユーザサイトと管理者サイトを制作
・ユーザサイトでは会員登録してログイン後、カートに商品を入れ、注文手続きをする
・管理者サイトでは注文が入った商品の注文ステータス(入金待ち、製作中、……)と製作ステータス(着手不可、製作中、……)の変更、商品や会員の管理を行うアプリ立ち上げ
$ rails new fumizuki # 7月なので店名(架空)は「ベーカリー文月」で。 $ cd fumizukiGemfileに以下を追加
# ログイン機能 gem 'devise' # view装飾 gem 'bootstrap' gem 'jquery-rails' # 画像投稿 gem 'refile', require: "refile/rails", github: 'manfe/refile' gem 'refile-mini_magick' # 環境変数の管理 gem 'dotenv-rails', require: 'dotenv/rails-now' # ページャ gem 'kaminari','~> 1.1.1' gem 'nokogiri', '1.10.9' # デバッグ gem 'pry-rails'インストール出来たら
$ rails g devise:installModel作成
DBの構成は下図の通りです。
管理者サイトもgemを使わず、自作したいと思います。
gemを使う場合は、active_adminが便利でした。
(地道な作業)$ rails g devise admin email:string $ rails g devise customer is_active:boolean first_name:string first_name_kana:string family_name:string family_name_kana:string post_code:string address:string tel:string email:string $ rails g model address customer_id:integer post_code:string addressee:string address:string $ rails g model cart_item product_id:integer customer_id:integer quantity:integer $ rails g model genre name:string validity:boolean $ rails g model order_item product_id:integer order_id:integer quantity:integer order_price:integer make_status:integer $ rails g model order customer_id:integer addressee:string post_code:string send_to_address:string how_to_pay:boolean deliver_fee:integer order_status:integer $ rails g model product genre_id:integer name:string introduction:text status:boolean image_id:string price:integer # 一通り作ったらmigrate $ rails db:migrateRouting
Routingも仮のものを用意しておきます。
config/routes.rbRails.application.routes.draw do #rootパス root 'homes#top' # 顧客用サイトのrouting devise_for :customers, controllers: { registrations: 'customers/registrations', passwords: 'customers/passwords', sessions: 'customers/sessions'} get 'homes/top' => 'homes#top', as: 'customer_top' get 'homes/about' => 'homes#about', as: 'customer_about' resources :customers, only: [:edit, :show, :update] get 'customers/:id/withdraw' => 'customers#withdraw', as: 'customer_withdraw' patch 'customers/:id/withdraw' => 'customers#withdraw_done', as: 'customer_withdraw_done' put "/customers/:id/withdraw" => "customers#withdraw_done", as: 'customers_withdraw_done' resources :orders, only: [:new, :index, :create, :show] post 'orders/confirm' => 'orders#confirm', as: 'order_confirm' get 'orders/thanks' => 'orders#thanks', as: 'order_thanks' resources :products, only: [:index, :show] resources :order_items, only: [:index, :create, :new] resources :addresses, only: [:index, :create, :edit, :update, :destroy] resources :genres, only: [:show] #カートアイテムを全て削除メソッドのために追加 resources :cart_items, only: [:index, :create, :update, :destroy] do collection do delete 'destroy_all' end end # 管理者用サイトのrouting devise_scope :admins do devise_for :admins, controllers: { registrations: 'admins/registrations', passwords: 'admins/passwords', sessions: 'admins/sessions' } end namespace :admins do get 'homes/top' => 'homes#top', as:'top' resources :customers, only: [:index, :edit, :show, :update] resources :products, only: [:index, :create, :new, :edit, :show, :update] resources :orders, only: [:index, :create, :show, :update] resources :order_items, only: [:index, :create, :show, :update] resources :genres, only: [:index, :create, :edit, :update] get 'search' => 'searches#search', as: 'search' end end後記
ひとまずModelとRoutingのみ作りました。アクションを書き込んでwebアプリらしい動きをするまで、まだまだ道のりは遠そうです。
それにしても、チーム実装の際に3人で作ったものを1人で作ろうとすると、作業量が尋常じゃないですね。でも、当時はかなり大変だと思っていた量も、今見るとそこまで高い壁でもない気がします。初めてこのアプリを作ったのは3か月前ですが、その間に私も成長したのかも知れません。この後は、ControllerとViewを揃えたいと思います。次回へ続く!
参考
active_adminの使い方(本記事では使ってないけど)
Railsで最速で管理画面を作る!
- 投稿日:2020-07-03T18:41:02+09:00
Ruby on Rails 住所自動入力実装方法
はじめに
現在のバージョン:
macOS Catalina 10.15.3Ruby on Railsで住所自動入力を実装してみました。
備忘録として記述します。もし参考になれば幸いです。手順
(1)はじめに下記からjsファイル(jQueryプラグイン)をダウンロード。
jquery.jpostal.js(2)ダウンロードしたjsファイルをapp/assets/javascriptsに配置。
(3)Gemfileに下記を追加後、ターミナルで
$ bundle install
。Gemfile# RailsでjQueryを使えるようにするため gem 'jquery-rails' # 住所機能 gem 'jp_prefecture'(4)Userモデルに下記カラムを追加後、ターミナルで
$ rails db:migrate
。ターミナルrails generate migration AddColumnsToUsers postal_code:string prefecture_code:string address_city:string address_street:string(5)Userモデルを編集する為、app/models/user.rbに下記追加
user.rbinclude JpPrefecture jp_prefecture :prefecture_code def prefecture_name JpPrefecture::Prefecture.find(code: prefecture_code).try(:name) end def prefecture_name=(prefecture_name) self.prefecture_code = JpPrefecture::Prefecture.find(name: prefecture_name).code end(6)表示をさせるビューに下記追加(本記事はdeviseを導入して新規会員登録画面で実装)app/views/devise/registrations/new.html.erbに追加記述
new.html.erb<%= f.label :郵便番号 %> <%= f.text_field :postal_code, autocomplete: "postal_code", id: "customer_postal_code" %> <%= f.label :都道府県 %> <%= f.collection_select :prefecture_code, JpPrefecture::Prefecture.all, :name, :name, autocomplete: "prefecture_code", id: "customer_prefecture_code" %> <%= f.label :市区町村 %> <%= f.text_field :address_city, autocomplete: "address_city", id: "customer_address_city" %> <%= f.label :町名番地 %> <%= f.text_field :address_street, autocomplete: "address_street", id: "customer_address_street" %>(7)app/assets/javascripts/user.coffeeにjpostalメソッドを呼び出す為記述。
user.coffee$ -> $("#user_postcode").jpostal({ postcode : [ "#user_postcode" ], address : { "#user_prefecture_code" : "%3", "#user_address_city" : "%4", "#user_address_street" : "%5%6%7" } }) # 入力項目フォーマット # %3 都道府県 # %4 市区町村 # %5 町域 # %6 大口事業所の番地 # %7 大口事業所の名称(8)app/controllers/users_controller.rbに下記追加記述
users_controller.rbprivate def user_params params.require(:user).permit(:postcode, :prefecture_code, :address_city, :address_street) #保存を許すカラム end以上で郵便番号入力後、住所が自動で入力がされるかと思います。
補足
住所自動入力を設定した画面に遷移してリロードしないと郵便番号の自動入力がしない場合、リンクの記述に data: {"turbolinks" => false} を追加すると解消できました。
<%= link_to '会員登録', '/customers/sign_up', data: {"turbolinks" => false} %>初めて実装した場合約20分はかかりました。
ご参考になれば幸いです。
- 投稿日:2020-07-03T18:37:45+09:00
エラーAn error occurred while installing mysql2 (0.5.3), and Bundler cannot continue. Make sure that `gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'` succeeds before bundling.の解決方法
- 投稿日:2020-07-03T18:34:35+09:00
ruby on rails にて 閲覧機能をつけたい
プログラミングを初めて2週間ほどです。
NoMethodError in Todolists#index
Showing /home/vagrant/work/sample_app/app/views/todolists/index.html.erb where line #2 raised:undefined method `each' for nil:NilClass
Extracted source (around line #2):
1NoMethodError in Todolists#index
Showing /home/vagrant/work/sample_app/app/views/todolists/index.html.erb where line #2 raised:undefined method `each' for nil:NilClass
Extracted source (around line #2
1投稿一覧
2 <% @lists.each do |list| %> ←ここがエラー
3タイトル
4 <%= list.title %>
5 <% end %>このようなエラーが出て解決方法がわかりません。
余りにもわからないのでテキストをコピペしましたが未可決です。
どこを見直せば大丈夫ですか?
- 投稿日:2020-07-03T18:32:19+09:00
[Rails]'devise'エラーメッセージの日本語化
application.rbを編集
application.rbrequire_relative 'boot' require 'rails/all' # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) module Tsumiage class Application < Rails::Application config.i18n.default_locale = :ja #記入コード # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. enz endGemのインストール
ターミナル.gem 'devise-i18n' gem 'devise-i18n-views'インストール後忘れずにbundle install。
devise.views.ja.ymlを作成
ターミナル.rails g devise:views:locale ja実行結果▼
ターミナル.Running via Spring preloader in process 62024 create config/locales/devise.views.ja.ymlconfig/locales/devise.views.ja.ymlが作成される。
最後に
rails sコマンドでローカル環境を再起動。
これで'devise'のエラー文を日本語に設定できました。参考にして下さい!
- 投稿日:2020-07-03T17:24:34+09:00
rails capybara使用時に出たエラー
今回の目的として、以下の参考記事のように、
rspecテスト実行時にsign_inメソッドを利用できるようになることであった。
https://qiita.com/jnchito/items/a8360e5e7a829d1e19b2そのために、railsでcapybaraの設定を試みたところ以下のエラーが出た
それぞれのエラーに参考になったリンクを貼っておく。
また、大まかな設定は次の記事を参考にさせていただいた。
https://qiita.com/morrr/items/0e24251c049180218db4undefined method `visit'https://qiita.com/terufumi1122/items/aefd6c965e9e946efc3b
visitはcapybaraで使えるメソッドなので、設定したうえでないと上記のエラーになるらしいFailure/Error: fill_in 'email', with: user.email Capybara::ElementNotFound: Unable to find field "email" that is not disabledhttps://qiita.com/pooooon/items/4fbc429d07e4b65ed928
私の場合、下記のように変更したところエラーがでなくなった。
fill_in 'user[email]', with: user.email
fill_in 'user[password]', with: 'password'
記事通り、'session[email]'ではないパターンもあるので、きちんとブラウザで確認した方がよいだろう。
- 投稿日:2020-07-03T17:14:32+09:00
勤怠管理
- 投稿日:2020-07-03T16:32:53+09:00
【Rails 5.x】フリーフォントの導入方法
フリーフォントをRailsに導入する方法
自作のポートフォリオにフリーフォントを導入したいと思い、Qiita記事を参考に導入しました。
今更感はありますが・・・備忘録かつ、
初学者の参考になればと思い、記事を作成しました。筆者の環境
・Ruby 2.5.3
・Rails 5.2.21 フリーフォントのファイルをダウンロード
2 フォントファイルを app/assets/fonts 配下に置く
(app名)/app/assets/fontshogehoge.ttf ( または hoge.otf ) のようなフォントファイルを、fonts ディレクトリ内に入れます。
自分の場合はfontsディレクトリがなかったのでassets配下にmkdirで作成しました。3 SCSSファイルに記述
SCSSに、
custom.scss@font-face { font-family: 'hoge'; # font-family名は適宜決定 src: font-url('hogehoge.ttf') format('truetype'); font-weight: normal; font-style: normal; }このように記述します。
ファイル内のどこに記述してもよいです。
そして、custom.scssbody { font-family: hoge; }先ほどのfont-family名を
bodyに記述し、全体に適用させます。4 完成!
この手順でフリーフォントを適用できているかと思います。
意外に簡単でしたね。
反映されない場合は、適宜サーバーをrestartなどしてみてください!5 参考記事
- 投稿日:2020-07-03T16:21:27+09:00
【Rails】タイマー機能
はじめに
Railsでタイマー機能を実装したくて、調べてもなかなか欲しい情報が見つからず
友人から「javascriptで実装できるよ!」と教えてもらったら、あるわ♪あるわ♪情報が
無知過ぎる自分の戒め&【Rails タイマー】で検索した際、ヒットするよう記事に残します
どんな機能?
時間を指定し、時間になったらアラートがでるという機能になります。
例えば、3秒と入力し、3秒後にアラートがでる。
こんな感じです(Gyazoなのでタイミングによっては見れなくなります)
↓↓↓↓
https://gyazo.com/89773746aac0c2a1640d48e6a536190b実装
view.html.haml.timer__input %form{name: "timer"} %input{type: "text", value: ""}>/ 分 %input{type: "text", value: ""}>/ 秒 %br/ %input{onclick: "cntStart()", type: "button", value: "スタート"}/ %input{onclick: "cntStop()", type: "button", value: "ストップ"}/javascript.js//タイマー処理 var timer1; //カウントダウン関数を1000ミリ秒ごとに呼び出す関数 function cntStart() { document.timer.elements[2].disabled=true; timer1 = setInterval("countDown()",1000); } // タイマー停止関数 function cntStop() { document.timer.elements[2].disabled=false; crearInterval(timer1); } // カウントダウン関数 function countDown() { var min=document.timer.elements[0].value; var sec=document.timer.elements[1].value; if((min=="")&&(sec=="")) { alert("時間を設定してください!"); reSet(); } else { if(min=="")min=0; min=parseInt(min); if(sec=="")sec=0; sec=parseInt(sec); tmWrite(min*60+sec-1); } } // 残り時間を書き出す関数 function tmWrite(int) { int=parseInt(int); if(int <= 0) { reSet(); alert("時間です!"); } else { //残り分すうはintを60で割って切り捨てる document.timer.elements[0].value=Math.floor(int/60); //残り秒数はintを60で割った余り document.timer.elements[1].value=int % 60; } } // フォームを初期状態に戻す(リセット)関数 function reSet() { document.timer.elements[0].value="0"; document.timer.elements[1].value="0"; document.timer.elements[2].disabled=false; clearInterval(timer1); }※上記はCSSはございません
(CSSを実装する前に記事にしてしまったためです)
皆様のデザイン力で素敵なビューを作成してください感想
javascriptは偉大なり
というのが正直な感想です 笑
まだまだ勉強中の身ではありますが、少しずつ知識をつけていきたいですね参考
- 投稿日:2020-07-03T14:27:58+09:00
fullcalendarを使ってイベント管理できるものを作ろう。①準備
fullcalendarを使ってイベント管理できるものを作成しようと思っています。
これは、RubyonRailsの勉強を兼ねてのものです。何か間違っている、こう改善した方がいい、など指摘があれば教えていただけたらと思っています。
参考書は、「現場で使えるRuby on Rails 5速習実践ガイド」です。
さあ、頑張っていこう。環境
OS:MacOS Catalina10.15.5
Ruby:2.6.3
Rails:5.2.4アプリの作成
$ rails new Mark -d postgresql
モデルの作成
$ rails g model Event
Railsのモデルは、主に2つの要素から構成
・モデルに対応するRubyのクラス
・モデルに対応するデータベースのテーブルクラス名とテーブル名には以下の命名規約がある。
・データベースのテーブル名は、モデルのクラス名を複数形にしたもの
・モデルのクラス名はキャメルケース、テーブル名はスネークケースEventモデルの属性を設定
属性の意味 属性名・カラム名 データ型 タイトル title string 始まり start datetime 終わり end datetime 終日 allday boolean カレンダー色 color string とりあえず、こんな感じで。
db/migrate/*******_create_events.rbclass CreateEvents < ActiveRecord::Migration[6.0] def change create_table :events do |t| t.string :title, null: false t.datetime :start, null: false t.datetime :end, null: false, t.boolean :allday, null: false, default: false t.string :color, null: false t.timestamps end end end
$ rails db:migrate
マイグレーションをデータベースに適用。コントローラーとビューの作成
$bin/rails g controller events index show new edit
コントローラ名は、モデルの複数形。その後ろに必要なアクション名を追記。ルーティングの設定
config/routes.rbRails.application.routes.draw do root to: 'events#index' resources :events endfullcalendarの実装
Gemfileに追加。
gem 'fullcalendar-rails' gem 'momentjs-rails'
$ bundle install
application.jsとapplication.cssに追加
assets/javascripts/application.js//= require jquery //= require moment //= require fullcalendar //= require_tree .assets/stylesheets/application.css*= require fullcalendar */application.jsにコードを記述。
assets/javascripts/application.js$(document).ready(function() { $('#calendar').fullCalendar({ events: '/events.json' }); });viewに表示できるように追加。
app/view/events/index.html.erb<div id="calendar"></div>お、出来ました。まだ、表示させただけなので、これから中身を作っていこう。。
参考
https://qiita.com/sasasoni/items/fb0bc1644ece888ae1d4
https://qiita.com/ShoutaWATANABE/items/3d0cddafadb4f275991e
https://fullcalendar.io/
- 投稿日:2020-07-03T12:43:34+09:00
herokuで常時SSL化(rails)
知ってれば何てことないのに、英語は読まなあかんわ、
情報は断片的だわってことで流れだけでもまとめました。とりあえず、google翻訳は必須だね・・・。
前提:herokuで証明書を発行する
流れ
思い出しながらやっているので、実際のところ前後があるかもしれませんがご容赦ください
1、herokuのDynoを有料化してSSL証明書を発行できる状態にする
7ドル以上の有料プランしかSSLを発行できないです。
https://jp.heroku.com/pricing2、herokuのsetting画面でSSL Certificatesを確認(画面の真ん中らへん
基本的には自動で作られるので、有料化したら作られるよう。
なければ、configure SSlボタンより作成3、オリジナルドメイン追加
4、★重要!Point DNSを導入
herokuのメニュータブ「recoreces」 → Add ons から追加。
細かなところは、他の記事をご覧ください。で、非常に重要なのが、
非常に重要!!!なのが、
ALIASと、CNAMEのDataには、上記3で表示されているDNS Targetの値をいれること!!
初期状態のheroku.app的なものだと、SSLが適応されません!5、SSLの有効化を行う
$ heroku certs:auto:enable
を実行
恐らく問題ないかと思いますが、エラーもろもろが出たら公式を日本語訳にして読んでね!
https://devcenter.heroku.com/articles/automated-certificate-management#setup6、★重要!常時SSL化を実施
herokuでSSl化ができても、常時SSLはherokuじゃなく、
自分のrails アプリでやる必要があります!
SSL化 = SSL常時化ではないので、気を付けてください。app/environments/puroduction.rb
の中に書いてある
# config.force_ssl = true
これのコメントアウトを解除。以上です。誰かのお役に立てると幸いです
- 投稿日:2020-07-03T12:12:15+09:00
【備忘録】Railsでgraphqlのmutationが増えていくとディレクトリがグちゃるので整理したい
問題点
実用案件でmutationを黙々と増やしていくと、
api/app/graphql/mutations/
に以下例のようにファイルが溢れてしまう。(例) create_user_mutation.rb update_user_mutation.rb delete_user_mutation.rb create_news_mutation.rb update_news_mutation.rb delete_news_mutation.rb create_item_mutation.rb update_item_mutation.rb delete_item_mutation.rb create_catalog_mutation.rb update_catalog_mutation.rb delete_catalog_mutation.rbGenerateコマンド
自分はDockerを利用しているため以下のようになりますが、Dockerを利用しない場合は通常のRailsコマンドで大丈夫です。(Userを例に)
docker-compose exec 環境名 rails g graphql:mutation Users::CreateMutation docker-compose exec 環境名 rails g graphql:mutation Users::UpdateMutation docker-compose exec 環境名 rails g graphql:mutation Users::DeleteMutationファイルの確認
app/graphql/mutations/users/create_mutation.rb
mutation配下にusersのディレクトリが生成され、その配下にmutationのファイルが生成されるので確認。
/app/graphql/types/mutation_type.rb
field :users/create_mutation, mutation: Mutations::Users::CreateMutationファイルに上記のようなfieldが追加されるので、いい感じに修正する。
field :users_create_mutation, mutation: Mutations::Users::CreateMutationひとこと
わたしの調べ方が悪いのか、graphqの文献が少なく雑魚園児には辛み。ゴミのようなメモですみません。
- 投稿日:2020-07-03T10:58:38+09:00
EC2、RDSを利用してRailsアプリをデプロイする [NGINX + puma + PostgreSQL + Rails 6]
目標:AWSのEC2、RDSを利用してRailsアプリをデプロイする
- Railsチュートリアルをより実践的な環境、Ultimate Hard Modeで挑みたい
- NGINX, puma, PostgreSQL, Rails 6で開発〜デプロイ
- ローカル開発環境へ移行 はじめてのDockerでRails開発環境構築 [NGINX + Rails 5 (puma) + PostgreSQL] - Qiita
- 脱Herokuで模擬CI/CD環境を取り入れたい(この後自動デプロイまでいきたい)
- AWSを利用した、より実践的な本番環境構築のウォームアップ
このような目的のため、とりあえずAWS上でアプリが動くことを目標にしています
そのためセキュリティ面で本来必要そうな手順(環境変数定義や、SSL対応)を排除して進行しております備忘録的に記録を残しておりますので、
思い出したことや、不備あれば随時訂正いたします気づき
環境構築は学習コスト(時間)高い、けど仮想環境なら壊しても怒られないし、やってみようでなんとかなる
作ったアプリが最終的にどのような環境で動くのか、開発にトップダウンの視点が加わる
AWSの無料範囲内で学べる、学習コスト(マネー)低すぎる
環境
AWS EC2 (amazon linux2)
AWS RDS (PostgreSQL 11.6)
NGINX: 1.12.2
puma: 3.12.1
Rails: 6.0.0
Ruby: 2.6.3
備忘録
前提
- EC2インスタンスが稼働している
- VPCはインターネット接続が可能(セキュリティグループ、サブネット、ルートテーブルの設定が適切)
- EC2インスタンスとローカル環境はSSH接続が可能(鍵認証)
- root権限をもつユーザーを作成しログインしていること
- RDSでデータベースインスタンスを作成している(今回はPostgreSQL 11.6, 初期データベースsample_app_production)
- EC2はGitHubと接続しており、
/var/www/rails
ディレクトリにgit clone
でアプリを設置済み- master.keyを設定済み
参考:上記については以下のサイトを参考に進めました、感謝。
(DBの選定、設定の部分が異なっております)
【画像付きで丁寧に解説】AWS(EC2)にRailsアプリをイチから上げる方法【その1〜ネットワーク,RDS環境設定編〜】 - Qiita
パッケージのアップデート
まずは
sudo yum updateNGINXインストール
amazon-linux-extras
からインストール可能sudo amazon-linux-extras install nginx1.12 -y起動と自動起動設定
sudo systemctl start nginx && sudo systemctl enable nginx && systemctl status nginxこの時点でブラウザからEC2のElastic IPにアクセスすると
"Welcome to nginx on Amazon Linux!"というページが表示される(NGINXがポート: 80ポートで待ち受けている)参考: NGINX関連コマンド
起動終了
$ sudo systemctl start nginx $ systemctl status nginx ● nginx.service - The nginx HTTP and reverse proxy server . . . [sample_app@ip-10-0-0-44 rails]$ sudo systemctl stop nginx自動起動
sudo systemctl enable nginxPostgreSQL(クライアント)をインストール
sudo amazon-linux-extras install postgresql11参考:クライアント以外もインストールする
sudo yum install postgresql-server postgresql-devel postgresql-contrib参考:
amazon-linux-extras
を使わない方法やアンインストールなどEC2(Amazon Linux2)にPostgreSQLをインストールする | my opinion is my own
PostgreSQL install Amazon linux2
Amazon linux2にpostgresqlをインストールする手順 | 瀬戸内の雲のようにPostgreSQL install
PostgreSQL: Linux downloads (Red Hat family)PostgreSQL uninstall
How To Completely Uninstall PostgreSQL | ObjectRocketPostgreSQLに接続
PostgreSQLクライアントであるpsqlからRDSに作成したDBインスタンスに接続
psql --host=<DBインスタンスのエンドポイント> --port=5432 --dbname=sample_app_production --username=sample_app --port=5432 --dbname=sample_app_production --username=sample_app ユーザ sample_app のパスワード: psql (11.5、サーバ 11.6) SSL 接続 (プロトコル: TLSv1.2、暗号化方式: ECDHE-RSA-AES256-GCM-SHA384、ビット長: 256、圧縮: オフ) "help" でヘルプを表示します。 sample_app_production=>RDSに接続可能なこと、初期データベースが存在することが確認できれば問題ないです
参考:AWS RDS公式
PostgreSQL データベースエンジンを実行する DB インスタンスへの接続 - Amazon Relational Database Service
psql を使用した PostgreSQL DB インスタンスへの接続
psql コマンドラインユーティリティのローカルインスタンスを使用して、PostgreSQL DB インスタンスに接続できます。PostgreSQL またはクライアントコンピュータにインストールされた psql クライアントのいずれかが必要です。psql を使用して PostgreSQL DB インスタンスに接続するには、ホスト情報とアクセス認証情報を指定する必要があります。
以下の形式のいずれかを使用して、Amazon RDS 上の PostgreSQL DB インスタンスに接続します。接続時にパスワードを求められます。バッチジョブまたはスクリプトには、
--no-password
オプションを使用します。この DB インスタンスに初めて接続する場合、デフォルトのデータベース名 *postgres** を
--dbname
オプションに使用してみてください。*Unix の場合、次の形式を使用します。
psql \ --host=<DB instance endpoint> \ --port=<port> \ --username=<master user name> \ --password \ --dbname=<database name>(中略)
たとえば、次のコマンドは、架空の認証情報を使用して、
mypgdb
という PostgreSQL DB インスタンス上のmypostgresql
というデータベースに接続します。psql --host=mypostgresql.c6c8mwvfdgv0.us-west-2.rds.amazonaws.com --port=5432 --username=awsuser --password --dbname=mypgdb参考:psql認証のtrust, ident, md5
RDSでDBインスタンスを作成したときの情報を使用します
接続認証にOSのユーザー名とDBのユーザー名の一致を求める仕組みがあるようですが
RDSとのhost-client接続ではユーザー名が違ってもパスワード認証で問題なかったですPostgreSQL 認証に失敗しないための Ident、MD5、Trust 比較 - eTuts+ Server Tutorial
うまくいかない時は
タイムアウトしてしまい、認証に至らない(超重要)
セキュリティグループの設定を確認する
私はアウトバウンドの設定が必要でした(控えめに言って半日悩んだ)
EC2からRDS(MySQL)に接続できない。セキュリティグループの設定について・・・ - Qiitaクライアント(EC2)とホスト(RDS)のPostgreSQLバージョンが異なる
amazon-linux-extras
を使用せずにインストールすると
クライアント側が古いバージョンになってしまうことがありました
(RDS側は初期設定のPostgreSQL 11.6)
古いバージョンのアンインストールをして、新しいものをインストールしてください警告: psql のメジャーバージョンは 9 ですが、サーバーのメジャーバージョンは 11 です。 psql の機能の中で、動作しないものがあるかもしれません。参考(メモ):以下はクライアントとしてpsqlを利用するだけなら関係ないかも
設定ファイルの場所
postgresql.conf
sudo vi /var/lib/pgsql/data/postgresql.confpg_hba.conf(認証関係)
sudo vi /var/lib/pgsql/data/pg_hba.confユーザーpostgresのパスワードがわからない
途中自動生成されたユーザーpostgresのpasswordがわからず再設定しました
Postgresでパスワードを忘れた場合の対策 - Qiitalocal(EC2)にDBを設置する場合のコマンドメモ
$ sudo postgresql-setup initdb $ sudo systemctl enable postgresql.service $ sudo systemctl start postgresql.servicePostgreSQLの設定をする
config/database.yml
を編集default: &default adapter: postgresql encoding: unicode username: postgres password: password pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> timeout: 5000 development: <<: *default host: db database: myapp_development test: <<: *default host: db database: myapp_test production: <<: *default database: sample_app_production username: postgres # RDS DBインスタンスのユーザー名 password: password # RDS DBインスタンスのパスワード host: xxxx.xxxx.ap-northeast-1.rds.amazonaws.com # RDS DBインスタンスのエンドポイント本来ここに平文でユーザー名やパスワードを記録しないほうがよい
productionの
<<: *default
が抜けていてadapterが指定されていないよと半日怒られ続けました...
"adapterActiveRecord::AdapterNotSpecified: database configuration does not specify adapter"hostを指定していればportの指定は不要でした
参考:database.ymlのPostgreSQL向け設定情報 ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
NGINXの設定をする
dockerで環境構築したものが引き継がれたらいいのに...
pumaとsocket接続するようにしています
/etc/nginx/conf.d/sample_app.confを作成、編集
vi /etc/nginx/conf.d/sample_app.conferror_log /var/www/rails/sample_app/log/nginx.error.log; access_log /var/www/rails/sample_app/log/nginx.access.log; ##ココ upstream sample_app { server unix:///var/www/rails/sample_app/tmp/sockets/puma.sock; } server { listen 80; client_max_body_size 4G; server_name 54.238.15.249; keepalive_timeout 5; # Location of our static files root /var/www/rails/sample_app/public; location ~ ^/assets/ { root /var/www/rails/sample_app/public; } location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; if (!-f $request_filename) { proxy_pass http://sample_app; break; } } error_page 500 502 503 504 /500.html; location = /500.html { root /var/www/rails/sample_app/public; } }NGINX再起動
sudo systemctl restart nginx参考:NGINXコマンドほか
自動起動設定
sudo chkconfig nginx onpumaの設定をする
アプリケーションサーバーとしてunicornは使用しません
Railsに組み込まれているpumaを利用してみますNGINXとpumaはソケット接続するため
config/puma.rb
に以下を2点編集
(私はDockerで開発環境をNGINX + pumaに設定をした時点で設定済みでした)# ポート: 3000をlistenしない #port ENV.fetch("PORT") { 3000 } # socketの設定 bind "unix://#{Rails.root}/tmp/sockets/puma.sock"参考:puma.rbの設定 puma/dsl.rb at master · puma/puma
参考:puma関連コマンド
production環境でpuma起動
bundle exec rails s -e productionpuma, pumactlコマンドは私の環境では使えませんでした
bundle exec pumactl startPumaの起動におけるpumaコマンドとpumactlコマンドの違い - Qiita
pumaコマンドとpumactlコマンドの違い
このあたりちゃんと説明した日本語の情報がとても少なくて、リポジトリのREADMEを読んだり「what is deference between puma command and pumactl command」みたいなキーワードでがんばって調べたらRuby Journalになんかそれっぽいことが書いてあった。英語ができないととてもつらい。
As we can see that above operations can be tedious and error prone and definitely not fun to work with a big deployment scale. Introducing pumactl, this utility automates all of above tasks.
Digesting Pumactl要するに、pumaコマンドで起動したりしてるといちいちオプション付けないといけないから大規模開発だとめっちゃつらいしやばいくらい闇だし、pumaコマンドでやってることはpumactlコマンドで自動化しような。ということを言っている。
なるほど、確かに。pumactlコマンドだと問答無用で設定ファイルが読まれるようになっているから、スレッドの数はどうするんだだの、ポート番号はどうするんだだの、ちゃんとコードで管理できる。
停止や再起動においてもpumactlコマンドで安全にできるらしい。
Available commands: halt, restart, phased-restart, start, stats, status, stop, reload-worker-directory
YarnとNodeをインストール、アップデート
Rails 6以降で必要
以降の過程でYarnが必要、nodeが古いと言われますYarn
# wgetインストール済みなら省略可能 $ yum -y install wget $ wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo $ curl --silent --location https://rpm.nodesource.com/setup_6.x | bash - $ sudo yum install yarn $ yarn --version 1.22.4Node
$ sudo npm install n -g $ sudo n stable $ node -v v12.18.2参考:capistrano・EC2・postgresql・rails6で自動デプロイ設定した際のエラー例 - Qiita
アプリを起動させる
マイグレーション、アセットプリコンパイル
# マイグレーション bundle exec rake db:migrate RAILS_ENV=production # プリコンパイル bundle exec rake assets:precompile RAILS_ENV=productionWebサーバー、アプリケーションサーバー起動
# NGINX再起動 sudo service nginx restart # puma起動 bundle exec rails s -e production問題なければブラウザからElastic IPにアクセスすることで、
アプリのインターフェイスが表示されるはずです"We're sorry, but something went wrong."
もうね、怒られるのなれましたよ
ブラウザにに上記メッセージが表示される場合ログを確認しましょう
cd log # アプリルート直下 tail -n 30 production.log tail -n 30 nginx.error.log私の場合
production.log
は空っぽで
nginx.error.log
で以下のエラーが記録されていました"connect() to unix:/home/var/www/rails/sample_app/tmp/sockets/puma.sock failed (2: No such file or directory) while connecting to upstream"
/etc/nginx/conf.d/sample_app.confのsocketパス指定が間違っておりました
その他既知の問題点
SSL非対応
config/environments/production.rb
のconfig.force_ssl = true
をconfig.force_ssl = false
に新規ユーザー登録のところでメール認証がうまく行かない
今後対応
- 投稿日:2020-07-03T01:04:08+09:00
Ruby on Rails チュートリアル(第4版) 第4章
4.2 nanoエディタから離脱する際、
Ctrl+X
→Y
だけでは離脱出来ず。Enter
キーを押せば離脱出来た(ググった)。4.2.2 演習
1.city変数に適当な市区町村を、prefecture変数に適当な都道府県を代入してください。
>> city = "横浜市" => "横浜市" >> prefecture = "神奈川県" => "神奈川県"2.先ほど作った変数と式展開を使って、「東京都 新宿区」のような住所の文字列を作ってみましょう。出力にはputsを使ってください。
>> puts "#{prefecture} #{city}" 神奈川県 横浜市 => nil3.上記の文字列の間にある半角スペースをタブに置き換えてみてください。(ヒント: 改行文字と同じで、タブも特殊文字です)
tabキー押しても何も無いと思ったら、\t
でtabらしい。>> puts "#{prefecture}\t#{city}" 神奈川県 横浜市 => nil4.タブに置き換えた文字列を、ダブルクォートからシングルクォートに置き換えてみるとどうなるでしょうか?
>> puts '#{prefecture}\t#{city}' #{prefecture}\t#{city} => nil4.2.3 演習
1."racecar" の文字列の長さはいくつですか? lengthメソッドを使って調べてみてください。
>> "racecar".length => 72.reverseメソッドを使って、"racecar"の文字列を逆から読むとどうなるか調べてみてください。
>> "racecar".reverse => "racecar"3.変数sに "racecar" を代入してください。その後、比較演算子 (==) を使って変数sとs.reverseの値が同じであるかどうか、調べてみてください。
>> s = "racecar" => "racecar" >> s == s.reverse => true4.リスト 4.9を実行すると、どんな結果になるでしょうか? 変数sに "onomatopoeia" という文字列を代入するとどうなるでしょうか? ヒント: 上矢印 (またはCtrl-Pコマンド) を使って以前に使ったコマンドを再利用すると一からコマンドを全部打ち込む必要がなくて便利ですよ。)
>> puts "It's a palindrome!" if s == s.reverse It's a palindrome! => nil >> s = "onomatopoeia" => "onomatopoeia" >> puts "It's a palindrome!" if s == s.reverse => nil4.2.4 演習
1.リスト 4.10のFILL_INの部分を適切なコードに置き換え、回文かどうかをチェックするメソッドを定義してみてください。ヒント: リスト 4.9の比較方法を参考にしてください。
>> def palindrome_tester(s) >> if s == s.reverse >> puts "It's a palindrome!" >> else >> puts "It's not a palindrome." >> end >> end => :palindrome_tester2.上で定義したメソッドを使って “racecar” と “onomatopoeia” が回文かどうかを確かめてみてください。1つ目は回文である、2つ目は回文でない、という結果になれば成功です。
>> palindrome_tester("racecar") It's a palindrome! => nil >> palindrome_tester("onomatopoeia") It's not a palindrome. => nil3.palindrome_tester("racecar")に対してnil?メソッドを呼び出し、戻り値がnilであるかどうかを確認してみてください (つまりnil?を呼び出した結果がtrueであることを確認してください)。このメソッドチェーンは、nil?メソッドがリスト 4.10の戻り値を受け取り、その結果を返しているという意味になります。
>> palindrome_tester("racecar").nil? It's a palindrome! => true4.3.1 演習
1.文字列「A man, a plan, a canal, Panama」を ", " で分割して配列にし、変数aに代入してみてください。
>> a="A man,a plan,a canal,Panama".split(',') => ["A man", "a plan", "a canal", "Panama"]2.今度は、変数aの要素を連結した結果 (文字列) を、変数sに代入してみてください。
>> s = a.join => "A mana plana canalPanama"3.変数sを半角スペースで分割した後、もう一度連結して文字列にしてください (ヒント: メソッドチェーンを使うと1行でもできます)。リスト 4.10で使った回文をチェックするメソッドを使って、(現状ではまだ) 変数sが回文ではないことを確認してください。downcaseメソッドを使って、s.downcaseは回文であることを確認してください。
>> s = s.split.join => "AmanaplanacanalPanama" >> palindrome_tester(s) It's not a palindrome. => nil >> palindrome_tester(s.downcase) It's a palindrome! => nil4.aからzまでの範囲オブジェクトを作成し、7番目の要素を取り出してみてください。同様にして、後ろから7番目の要素を取り出してみてください。(ヒント: 範囲オブジェクトを配列に変換するのを忘れないでください)
>> a = ('a'..'z').to_a => ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"] >> a[6] => "g" >> a[-7] => "t"4.3.2 演習
1.範囲オブジェクト0..16を使って、各要素の2乗を出力してください。
>> (0..16).each { |i| puts i**2 } 0 1 4 9 16 25 36 49 64 81 100 121 144 169 196 225 256 => 0..162.yeller (大声で叫ぶ) というメソッドを定義してください。このメソッドは、文字列の要素で構成された配列を受け取り、各要素を連結した後、大文字にして結果を返します。例えばyeller(['o', 'l', 'd'])と実行したとき、"OLD"という結果が返ってくれば成功です。ヒント: mapとupcaseとjoinメソッドを使ってみましょう。
>> def yeller(x='') >> x.map(&:upcase).join >> end => :yeller >> yeller(['o', 'l', 'd']) => "OLD"3.random_subdomainというメソッドを定義してください。このメソッドはランダムな8文字を生成し、文字列として返します。ヒント: サブドメインを作るときに使ったRubyコードをメソッド化したものです。
>> def random_subdomain >> ('a'..'z').to_a.shuffle[0..7].join >> end => :random_subdomain4.リスト 4.12の「?」の部分を、それぞれ適切なメソッドに置き換えてみてください。ヒント:split、shuffle、joinメソッドを組み合わせると、メソッドに渡された文字列 (引数) をシャッフルさせることができます。
>> def string_shuffle(s) >> s.split('').shuffle.join >> end => :string_shuffle >> string_shuffle("foobar") => "robofa"4.3.3 演習
だんだん難しくなってきた。
1.キーが'one'、'two'、'three'となっていて、それぞれの値が'uno'、'dos'、'tres'となっているハッシュを作ってみてください。その後、ハッシュの各要素をみて、それぞれのキーと値を"'#{key}'のスペイン語は'#{value}'"といった形で出力してみてください。
>> a = { one: "uno", two: "dos", three: "tres"} => {:one=>"uno", :two=>"dos", :three=>"tres"} >> a.each do |key, value| ?> puts "'#{key}'のスペイン語は'#{value}'" >> end 'one'のスペイン語は'uno' 'two'のスペイン語は'dos' 'three'のスペイン語は'tres' => {:one=>"uno", :two=>"dos", :three=>"tres"}2.person1、person2、person3という3つのハッシュを作成し、それぞれのハッシュに:firstと:lastキーを追加し、適当な値 (名前など) を入力してください。その後、次のようなparamsというハッシュのハッシュを作ってみてください。1.) キーparams[:father]の値にperson1を代入、2). キーparams[:mother]の値にperson2を代入、3). キーparams[:child]の値にperson3を代入。最後に、ハッシュのハッシュを調べていき、正しい値になっているか確かめてみてください。(例えばparams[:father][:first]がperson1[:first]と一致しているか確かめてみてください)
>> person1 = { first: "Taro", last: "Tanaka" } => {:first=>"Taro", :last=>"Tanaka"} >> person2 = { first: "Jiro", last: "Sato" } => {:first=>"Jiro", :last=>"Sato"} >> person3 = { first: "Saburo", last: "Suzuki" } => {:first=>"Saburo", :last=>"Suzuki"} >> params = {} => {} >> params[:father] = person1 => {:first=>"Taro", :last=>"Tanaka"} >> params[:mother] = person2 => {:first=>"Jiro", :last=>"Sato"} >> params[:child] = person3 => {:first=>"Saburo", :last=>"Suzuki"} >> params[:father][:first] == person1[:first] => true3.userというハッシュを定義してみてください。このハッシュは3つのキー:name、:email、:password_digestを持っていて、それぞれの値にあなたの名前、あなたのメールアドレス、そして16文字からなるランダムな文字列が代入されています。
>> user = { name: "muramako", email: "muramako@example.com", password_digest: ('a'..'z').to_a.shuffle[0..15].join } => {:name=>"muramako", :email=>"muramako@example.com", :password_digest=>"xqsthfkjovdimeua"}4.Ruby API (訳注: もしくはるりまサーチ) を使って、Hashクラスのmergeメソッドについて調べてみてください。次のコードを実行せずに、どのような結果が返ってくるか推測できますか? 推測できたら、実際にコードを実行して推測があっていたか確認してみましょう。
{ "a" => 100, "b" => 200 }.merge({ "b" => 300 })
"b" => 200 が "b" => 300 に変わる>> { "a" => 100, "b" => 200 }.merge({ "b" => 300 }) => {"a"=>100, "b"=>300}合ってた!
4.4.1 演習
1.1から10の範囲オブジェクトを生成するリテラルコンストラクタは何でしたか? (復習です)
1..102.今度はRangeクラスとnewメソッドを使って、1から10の範囲オブジェクトを作ってみてください。ヒント: newメソッドに2つの引数を渡す必要があります
>> a = Range.new(1..10) Traceback (most recent call last): 3: from (irb):85 2: from (irb):85:in `new' 1: from (irb):85:in `initialize' ArgumentError (wrong number of arguments (given 1, expected 2..3))自信満々に打ったら、エラーが出た。
Range.new
の範囲の指定について調べたら,で引数を2つ指定すればいいらしい(そういう意味か!)。>> a = Range.new(1,10) => 1..103.比較演算子==を使って、上記2つの課題で作ったそれぞれのオブジェクトが同じであることを確認してみてください。
>> a = Range.new(1,10) => 1..10 >> a == (1..10) => true4.4.2 演習
1.Rangeクラスの継承階層を調べてみてください。同様にして、HashとSymbolクラスの継承階層も調べてみてください。
>> Range.class => Class >> Range.class.superclass => Module >> Range.class.superclass.superclass => Object >> Range.class.superclass.superclass.superclass => BasicObject >> Hash.class => Class >> Hash.class.superclass => Module >> Hash.class.superclass.superclass => Object >> Hash.class.superclass.superclass.superclass => BasicObject >> Symbol.class => Class >> Symbol.class.superclass => Module >> Symbol.class.superclass.superclass => Object >> Symbol.class.superclass.superclass.superclass => BasicObject2.リスト 4.15にあるself.reverseのselfを省略し、reverseと書いてもうまく動くことを確認してみてください。
>> class Word < String >> def palindrome? >> self == reverse >> end >> end => :palindrome? >> s = Word.new("level") => "level" >> s.palindrome? => true4.4.3 演習
1.palindrome?メソッドを使って、“racecar”が回文であり、“onomatopoeia”が回文でないことを確認してみてください。南インドの言葉「Malayalam」は回文でしょうか? ヒント: downcaseメソッドで小文字にすることを忘れないで。
>> class String >> def palindrome? >> self == reverse >> end >> end => :palindrome? >> "racecar".palindrome? => true >> "onomatopoeia".palindrome? => false >> "Malayalam".downcase.palindrome? => true2.リスト 4.16を参考に、Stringクラスにshuffleメソッドを追加してみてください。ヒント: リスト 4.12も参考になります。
>> class String >> def shuffle >> self.split('').shuffle.join >> end >> end => :shuffle >> "foobar".shuffle => "roboaf"3.リスト 4.16のコードにおいて、self.を削除してもうまく動くことを確認してください。
>> class String >> def shuffle >> split('').shuffle.join >> end >> end => :shuffle >> "foobar".shuffle => "rofaob"4.4.4 演習
1.第2章で作ったToyアプリケーションのディレクトリでRailsコンソールを開き、User.newと実行することでuserオブジェクトが生成できることを確認してみましょう。
>> user = User.new => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>2.生成したuserオブジェクトのクラスの継承階層を調べてみてください。
>> user.class => User(id: integer, name: string, email: string, created_at: datetime, updated_at: datetime) >> user.class.superclass => ApplicationRecord(abstract) >> user.class.superclass.superclass => ActiveRecord::Base >> user.class.superclass.superclass.superclass => Object >> user.class.superclass.superclass.superclass.superclass => BasicObject4.4.5 演習
1.Userクラスで定義されているname属性を修正して、first_name属性とlast_name属性に分割してみましょう。また、それらの属性を使って "Michael Hartl" といった文字列を返すfull_nameメソッドを定義してみてください。最後に、formatted_emailメソッドのnameの部分を、full_nameに置き換えてみましょう (元々の結果と同じになっていれば成功です)
/example_user.rbclass User attr_accessor :first_name, :last_name, :email def initialize(attributes = {}) @first_name = attributes[:first_name] @last_name = attributes[:last_name] @email = attributes[:email] end def full_name "#{@last_name} #{@first_name}" end def formatted_email "#{full_name} <#{@email}>" end end>> user = User.new(first_name: "Hartl", last_name: "Michael", email: "mhartl@example.com") => #<User:0x00007f8148fbcf40 @first_name="Hartl", @last_name="Michael", @email="mhartl@example.com"> >> user.formatted_email => "Michael Hartl <mhartl@example.com>"2."Hartl, Michael" といったフォーマット (苗字と名前がカンマ+半角スペースで区切られている文字列) で返すalphabetical_nameメソッドを定義してみましょう。
def alphabetical_name "#{@first_name},#{@last_name}" end3.full_name.splitとalphabetical_name.split(', ').reverseの結果を比較し、同じ結果になるかどうか確認してみましょう。
>> user.full_name.split == user.alphabetical_name.split(',').reverse => trueメモ
- Rubyはシングルクォート文字列の中では式展開を行わない。
- Railsでは自動的にヘルパーモジュールを読み込んでくれるので、include行をわざわざ書く必要がなく、メゾットが自動的にすべてのビューで利用できるようになっている。
initialize
は、User.newを実行すると自動的に呼び出されるRubyの特殊なメソッド。
- 投稿日:2020-07-03T00:41:43+09:00
Rails こんなこともできるよ中間テーブル
どうもチャンクノです!!
今回は中間テーブルについて書いていきます。多分基本的に中間テーブルを作成する場合、多対多の両テーブルのidを中間テーブルに持たせると思います。
https://qiita.com/gomasio1010/items/2cff1e11d2cc82fde464
この記事のような感じ。でも中間テーブルって必ずしもidじゃなくていいみたいです。
自分が今担当している部分の中間テーブルはcreate_table :search_condition_areas do |t| t.string :area_code t.references :search_condition, foreign_key: true t.timestamps endこんな感じになってます。
このままだと :area_codeがただのstring型のカラムになってしまうので、
has_many :search_condition_areas, foreign_key: :area_code, primary_key: :code has_many :search_conditions, through: :search_condition_areasモデル側でforeign_keyとprimary_keyを指定してあげてます。
これで基本的な中間テーブルを作成した時と同じ状態になりました。さて、気になるのはこれをcreateする時ですね。
def search_condition_params params.require(:search_condition).permit( :hoge, :huga, :foo, area_?: [] <= ここ ) endここの?に何が入るかわかりますか?
僕は脳死で area_codes: [] にしました!!!
え、違うの?そんな声が聞こえてきそうですねえ。うんうん。違うんです。
ここは普通に area_ids: [] です。
例えば @search_condition.area_ids とコンソールで叩けばidではなく[83] pry(main)> @search_condition.area_ids => ["11101000000", "13101000000", "13103000000"]こんな感じで @search_condition に紐づく中間テーブルが持っているarea_codeが返ってきます。
[99] pry(main)> area.search_condition_ids (1.8ms) SELECT `search_conditions`.`id` FROM `search_conditions` INNER JOIN `search_condition_areas` ON `search_conditions`.`id` = `search_condition_areas`.`search_condition_id` WHERE `search_condition_areas`.`area_code` = '13000000000' => [3]逆も然り。これは普通にsearch_conditionのidが返ってきます。
〜ids <= この部分は collection_singular_ids このメソッドを使っています。
一対多の時使えるメソッドみたいですね。
https://railsdoc.com/association
ここに色々書かれています?♂️
https://www.rubydoc.info/gems/activerecord/ActiveRecord%2FAssociations%2FClassMethods:has_many
英語できる人はこっちの方がいいっぽいです。これがあったので、もしかしてarea_codesじゃなくてarea_idsなんじゃね?って気付くことができました!!
で、ここで気を付けなければいけないのが
@search_condition.area_ids = @new_search_condition.area_ids @search_condition.saveコントローラーの処理でこのような記述を使用する場合ですね。
どうなると思いますか?@search_condition.area_ids = @new_search_condition.area_idsこれ @search_condition.save の処理が走る前に上記の部分が上書き保存されてしまいます。
どうやらautosaveが自動で走ってしまうみたいです。
accepts_nested_attributes_for を使っている場合必ずautosaveがtrueになってしまうのでどうしようもないっぽいです。User.rb has_one :search_condition accepts_nested_attributes_for :search_condition <= 多分ここが原因おそらくですが親テーブルに accepts_nested_attributes_for が使用されていたらautosave: falseは使えないんでしょうね。
なので、ここでは親テーブルが保存されてから子テーブルを上書き保存する処理を書くのがいいのかなーと思います。if @search_condition.save @search_condition.area_ids = new_search_condition.area_ids else render "new" endこんな感じ。
多分もっといいやり方があるんだろうなあ。ごちゃごちゃと色々書いてきましたが、まとめると
・中間テーブルに持たせるものは両テーブルのidじゃなくても良い。 ・でもその場合はidを持たせないテーブルにforeign_keyとprimary_keyの記述を忘れずに。 ・idを持たせない場合でもコントローラーのパラメーターは必ず~idsにする。 ・accepts_nested_attributes_forを使用している場合はautosaveに気を付ける。こんなところですかね?
あんまり中間テーブルの実装には触れてこなかったのでお勉強になりました?
覚えることは山のようにあるなあ、、、。
では今回はこんな感じで!!
間違ってるところとかあったらコメントください!
それでは皆様良きプログラミングライフを?
- 投稿日:2020-07-03T00:17:28+09:00
Rails Tutorial MEMO#8
Rails Tutorial第8章
基本的なログイン機構
本章では、ログインの基本的な仕組みを実装していく。
8.1 セッション
HTTPはそれより前のリクエストの情報を全く利用できない、Statelessなプロトコル。故にユーザーのIDを保持しておく手段がHTTPプロトコル内「には」全く無い。
ユーザーログインの必要なWebアプリケーションでは、セッション(Session)と呼ばれる半永続的な接続をコンピュータ間(ユーザーのパソコンのWebブラウザとRailsサーバーなど)に別途設定する。
Railsでセッションを実装する方法として最も一般的なのは、cookies
を使う方法。8.1.1 Sessionsコントローラ
create
アクションにPOSTリクエストを送信すると、実際にログインする。
destroy
アクションにDELETEリクエストを送信するとログアウトする。
Sessionsコントローラを生成する
$rails generate controller Sessions new
routesにリソースを追加する。config/routes.rbRails.application.routes.draw do root 'static_pages#home' get '/help', to: 'static_pages#help' get '/about', to: 'static_pages#about' get '/contact', to: 'static_pages#contact' get '/signup', to: 'users#new' get '/login', to: 'sessions#new' #新しいセッションのページ post '/login', to: 'sessions#create' #新しいセッションの作成 delete '/logout', to: 'sessions#destroy' #セッションの削除 resources :users endSessionsコントローラのテストで名前付きルートを使うようにする。
test/controllers/sessions_controller_test.rbrequire 'test_helper' class SessionsControllerTest < ActionDispatch::IntegrationTest test "should get new" do get login_path assert_response :success end end
$rails routes
コマンドで現状のルーティングを確認できる。8.1.2 ログインフォーム
セッションフォームとユーザー登録フォームの最大の違いは、セッションにはSessionモデルというものがなく、そのため@userのようなインスタンス変数に相当するものもない点。したがって、新しいセッションフォームを作成するときには、form_forヘルパーに追加の情報を独自に渡さなければならない。
Railsでは以下のように書くだけで、「フォームのactionは/usersというURLへのPOSTである」と自動的に判定するが
form_for(@user)
↓セッションの場合は、リソースの名前とそれに対応するURLを具体的に指定する必要がある
form_for(:session, url: login_path)
ログインフォームのコードapp/views/sessions/new.html.erb<% provide(:title, "Log in") %> <h1>Log in</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(:session, url: login_path) do |f| %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.submit "Log in", class: "btn btn-primary" %> <% end %> <p>New user? <%= link_to "Sign up now!", signup_path %></p> </div> </div>8.1.3 ユーザーの検索と認証
ログインでセッションを作成する場合の作業順番
1,入力が無効な場合の処理
2,ログインが失敗した場合に表示されるエラーメッセージの配置
3,ログイン成功した場合に使う土台部分作成
今回はパスワードとメールアドレスの組み合わせが有効かどうかの判定
createアクションを実行するとnewビューが出力されるapp/controllers/sessions_controller.rbclass SessionsController < ApplicationController def new end def create render 'new' end def destroy end endcerateで最初に失敗したログインのデバッグ情報 --- session: email: 'user@example.com' password: 'foobar' commit: Log in action: create controller: sessionsparamsは次のような入れ子ハッシュ(ハッシュの中にハッシュがある構造)になっている
{ session: { password: "foobar", email: "user@example.com" } }
createアクションの中では、ユーザーの認証に必要なあらゆる情報をparamsハッシュから簡単に取り出せる。app/controllers/sessions_controller.rbclass SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) # ユーザーログイン後にユーザー情報のページにリダイレクトする else # エラーメッセージを作成する render 'new' end end def destroy end end
&&(論理積(and))
は、取得したユーザーが有効かどうかを決定するために使う。8.1.4 フラッシュメッセージを表示する
app/controllers/sessions_controller.rbclass SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) # ユーザーログイン後にユーザー情報のページにリダイレクトする else flash[:danger] = 'Invalid email/password combination' # 本当は正しくない render 'new' end end def destroy end end上記のままでは一度表示されたフラッシュメッセージが消えずに残ってしまう
8.1.5 フラッシュのテスト
最初に統合テストを生成する
$ rails generate integration_test users_login
以下テストコード再現の流れ
1,ログイン用のパスを開く
2,新しいセッションのフォームが正しく表示されたことを確認する
3,わざと無効なparamsハッシュを使ってセッション用パスにPOSTする
4,新しいセッションフォームが再度表示され、フラッシュメッセージが追加されることを確認する
5,別のページに一旦移動する
6,移動先のページでフラッシュメッセージが表示されていないことを確認するtest/integration/users_login_test.rbrequire 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest test "login with invalid information" do get login_path (1) assert_template 'sessions/new' (2) post login_path, params: { session: { email: "", password: "" } } (3) assert_template 'sessions/new' (4)新しいセッションフォームが再表示されることを確認 assert_not flash.empty? (4)フラッシュメッセージが追加されることを確認 get root_path (5) assert flash.empty? (6) end end
$ rails test test/integration/users_login_test.rb
このようにrails testの引数にテストファイルを与えると、そのテストだけを実行することができる
テストをパスさせるにはcreateアクションのflash
をflash.now
に置き換える
flash.now
のメッセージはその後のリクエストが発生したときに消滅する
これによってテストもGREENになる8.2 ログイン
ログイン中の状態での有効な値の送信をフォームで正しく扱えるようにする
セッションを実装するには様々なコントローラやビューで沢山のメソッドを定義する必要がある。そうしたメソッドを一箇所にパッケージ化できるRubyのモジュール機能を使う。
Sessionsコントローラを生成した時点で既にセッション用ヘルパーモジュールも自動生成されている。さらに、Railsのセッション用ヘルパーはビューにも自動的に読み込まれる。Railsの全コントローラの親クラスであるApplicationコントローラにこのモジュールを読み込ませれば、どのコントローラでも使えるようになる。app/controllers/application_controller.rbclass ApplicationController < ActionController::Base protect_from_forgery with: :exception include SessionsHelper end8.2.1 log_inメソッド
Railsで事前定義済みの
session
メソッドを使って、単純なログインを行えるようにする。このsession
メソッドはハッシュのように扱える。
seesion[:user_id] = user.id
上のコードを実行すると、ユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成される。app/helpers/sessions_helper.rbmodule SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end endsessionメソッドで作成した一時cookiesは自動的に暗号化され、上記のコードは保護される。
app/controllers/sessions_controller.rbclass SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) log_in user redirect_to user else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end def destroy end endこれで
app/views/sessions/new.html.erb
で定義したログインフォームも正常に動作するようになった。8.2.2 現在のユーザー
current_user
メソッドを定義して、セッションIDに対応するユーザー名をデータベースから取り出せるようにする。しかし、find
を使うと例外が発生してしまう。find_by
メソッドを使うことで、IDが無効な場合(=ユーザーが存在しない場合)にもメソッドは例外を発生せず、nilを返す。
User.find_by(id: session[:user_id])
app/helpers/sessions_helper.rbdef current_user if session[:user_id] @current_user ||= User.find_by(id: session[:user_id]) end end endif @current_user.nil? @current_user = User.find_by(id: session[:user_id]) else @current_user endor演算子「||」を使えば上記の「メモ化」コードが次のように変換できる
@current_user = @current_user || User.find_by(id: session[:user_id])
「Ruby的に」正しいコードではないので以下のように変換させる
この書き換えは、 x = x + 1 が x += 1 になることと同じ
@current_user ||= User.find_by(id: session[:user_id])
8.2.3 レイアウトリンクを変更する
ユーザーがログインしている時とそうでない時でレイアウトを変更する。
論理値を返すlogged_in?
メソッドを定義、ユーザーがログイン中の状態とは「sessionにユーザーidが存在している」こと、つまりcurrent_user
がnil
ではないという状態を指す。これをチェックするには!(否定演算子)
を使っていく。app/helpers/sessions_helper.rbmodule SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end # 現在ログイン中のユーザーを返す (いる場合) def current_user if session[:user_id] @current_user ||= User.find_by(id: session[:user_id]) end end # ユーザーがログインしていればtrue、その他ならfalseを返す def logged_in? !current_user.nil? end endログイン中のユーザー用のレイアウトのリンクを変更する
app/views/layouts/_header.html.erb<header class="navbar navbar-fixed-top navbar-inverse"> <div class="container"> <%= link_to "sample app", root_path, id: "logo" %> <nav> <ul class="nav navbar-nav navbar-right"> <li><%= link_to "Home", root_path %></li> <li><%= link_to "Help", help_path %></li> <% if logged_in? %> <li><%= link_to "Users", '#' %></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> Account <b class="caret"></b> </a> <ul class="dropdown-menu"> <li><%= link_to "Profile", current_user %></li> <li><%= link_to "Settings", '#' %></li> <li class="divider"></li> <li> <%= link_to "Log out", logout_path, method: :delete %> </li> </ul> </li> <% else %> <li><%= link_to "Log in", login_path %></li> <% end %> </ul> </nav> </div> </header>レイアウトに新しいリンクを追加したので、上記のコードにBootstrapのドロップダウンメニュー機能
dropdown
クラスやdropdown-menu
など使えるようになる。これらのドロップダウン機能を有効にするため、Railsのapplication.jsファイルを通して、Bootstrapに同梱されているJavaScriptライブラリとjQueryを読み込むようアセットパイプラインに指示する。app/assets/javascripts/application.js//= require rails-ujs //= require jquery //= require bootstrap //= require turbolinks //= require_tree .8.2.4 レイアウトの変更をテストする
統合テストを書いてこの動作をテストで表現し、今後の回帰バグの発生をキャッチで切るようにする。手順は
test/integration/users_login_test.rb
を元に作成する。テストの確認をするためにはテスト時に登録済みユーザーとしてログインしておく必要がある。Railsでは、このようなテスト用データを
fixture(フィクスチャ)
で作成できる。このfixtureを使って、テストに必要なデータをtestデータベースに読み込んでおくことができる。現時点のテストでは、ユーザーは一人いれば問題なので有効な名前とメールアドレスを設定しておく。テスト中にそのユーザーとして自動ログインするために、そのユーザーの有効なパスワードも用意して、
Sessions
コントローラのcreate
アクションに送信されたパスワードと比較できるようにする必要がある。app/models/user.rbclass User < ApplicationRecord before_save { self.email = email.downcase } validates :name, presence: true, length: { maximum: 50 } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false } has_secure_password validates :password, presence: true, length: { minimum: 6 } # 渡された文字列のハッシュ値を返す def User.digest(string) cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost BCrypt::Password.create(string, cost: cost) end endtest/fixtures/users.ymlmichael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %>test/integration/users_login_test.rbrequire 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) #fixtureのデータを参照 end . . . test "login with valid information" do get login_path post login_path, params: { session: { email: @user.email, password: 'password' } } assert_redirected_to @user #リダイレクト先が正しいかチェック follow_redirect! #リダイレクト先に移動、ログイン用リンクが表示されなくなったことを確認 assert_template 'users/show' assert_select "a[href=?]", login_path, count: 0 assert_select "a[href=?]", logout_path assert_select "a[href=?]", user_path(@user) end end8.2.5 ユーザー登録時にログイン
登録の終わったユーザーがデフォルトでログインされている状態にする
Usersコントローラのcreateアクションにlog_inを追加するapp/controllers/users_controller.rbclass UsersController < ApplicationController def show @user = User.find(params[:id]) end def new @user = User.new end def create @user = User.new(user_params) if @user.save log_in @user #追加 flash[:success] = "Welcome to the Sample App!" redirect_to @user else render 'new' end end private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end endtest/integration/users_signup_test.rbrequire 'test_helper' class UsersSignupTest < ActionDispatch::IntegrationTest . . . test "valid signup information" do get signup_path assert_difference 'User.count', 1 do post users_path, params: { user: { name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password" } } end follow_redirect! assert_template 'users/show' assert is_logged_in? #追加 end end8.3 ログアウト
ログアウト機能を追加する
app/helpers/sessions_helper.rbmodule SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end . . . # 現在のユーザーをログアウトする def log_out session.delete(:user_id) @current_user = nil end endapp/controllers/sessions_controller.rbclass SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) log_in user redirect_to user else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end def destroy log_out redirect_to root_url end endユーザーログアウトのテスト
test/integration/users_login_test.rbrequire 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest . . . test "login with valid information followed by logout" do get login_path post login_path, params: { session: { email: @user.email, password: 'password' } } assert is_logged_in? #ログイン出来ているか確認 assert_redirected_to @user #以降ログインテスト follow_redirect! assert_template 'users/show' assert_select "a[href=?]", login_path, count: 0 assert_select "a[href=?]", logout_path assert_select "a[href=?]", user_path(@user) delete logout_path #以降ログアウトテスト assert_not is_logged_in? assert_redirected_to root_url follow_redirect! assert_select "a[href=?]", login_path assert_select "a[href=?]", logout_path, count: 0 assert_select "a[href=?]", user_path(@user), count: 0 end endまとめ
- Railsのsessionメソッドを使うと、あるページから別のページに移動するときの状態を保持できる。一時的な状態の保存にはcookiesも使える
- ログインフォームでは、ユーザーがログインするための新しいセッションが作成できる
- flash.nowメソッドを使うと、描画済みのページにもフラッシュメッセージを表示できる
- テスト駆動開発は、回帰バグを防ぐときに便利
- sessionメソッドを使うと、ユーザーIDなどをブラウザに一時的に保存できる
- ログインの状態に応じて、ページ内で表示するリンクを切り替えることができる
- 統合テストでは、ルーティング、データベースの更新、レイアウトの変更が正しく行われているかを確認できる
あいも変わらずテストで理解が追いつかなかった。