- 投稿日:2019-10-04T23:28:29+09:00
railsでscopeがnilを返すとallが返る
railsのscope
- 文字通り、そのクラスの一部のインスタンスを返します
- 意外と知られてないですが、nilを返すとallが返ります
- クラスメソッドと全く同じ挙動ではない。classメソッドはnilを返せばnilが返る
scope
[1] pry(main)> class User < ApplicationRecord [1] pry(main)* scope :nil_scope, ->() { [1] pry(main)* nil [1] pry(main)* } [1] pry(main)* end => :nil_scope [2] pry(main)> User.nil_scope User Load (4.0ms) SELECT `users`.* FROM `users` => [#<User id: 1, email: "xxx@gmail.com", created_at: "2018-04-14 12:05:50", updated_at: "2019-09-05 01:01:24", provider: "facebook", uid: ...] pry(main)> User.nil_scope.size (0.9ms) SELECT COUNT(*) FROM `users` => 355class method
[1] pry(main)> class User < ApplicationRecord [1] pry(main)* def self.nil_scope [1] pry(main)* nil [1] pry(main)* end [1] pry(main)* end => :nil_scope [2] pry(main)> User.nil_scope => nil
- 投稿日:2019-10-04T22:51:12+09:00
turbolinksをfalseのページとtrueのページに分ける
こんばんは!
ポートフォリオ作成中にハマったエラーの簡単な解決方法を備忘録として。いいね機能をremote: trueで動かす
railsチュートリアルにて学習した
remote: true
を使ったajax通信。jsが苦手な私にぴったり!turbolinksが必要みたい
Uncaught ReferenceError: Turbolinks is not defined
application.js/=require turbolinks動きます。
次はこっちが動かないのね
<%= link_to "新規投稿", new_meal_path, class:'hoge'%>
に遷移してfile_field
に画像添付したらimageがpreview表示されなくなったリロードすれば正常に動作します
解決方法
index.html.erb<%= link_to "新規投稿", new_meal_path, class:'hoge',data: {"turbolinks"=>false} %>
data: {“turbolinks" => false}
は遷移先ページのみturbolinksを切ることが出来ますまとめ
jsファイルにajax通信を記述すればturbolinksがtrueでも問題ないみたいです
remote: true
を使ったajax通信とjsイベントを両方実装している時にどちらかのイベント発火しない場合があればdata: {"turbolinks"=>false}
が役立ちます終わり
- 投稿日:2019-10-04T20:33:47+09:00
[Rails]動画プレビュー機能
はじめに
今回以下の様な動画のプレビュー機能を追加したのでその方法について、説明します。
環境
Rails 5.2.3
Ruby 2.5.1導入方法
ビューファイル
気を付ける点は、
file_field
にid: "upload-image"
をjQueryを使用する為に記述します。new.html.haml= form_for @video do |f| 省略 .movie-group-form__field--right--mask = f.label :image, for: "upload-image", class: "movie-group-form__movie" do = fa_icon "cloud-upload fa-spin", class: "icon" = f.file_field :image, class: "hidden", id: "upload-image" = f.label :動画を選択してください , class: "movie-group-form__choice" = f.submit "投稿する", class: "btn btn-dark"jQuery
次にJSファイルに動画プレビューをさせる記述をします。
$fileField
にビューのid
に記述したupload-image
に#
を付けて記述します。
$preview
には動画をプレビュー表示させたい部分(今回は.movie-group-form__choice
)に記述します。
$preview.append($('<video>').attr({
内にCSSを記述してます。autoplay: "autoplay"
、loop: "loop"
で動画のプレビューをオート再生させています。
playsinline: "true"
はスマホで動画を投稿した際に、オート再生するのに使用します。preview.js$(document).on('turbolinks:load', function() { $fileField = $('#upload-image') $($fileField).on('change', $fileField, function(e) { file = e.target.files[0] reader = new FileReader(), $preview = $(".movie-group-form__choice"); reader.onload = (function(file) { return function(e) { $preview.empty(); $preview.append($('<video>').attr({ src: e.target.result, width: "45%", height: "110px", class: "preview-image", autoplay: "autoplay", loop: "loop", playsinline: "true", title: file.name })); }; })(file); reader.readAsDataURL(file); }); });今回はJS内にCSSを直接記述しているので、CSSファイルの変更は必要ないです。後は好みで調整して下さい。
まとめ
動画のプレビューも画像のプレビューとほぼ同じで、後はappendに
'<video>'
か'<img>'
を記述するだけで、動画か画像のプレビュー表示かを変更する事が出来ます。気になったら試してみて下さい。
- 投稿日:2019-10-04T20:09:26+09:00
Rails EngineのRootからRspecを実行すると、Webpackerのエラーが出たので対応する
状況
アプリケーションが複数のUIで構成されているので、以下のような対応をしていた。分離することで、Webpackのバージョンから何やら分けることができる。
Webpackerのインスタンスをクラスメソッドで定義して、別のところから呼び出せるようにする。
module Convenience ROOT_PATH = Pathname.new(File.join(__dir__, "..")) class << self def webpacker @webpacker ||= ::Webpacker::Instance.new( root_path: ROOT_PATH, config_path: ROOT_PATH.join("config/webpacker.yml") ) end end endヘルパーに以下を記載し、先程のインスタンスを呼ぶようにすれば、stylesheet_pack_tagやjavascript_pack_tagが呼びだせる。
module Convenience module ApplicationHelper include ::Webpacker::Helper def current_webpacker_instance Convenience.webpacker end end endちなみにこのメソッドは、カレントWebpackerインスタンスを返すので、先程のConvenienceの例だと、ConvenienceのWebpackerインスタンスを返すようにOverrideしてあるようだ。
module Webpacker::Helper # Returns current Webpacker instance. # Could be overridden to use multiple Webpacker # configurations within the same app (e.g. with engines) def current_webpacker_instance Webpacker.instance endRspecでエラー
ただ、EngineRootからrspecを実行するとエラーになるので、test環境だけ分岐する必要がある。もしかしたらもっといい方法があるのかもしれないが、一旦EngineRootから直でrspecを呼び出すという要件に合わせるためにコードを追記した。
module Convenience module ApplicationHelper include ::Webpacker::Helper unless Rails.env.test? def current_webpacker_instance Convenience.webpacker end end end endなお、コードは https://github.com/rails/webpacker/blob/master/docs/engines.md#using-in-rails-engines こちらからコピペしたものだと予想できる。
- 投稿日:2019-10-04T18:06:07+09:00
【Rails】インスタンスの状態を保ちながら入力 → 確認画面 → 保存 を実装する方法
実現したいこと
画面をまたいでフォームを入力する際、インスタンスの状態を保ちながら画面遷移したいときがあります。
例えばこんな感じで。① 各項目を入力して「確認へ」を押す
↓
② 確認画面で確認したら「送信」を押す
↓
③ 完了画面へリダイレクトさらに、バリデーションにひっかかったり、戻るボタンを押しても状態を維持したいですよね。
このように、画面をまたいでもインスタンスの状態を維持するやり方について簡単に解説していきます。実装手順
アクションにおける処理の流れは以下の通りです。
new(作成フォーム)
↓
confirm(確認画面)
↓
create(作成)
↓
show(完了画面)今回はフォームのバリデーション、戻るボタンを考慮するため、イメージはこんな感じです。
次のアクションへPOSTするたびに、
・invalid(無効)なら画面を変えない
・valid(有効)なら次の画面を表示する
・back(パラメータ)が渡ってきたら前の画面に戻る
という実装をすればいいわけです。(厳密にはアクションごとに違いますがイメージです)
具体的にコードで見ていきましょう。Route
config/routes.rbRails.application.routes.draw do resources :users, only: [:new, :create, :show] do collection do post :confirm end end end
new
→create
の流れは今まで通りですが、やはりポイントはconfirm
を追加したことであり、new
→confirm
→create
と挟みこむ感じになります。フォームでは
new
→confirm
にPOSTするイメージで捉えてください.Model
app/models/user.rb# Table name: users # # id :integer not null, primary key # first_name :string # last_name :string # email :string # created_at :datetime not null # updated_at :datetime not null # class User < ApplicationRecord with_options presence: true do validates :first_name validates :last_name validates :email end end今回は便宜上、空のバリデーションのみ設定しました。
Controller
app/controllers/users_controller.rbclass UsersController < ApplicationController def new @user = User.new end def confirm @user = User.new(user_params) render :new if @user.invalid? end def create @user = User.new(user_params) render :new and return if params[:back] || !@user.save redirect_to @user end def show @user = User.find_by(id: params[:id]) end private def user_params params.require(:user).permit(:first_name, :last_name, :email) end end一番のポイントは、アクションを移動する度にインスタンス情報の入ったパラメーターを渡しているところです。
ここでいうUser.new(user_params)
のとこですね。ここに全てのインスタンス情報が入っているので、確認画面へ橋渡しすれば良いわけです。そして戻るボタンを押した時の挙動も、
params[:back]
で制御しています。back
が渡ってきたら前の画面にrender
すればOK。View
※ ここでは
slim
,bootstrap
,simple_form
を使ってます。
slimに関してよくわからない方は、導入から文法までまとめたので、よかったらこちらを参考にしてみてください。new
app/views/users/new.html.slimh2 ユーザー新規登録 = simple_form_for @user, url: confirm_users_path(@user) do |f| = f.input :first_name = f.input :last_name = f.input :email = f.submit "確認画面へ", class: "btn btn-primary"ポイントは, URL指定で
confirm_users_path
へリクエストを投げていることです。
有効であればconfirm
アクションへ移動して次の確認画面ではUser.new(user_params)
が表示されるので状態が維持されます。def confirm @user = User.new(user_params) render :new if @user.invalid? endconfirm
app/views/users/confirm.html.slimh2 以下の詳細を確認してください = render "detail", user: @user = simple_form_for @user do |f| = f.input :first_name, as: :hidden = f.input :last_name, as: :hidden = f.input :email, as: :hidden = f.submit "送信", class: "btn btn-primary" = f.submit "戻る", name: :back, class: "btn btn-secondary"次は
confirm
からcreate
にPOSTします。
前回のnew
アクションから渡ってきたuser_params
を参照してインスタンスを表示しているため、空にならずにちゃんと表示され、フォームのvalue
にもちゃんと入力されてるのが確認できました。
フォームが見えても邪魔なので、input type
はhidden
にしておくと良いです。戻るボタンは
name
でパラメータを指定して、アクション側で存在すれば前の画面にrender
してあげます。= f.submit "戻る", name: :back, class: "btn btn-secondary"`def create @user = User.new(user_params) render :new and return if params[:back] || !@user.save redirect_to @user endあとはいつも通り、作成に成功したらリダイレクトしてあげれば完了です。
app/views/users/show.html.slimh2 ユーザーを作成しました = render "detail", user: @userapp/views/users/_detail.html.slimul li | First name: #{user.first_name} li | Last name: #{user.last_name} li | Email: #{user.email}さらに入力画面を増やしたい場合
先ほどの例に加えて、次は性別, 年齢, 電話番号, 住所を入力する画面を加えたいとしましょう。
完成デモ↓
このように、どこでどんなリクエストを送ろうと、インスタンスの状態を維持できることがゴールです。実装手順
今回は
next
アクション(新しい画面)を追加し、アクションの流れは以下のようにします。
new → next(追加) → confirm → create
Controller
側のロジックは以下をイメージしてみてください。
Route
config/routes.rbRails.application.routes.draw do resources :users, only: [:new, :create, :show] do collection do post :next #追加 post :confirm end end end入力画面が一つ増えたので、
next
を追加してあげましょう。
先ほどと同様、new
アクションからnext
アクションへPOSTしてあげるイメージです。Model
app/models/user.rb# Table name: users # # id :integer not null, primary key # first_name :string # last_name :string # email :string # gender :integer # age :integer # phone_number :integer # address :string # created_at :datetime not null # updated_at :datetime not null # class User < ApplicationRecord with_options presence: true do validates :first_name validates :last_name validates :email end #追加 with_options on: :confirm do validates_presence_of :gender validates_presence_of :age, presence: true validates_presence_of :phone_number, presence: true validates_presence_of :address, presence: true end enum gender: { man: 0, woman: 1, gay: 2, bisexual: 3, transgender: 4 } end
on: :confirm
を記述することで、invalid?(context: :confirm)
とした時のみ、カラムの有無を検証できるようにしています。
これは最初のnew
画面のみ、次画面のカラムのバリデーションをスキップするためです。Controller
app/controllers/users_controller.rbclass UsersController < ApplicationController def new @user = User.new end # 追加 def next @user = User.new(user_params) render :new if @user.invalid? end # 変更 def confirm @user = User.new(user_params) render :new and return if params[:back] render :next if @user.invalid?(:confirm) end # 変更 def create @user = User.new(user_params) render :next and return if params[:back] render :confirm and return if !@user.save redirect_to @user end def show @user = User.find_by(id: params[:id]) end private # 変更 def user_params params.require(:user).permit(:first_name, :last_name, :email, :age, :gender, :phone_number, :address) end endカラムとアクションが増えただけで、やってること自体は最初に実装したものとほとんど同じなのが分かると思います。
基本的には、渡ってくるパラメーターの状態によって画面遷移を条件分岐しています。View
new
app/views/users/new.html.slimh2 ユーザー新規登録(1) = simple_form_for @user, url: next_users_path(@user) do |f| = f.input :first_name = f.input :last_name = f.input :email = f.input :age, as: :hidden = f.input :gender, as: :hidden = f.input :phone_number, as: :hidden = f.input :address, as: :hidden = f.submit "次へ", class: "btn btn-primary"
new
→next
へPOSTするために、URLを直接指定するようにしましょう。ここでのポイントは、状態管理したい全カラムを送信することです。
これを書くことによって、入力→戻る→次へとしたときにも、入力した値が常に維持されます。
例えば、next
で入力→new
に戻る→next
へ遷移としたときに、next
で入力した値が維持されます。
これを各アクションでも適応してあげればユーザービリティは向上するでしょう。next
app/views/users/next.html.slimh2 ユーザー新規登録(2) = simple_form_for @user, url: confirm_users_path(@user) do |f| = f.input :first_name, as: :hidden = f.input :last_name, as: :hidden = f.input :email, as: :hidden = f.input :age = f.input :gender, collection: User.genders.keys = f.input :phone_number, as: :tel = f.input :address = f.submit "確認画面へ", class: "btn btn-primary" = f.submit "戻る", name: :back, class: "btn btn-secondary"これも先ほど同様、
next
→confirm
へPOSTするため、URLを直指定しています。confirm
app/views/users/confirm.html.slimh2 以下の詳細を確認してください = render "detail", user: @user = simple_form_for @user do |f| = f.input :first_name, as: :hidden = f.input :last_name, as: :hidden = f.input :email, as: :hidden = f.input :age, as: :hidden = f.input :gender, as: :hidden = f.input :phone_number, as: :hidden = f.input :address, as: :hidden = f.submit "次へ", class: "btn btn-primary" = f.submit "戻る", name: :back, class: "btn btn-secondary"最後はバケツリレーで維持してきたインスタンスから
value
を送信してあげればOK。app/views/users/_detail.html.slimul li | First name: #{user.first_name} li | Last name: #{user.last_name} li | Email: #{user.email} li | Age: #{user.age} li | Age: #{user.gender} li | PhoneNumber: #{user.phone_number} li | Address: #{user.address}お疲れ様でした。
まとめ
アクションをまたいでインスタンスの状態を維持するにあたり、色々考えることが多かったです。
初めのころはlink_to
から長ったらしいインスタンスのパラメーターを送ったり、session
で状態を管理したりと割と強引なやり方で実装していました。しかし今回紹介した、渡ってきたパラメーターからインスタンスを生成し、バケツリレー式に状態を管理するというやり方であれば、比較的簡単に実装できることに気づきました。
長いフォーム画面を分割したいときや、決済フォームを挟んだりするときに使える手法なので、ぜひ参考にしてみてください。
参考記事
https://kossy-web-engineer.hatenablog.com/entry/2018/10/19/063937
https://remonote.jp/rails-confirm-form
- 投稿日:2019-10-04T18:02:46+09:00
Rails エラーメッセージの出し方 バリデーションエラー
はじめに
DBに値を保存する際(ユーザー登録やツイートの登録等)のバリデーションエラー文を表示するやり方について備忘録のために記載しておきます。
参考程度にどうぞ。バリデーションとは?
バリデーションとはDBに値を保存する際に何か値が抜けていると保存できなくするものです。
簡単に言えばフィルターみたいなものですね。例えばユーザー登録時にpasswordを入力してもらうとします。passwordが「111」とかだったらセキュリティーが甘くなりますね。
そこでバリデーションを記載してpasswordを8文字以上にしないと登録できないようにすれば、「111」と入力しても弾かれて保存できないようになります。
バリデーションの組み方は無数にあり、やり方を変えれば自分の思い通りに値を保存することができます。例1)
モデルに下記のように記載すれば空では保存できません。product.rbclass User < ApplicationRecord validates :title, presence: true end例2)
モデルに下記のように記載すれば漢字のみ保存できるようになります。product.rbclass User < ApplicationRecord kanji = /\A[一-龥]+\z/ validates :name, format: { with: kanji } end例3)
モデルに下記のように記載すれば"2019-10-04"のような形でのみ保存できるようになります。product.rbclass User < ApplicationRecord year_month_day = /\A\d{4}-\d{2}-\d{2}\z/ validates :birthday, presence: true, format: { with: year_month_day } endバリデーションエラーメッセージの出し方
①商品の保存を行うアクション(ここではcreate)に保存の成功/失敗の条件分岐を記載。
ここで気をつけて欲しいのは失敗時のviewの読み込みはrenderを使用しましょう。
理由は後で解説します。products_controller.rbdef create @product = Product.new(product_params) if @product.save redirect_to controller: :products, action: :index else render "new" end end②エラーメッセージのview作成
htmlのif文をmodel.errors.any? にしておくことでどのモデルのバリデーションにも対応させることができます。layouts/_error_messages.html.haml-if model.errors.any? .alert %ul -model.errors.full_messages.each do |message| %li= messagelayouts/error_messages.scss.alert { olor:#262626; background:#FFEBE8; text-align: center; border:2px solid #990000; padding:12px; font-weight:850; }③エラーメッセージの表示
あとはエラーメッセージを表示させたいところに以下のコードを記載するだけです。products/new.html.haml= render 'layouts/error_messages', model: f.object完成です。
こんな感じになります。めちゃめちゃ簡単です。redirect_toとrenderの違い
先ほど、失敗時のviewの読み込みはrenderを使用すると記載しましたが、そこについて軽く触れます。
redirect_toとrenderはルーティングを通るか通らないかという違いがあります。
redirect_to…ルーティングを通り、新たにviewページを呼び出す。
render…ルーティングを通らず、viewページに飛ぶ。
正式にはもっと違いがありますが、とりあえずこの認識でOKです。なので失敗時のviewの読み込み(以下の部分)をredirect_toにすると、
新しいビューページが呼ばれてしまうため、ページに書いたif文が反応しません。products_controller.rbdef create ~省略~ else render "new" end endおわりに
特定のgem(例えばdeviseとか)には初めから実装されていることがあります。
ぜひ調べてみてください
- 投稿日:2019-10-04T17:42:41+09:00
imagemagickを使って画像をアップロードするとSSLが外れてしまう時の対処法
minimagickのgemを使って画像をアップロードすると、その画像が表示されるページのSSLマークが外れるという問題が発生したので、その時の解決方法を残します。
このサイトで目にする画像は、悪意のあるユーザーによって差し替えられたものである可能性があります
事象
・minimagickのgemを使って画像をアップロードすると、その画像が表示されるページでSSLマーク(鍵マーク)が外れ、びっくりマークに変わる
・びっくりマークをクリックすると「このサイトで目にする画像は、悪意のあるユーザーによって差し替えられたものである可能性があります」のメッセージがでる。解決方法
nginxのconfファイルにproxy_set_header X-Forwarded-Proto https;を追記
/etc/nginx/conf.d/hoge.conflocation @app { proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Server $hostname; proxy_set_header X-Forwarded-Proto https; #この一行を追加 proxy_set_header X-Real-IP $remote_addr; proxy_redirect off; proxy_pass http://app_server; }Nginx再起動
systemctl restart nginx解決までの流れ
①画像ファイルへのリンクの確認
この事象はサイト内で使用している画像に、httpアクセスで記述している箇所があると発生するらしいので、デベロッパーツールで画像リンクのプロトコルを確認。
参考:https://stackdesign.jp/how-to-create-ssl-wordpress-site/→きちんとhttpsになってた。
②Railsの環境設定を変更
production.rbのconfig.force_ssl = trueを有効にすると、本番環境でSSL通信を強制できるので、コメントアウトを外し設定を有効にして再デプロイ。
参考:https://railsguides.jp/configuring.html/config/environments/production.rbconfig.force_ssl = true→ページにアクセスできなくなる
メッセージ
・このページは動作していません
・リダイレクトが繰り返し行われました。→再びコメントアウト
③nginxのconfファイル修正
HTTPSアクセスであることをX-Forwarded-ProtoでRailsへ通知することでリダイレクトのループを回避する。
参考:https://keruuweb.com/rails-nginx-https%E3%81%A7%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%82%92%E3%81%99%E3%82%8B//etc/nginx/conf.d/hoge.conflocation @app { proxy_set_header X-Forwarded-Proto https; #この一行を追加 }→無事に全ページでSSL化が有効になりました
まとめ
「proxy_set_header X-Forwarded-Proto https;」はリダイレクトのループを回避するための設定で、本来はSSL化をする際に設定しておく必要があったらしい。
SSL化の際にきちんと設定ができていたら、そもそも今回のエラーは生じなかったってことですね、、、
- 投稿日:2019-10-04T16:14:41+09:00
FactoryBotのすごくいいチートシートを発見!
すごくいい〜??
- 投稿日:2019-10-04T15:50:48+09:00
[初学者]findメソッドについて(ruby)
目的
学習の備忘録と初学者の参考資料として投稿
findメソッド
該当するモデルのテーブルからデータを検索する際に使われるメソッド。
例えば・・・
userテーブル
id name age gender 1 taro 12 male 2 hanako 11 female 3 jiro 10 male 4 keiko 9 female 検索して表示させたい→
id name age gender ○○○ ○○○ ○○○ ○○○ users_controller.rbdef show @user = User.find(user:id) idを引数として検索する end *viewは省略findは検索条件として指定できるものは「idのみ」です。データを複数取得できます。
findとfind_byの違い
find_byは、複数の条件を指定したりid以外のカラムでも検索できます。データは条件があった中で最初の一つのみ取得できる。
find_by(gender:"male") 上の例から、こんな感じで検索する。まとめ
テーブルからデータを表示や更新や削除する場合によく使うメソッドです。他にもwhereというメソッドもあります。
今後も学習で気づきや参考になるものがあれば、アップしていきます。
もし参考になったらいいね!!よろしくお願いします
- 投稿日:2019-10-04T15:17:07+09:00
Railsエラーメッセージのフォーマットを変更したい
発端
- Nameを入力してください。
- Emailを入力しください。
- Passwordを入力してください。
Railsでは上記のように、エラー文の先頭に必ずモデルの属性名が表示されてしまいます。
(今回は、属性名の日本語化が本題ではありません。)
しかし、先頭にモデルの属性名をつけたくない場合やエラーメッセージをカスタマイズしたい場合、このままでは不都合です。解決策
config/locales/ja.ymlja: errors: format: "%{message}"上記を追記してください。
デフォルトの属性名が、"'%{attribute}' %{message}"
になっているため、フォーマットをオーバーライドすれば良いようです。
様々、カスタマイズすることも可能ですね!参考
- 投稿日:2019-10-04T15:14:02+09:00
【Rails】jQueryでフォームに文字数カウントをつける
フォームに文字を入力していく時、あと何文字まで打てるのか分かった方がユーザーに優しいと思ったため、文字数カウント機能を作りました。
▷今回やること
フォームに残り何文字入力できるかを表示する文字数カウント方法を考える
・文字を入力したり、消したりするたびフォームの文字数を数える → 残り何文字まで入力できるか表示する
・文字数がオーバーしていた場合、文字色を赤にする → ユーザーに警告する文字数カウントのアルゴリズムを考える
jQueryで文字数カウントをする
▷jsが読み込まれたら以下の処理をする ・処理内容(ページが読み込まれた時、フォームに残り何文字入力できるかを数えて表示する) フォームのvalueを取得して文字数を数える <!-- Macの改行コードは1文字(\n)であるが、Rails側では改行コードが2文字(\r\n)で保存されるため、\nは2文字とカウントするように設定 --> 最大文字数 ー 取得した文字数 (=残りの入力できる文字数) if 最大文字数より取得文字数の方が大きい 表示する文字色を赤にする “あと (残りの入力できる文字数)文字” と表示する ▷jQueryのkeyup()でキーボードが押されて離れるたびに処理を行う ・処理内容(キーボードを押した時、フォームに残り何文字入力できるかを数えて表示する) フォームのvalueを取得して文字数を数える。 <!-- Macの改行コードは1文字(\n)であるが、Rails側では改行コードが2文字(\r\n)で保存されるため、\nは2文字とカウントするように設定 --> 入力最大文字数 ー 取得した文字数 (=残りの入力できる文字数) if 最大文字数より取得文字数の方が大きい 表示する文字色を赤にする “あと (残りの入力できる文字数)文字” と表示する else 表示する文字色を黒にする ”あと (残りの入力できる文字数)文字” と表示する 処理終了 処理終了コードで文字数カウント処理を書く
1.htmlでフォーム入力タグと文字数表示タグにclassを設定し、jsで指定できるようにする
フォーム入力タグtext_areaのクラスに
js-text
、文字数表示タグpのクラスにjs-text-count
を指定する
(フォームにbootstrapを適用しています)form.html.erb<%= form_for(@profile) do |f| %> <div class="panel-body"> <div class="form-group"> <%= f.label :yourself, '自己紹介(最大150文字)' %> <%# js-textをclassに追加 %> <%= f.text_area :yourself, class: 'form-control js-text', rows: 6, placeholder: '改行は反映されません' %> <%# js-text-count をclassに追加 %> <P class="js-text-count pull-right"></P> </div> <div class="text-center"> <%= f.submit '保存する', class: 'btn btn-info'%> </div> <% end %>2.jsファイルにフォームと文字数表示に対する処理を書く
js-text
のvalueを数えて、js-text-count
に結果を表示する
app/assets/javascripにcount.jsファイルを作成し、文字数カウントの処理を書いていく
(今回のフォームの最大文字数は150)app/assets/javascrip/count.js// jquery書きはじめ $(function (){ // 処理(ページが読み込まれた時、フォームに残り何文字入力できるかを数えて表示する) //フォームに入力されている文字数を数える //\nは"改行"に変換して2文字にする。オプションフラグgで文字列の最後まで\nを探し変換する var count = $(".js-text").text().replace(/\n/g, "改行").length; //残りの入力できる文字数を計算 var now_count = 150 - count; //文字数がオーバーしていたら文字色を赤にする if (count > 150) { $(".js-text-count").css("color","red"); } //残りの入力できる文字数を表示 $(".js-text-count").text( "残り" + now_count + "文字"); $(".js-text").on("keyup", function() { // 処理(キーボードを押した時、フォームに残り何文字入力できるかを数えて表示する) //フォームのvalueの文字数を数える var count = $(this).val().replace(/\n/g, "改行").length; var now_count = 150 - count; if (count > 150) { $(".js-text-count").css("color","red"); } else { $(".js-text-count").css("color","black"); } $(".js-text-count").text( "残り" + now_count + "文字"); }); });3.リンクをクリックしてページ遷移した時、jacascriptを読み込むようにする
現在のままでは、ページ遷移してもjsが機能しない
▷原因
rails4.0からデフォルトで導入されたgem「turbolinks」が読み込まれているため〜turbolinksとは〜
画面遷移を高速化させるライブラリ。
ページ遷移するとき、現在のページのtitleとbodyだけを抜き取り、
新しいhtmlのtitleとbodyに交換することで、ページ遷移を高速化させる。
→ページ読み込みを起点としたJavascriptは機能しなくなる
jQuery を使用している場合、 下記の様にページが読み込まれたときの処理を記述出来ますが、 Turbolinksによって、実行されなくなります。
$(function() {
#処理内容
});▷解決方法
count.jsを読み込みたいリンクのtubolinksを無効にする
aタグはdata-turbolinks="false"
、link_toメソッドはdata: {"turbolinks" => false}
でtubolinksを無効化できるリンク設定がaタグの場合
<a href=“◯◯” data-turbolinks="false">turbolinksを使わずにリンクする</a>リンク設定がlink_toメソッドの場合
<%= link_to "turbolinksを使わずにリンクする", ◯◯_path, data: {"turbolinks" => false} %>これでページ遷移した時、count.jsが読み込まれるようになり、文字数カウント処理が行われる。
参考
・[簡単すぎる]Rails + jQueryで文字数カウント
・【JavaScript】文字列から改行を全て削除する方法
・turbolinksチートシート
・『Turbolinks』って、なんぞ?
- 投稿日:2019-10-04T15:09:14+09:00
ブログ投稿サイトを作ってみました
こんにちは。
本日は初投稿ということで、プログラミングを始めて2週間少しの初心者が0から作った作品を紹介します。
ーーーーーーーーブログ投稿サイト
トップ画面
ユーザー新規登録、ログイン画面
投稿画面
詳細確認画面
(写真引用元:https://www.travel.co.jp/guide/article/24996/)1. 実装機能
・ユーザ登録、ログイン、ログアウト可能
・投稿はタイトル、本文、画像のみ(非ログイン時は投稿不可)
・投稿したユーザ本人による投稿の編集、削除が可能
・ログインしていなくても投稿一覧、詳細が閲覧可能
・ページネーション機能(投稿は新着順)2. 使用言語・ツール
・Ruby
・Haml/Sass
・JQuery
・Ruby on Rails
・Bootstrap3. 工夫した点
bootstrapというwebアプリケーションフレームワークを使用し、
ボタンやページネーション等を簡単に作成、装飾してみました。
フォームなどにも適用させて、もっと簡単に作成するのが理想でしたが、
公式サイトはもちろん英語だったのでなかなか上手く使えなかったのが少し残念なところです。。。
あとは綺麗な画像を貼ることで見栄えを良くしました!笑4. 苦労した点
devise gem をインストール後、userテーブルをmigrateする際にエラーが発生し、
解決に時間が掛かりました。
エラー内容は「max key length is 767 bytes(768バイト以上のカラムに対するインデックスキーは設定できないよ)」と言った内容。
調べてみると、MySQLの単一カラムインデックスのインデックスキーは、デフォルトで
最大 767 バイトまでとのことでした...。
マイグレーションファイルに於いて、string 型の容量にリミットをつけてあげることで解決出来ました。
ex) t.string :reset_password_token, limit: 1915. 感想
制作時間の半分以上がエラーとの奮闘でしたが、解決した時にはこの上ない感動でした。
普段使っているウェブサイト、アプリ、何から何までこんなに苦労しているんだなと感じることができました。
「こんな機能があるといいな」「こんな風に配置したいな」と、色々とこだわりを入れたかったのですが、そこは自分の今の技術的に厳しめだったので断念。
これからも毎日勉強して、良いものを作ろうと思いました!
- 投稿日:2019-10-04T14:24:41+09:00
日時の扱い方
日時の扱い方
Rubyには日付や時刻を扱うためのDateやTimeといったクラスがあります。そのため、Rialsアプリケーションで日付や時刻を扱う際にもただこれらのクラスを使えばいいと思われるかもしれませんが、ひとつ課題があります。それは、タイムゾーンです。
例えば、Railsアプリケーションがデータベース内に、ある日時を保存するとしましょう。その日時を東京に住むユーザーが見たときは東京時間、シンガポールに住む人が見たときはシンガポール時間で表示した方が良いでしょう。このように、地域の時間に合わせた日時を表示する必要があるということです。そのため、Railsでは日時のデータをタイムゾーンとともに取り扱うことができるようになっている。
タイムゾーンとともに日時を扱うには、Timeの代わりにActiveSupport::TimeWithZoneクラスを用います。Railsが日時を扱う際(日時型のデータをデータベースから読むときや、created_atなどのタイムスタンプを記録する際)には、自動的にこのクラスが利用されます。
それでは、Railsコンソールを立ち上げて挙動を確認してみましょう。(※Rialsアプリケーションのコンソールで動作します。)
まず、現在有効なタイムゾーンを確認してみましょう。Time.zoneと売ってみてください。何も手を加えていないRailsアプリケーションでは次のように、"UTC"という名前を持つActiveSupport::TimeZoneオブジェクトが返されるはずです。これは、UTC(協定世界時)タイムゾーンを意味しています。
2.6.0 :013 > Time.zone => #<ActiveSupport::TimeZone:0x000000000a8e9568 @name="UTC", @utc_offset=nil, @tzinfo=#<TZInfo::DataTimezone: Etc/UTC>>続いて、そのUTCタイムゾーンにおける現在時刻を表すオブジェクトを取得しましょう。それには、Time.zone.nowを実行します。
2.6.0 :003 > Time.zone.now => Fri, 04 Oct 2019 13:52:47 JST +09:00Time.zone.nowは、ActiveSupport::TimeWithZoneクラスのオブジェクトとなっています。試しに、classメソッドを調べてみましょう。
2.6.0 :004 > Time.zone.now.class => ActiveSupport::TimeWithZone確かにそのようになっていますね。
オブジェクトの作成日時を表すreated_atなどもActiveSupport::TImeWithZoneのオブジェクトとなっています。
タイムゾーンの変更
タイムゾーンは「現在のタイムゾーン」に当たります。データベースからモデルオブジェクトを取ってくると、オブジェクト内のcreated_atやupdated_atは、Time.zoneで示されたタイムゾーンに対応する時間オブジェクトとなります。
したがって、もしもモデルのcreated_atなどを日本時間での時間オブジェクトとして取得したいのであれば、Time.zoneを日本時間に指定してからオブジェクトを取得すれば良いということになります。試しに、Time.zoneに日本時間を指定してみましょう。次のように、Time.zoneにAsia/Tokyoを代入してみてください。
2.6.0 :009 > Time.zone = 'Asia/Tokyo' => "Asia/Tokyo"これで設定が変わるので、Time.zone.nowを実行すると、今度は日本時間での現在時刻が取得できます。
2.6.0 :010 > Time.zone => #<ActiveSupport::TimeZone:0x000000000a8d9e10 @name="Asia/Tokyo", @utc_offset=nil, @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>> 2.6.0 :011 > Time.zone.now => Fri, 04 Oct 2019 14:19:30 JST +09:00同様に、クラスのcreated_atも、日本時間のオブジェクトを取得できるようになりました。
2.6.0 :018 > User.first.created_at => Fri, 04 Oct 2019 02:14:57 UTC +00:00
- 投稿日:2019-10-04T14:08:46+09:00
【Rails】日付期間の重複チェック
発端
「新規で作成する期間が作成済みの期間と被らないようにバリデーションつけといて〜」
こんな頼みが上司からあり、すぐ実装できるだろうと思ったのですが・・・
なかなか苦戦したので、その備忘録です。最初に苦戦した点を話すと、
作成済みの開始〜終了の間の期間に新規作成すると検索に引っかからない点です。その解決策と、Railsでの実装方法について書いていきます。
試したこと①
今回は、例として予約の期間が被らないようにする、Reservationモデルで試します。
最初は、効率がよくないですが、こちらで試しました。Reservation.where(started_at: self.started_at..self.finished_at) or Reservation.where(finished_at: self.started_at..self.finished_at)試したこと②
次に、こちらなどを参考に、下記を実装しました。
Reservation.where('started_at <= ? and finished_at >= ?', self.started_at, self.finished_at)しかし、問題点が・・・
しかし、上記2つの方法では、問題点が浮かび上がりました。
対象期間 2019/08/05 ~ 2019/10/15
期間① 2019/07/01 ~ 2019/07/30
期間② 2019/11/01 ~ 2019/11/30
期間③ 2019/07/30 ~ 2019/08/15
期間④ 2019/09/15 ~ 2019/10/30
期間⑤ 2019/09/01 ~ 2019/09/30①〜②は対象外のため引っかからない、③〜④は正常に引っかかるのですが、
問題は、⑤が引っかからないのです。
仕様にもよるかもしれませんが、期間が完全に重複しているのに、検索で引っかかってくれないのは大問題です!解決策
調べたところ、解決策がありました!
期間が重複しているかを判定する条件式の導出方法がとても参考になりました。qiita.rbputs 'code with syntax'条件式(対象終了日付 > 比較開始日付) AND (比較終了日付 > 対象開始日付)これをActiveRecordで書き直すと、
Reservation.where('finished_at > ? and ? > started_at', self.started_at, self.finished_at)これで既存の開始〜終了の間の期間に、作ろうとした新規予約を弾くことができるようになりました!
感想
アルゴリズムの大切さを実感しました。
まず該当しない場合を考え、逆にして該当する場合を導き出すド・モルガンの法則は、今後も使う場面がありそうです!参考
以下の記事を参考にさせていただきました。ありがとうございました。
- 投稿日:2019-10-04T14:08:46+09:00
【Rails】日付期間の重複を完璧にチェックしたい
発端
「新規で作成する期間が作成済みの期間と被らないようにバリデーションつけといて〜」
こんな頼みが上司からあり、すぐ実装できるだろうと思ったのですが・・・
なかなか苦戦したので、その備忘録です。最初に苦戦した点を話すと、
作成済みの開始〜終了の間の期間に新規作成すると検索に引っかからない点です。その解決策と、Railsでの実装方法について書いていきます。
試したこと
今回は、例として予約の期間が被らないようにする、Reservationモデルで試します。
最初は、効率がよくないですが、こちらで試しました。Reservation.where(started_at: self.started_at..self.finished_at) or Reservation.where(finished_at: self.started_at..self.finished_at)しかし、問題点が・・・
しかし、上記の方法では、問題点が浮かび上がりました。
対象期間 2019/08/05 ~ 2019/10/15
期間① 2019/07/01 ~ 2019/07/30
期間② 2019/11/01 ~ 2019/11/30
期間③ 2019/07/30 ~ 2019/08/15
期間④ 2019/09/15 ~ 2019/10/30
期間⑤ 2019/09/01 ~ 2019/09/30①〜②は対象外のため引っかからない、③〜④は正常に引っかかるのですが、
問題は、⑤が引っかからないのです。
仕様にもよるかもしれませんが、期間が完全に重複しているのに、検索で引っかかってくれないのは大問題です!解決策
調べたところ、解決策がありました!
期間が重複しているかを判定する条件式の導出方法がとても参考になりました。qiita.rbputs 'code with syntax'条件式(対象終了日付 > 比較開始日付) AND (比較終了日付 > 対象開始日付)これをActiveRecordで書き直すと、
Reservation.where('finished_at > ? and ? > started_at', self.started_at, self.finished_at)これで既存の開始〜終了の間の期間に、作ろうとした新規予約を弾くことができるようになりました!
感想
アルゴリズムの大切さを実感しました。
まず該当しない場合を考え、逆にして該当する場合を導き出すド・モルガンの法則は、今後も使う場面がありそうです!参考
以下の記事を参考にさせていただきました。ありがとうございました。
- 投稿日:2019-10-04T13:22:13+09:00
RailsアプリにRSpecを導入する手順
はじめに
RSpecはRubyのテストフレームワークです。これ以外にMinitestというものがありますが、実際の現場では、RSpecが使われることが多いようなので導入する手順をメモしておきます。
環境
Widows 10
cloud9RSpecをインストール
gemを追加
最近はsystemスペックの利用が推奨されています。systemスペックは、バージョンが3.8.0以上である必要があるためそれをインストールしましょう。Gemfilegroup :development, :test do gem 'rspec-rails' # バージョン指定する必要があるかも'~> 3.8.0' end$ bundle installRSpecをインストールする
$ rails generate rspec:install必須ではないが、RSpec の出力をデフォルトの形式から読みやすいドキュメント形式に変更できる。やっといたほうが見た目が良くなるのでおすすめです。
.rspec--require spec_helper --format documentationテストを速くする
このgemを追加するとSpringにより、テストスイートが速くなる。必要だと思ったら入れてください。Gemfilegroup :development do gem 'spring-commands-rspec' end # bundle installを忘れずに!そしたら、以下のコマンドを実行して下さい。
$ bundle exec spring binstub rspecそしてこれからは、RSpecのコマンドを打つ際先頭に、bin/をつけるようにしましょう。
ジェネレータ
rails g する際に一緒にスペックファイルも生成されるようにする。また、あまり使うことのないファイルは生成されないようにします。コードが複雑になってきたら使うかもしれないので、その時は削除していってください。config/application.rb. . module Sample class Application < Rails::Application config.generators do |g| g.test_framework :rspec, fixtures: false, # factory_bot_railsを使うなら削除してください view_specs: false, helper_specs: false, routing_specs: false end end endこれで基本的なセットアップは終了です!
便利なgem
ここからはRSpecを使っていく際に便利なgemを紹介します。最新のものを使うようにしましょう。
Gemfilegroup :development, :test do gem 'factory_bot_rails' # テストのサンプルデータを生成できる end group :test do gem 'capybara' #統合テスト(system)をするのに必要 gem 'launchy' # save_and_open_page をスペック内で呼びだしたときに、HTMLを自動的に開く gem 'webdrivers' # js付きののテストができるようになる。 endCapybaraのセットアップ
Capybaraを読み込めるように、requireする。spec/rails_helper.rb# その他の設定 require 'capybara/rspec'Capybaraで、JavaScriptを使ったテスト
1 設定ファイルを書く
rails_helper.rbに直接書いてもいいが、今後設定が多くなってくると見にくくなるので、独立したファイルに設定を書いていきます。spec/rails_helper.rbDir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } #このコメントアウトを外す。これで、supportディレクトリ下にあるファイルを読み込めるようになる。このディレクトリを作り、そこにcapybara.rbというファイルを生成し、設定を書きます。
spec/support/capybara.rbCapybara.javascript_driver = :selenium_chrome or Capybara.javascript_driver = :selenium_chrome_headless # ヘッドレスモードを使いたい場合2 gemを追加
webdriversは、selenium-webdriver も一緒にインストールしてくれるので、selenium-webdriver がある場合は削除しといてください。Gemfilegem 'webdrivers'これで、js: trueオプションをつけることで、JavaScript付きのテストができる。
cloud9でヘッドレスモードを使えるようにする
僕は今、cloud9を使っているので、ここで使えるようにしていきます。cloud9では、ヘッドレスモードしかできないみたいです。1 Chromeブラウザをインストール
初期のAWSは、Chromeがインストールされてないのでインストールする。$ curl https://intoli.com/install-google-chrome.sh | bash2 ヘッドレスモードにする
spec/support/capybara.rbCapybara.javascript_driver = :selenium_chrome_headless3 selenium-webdriverとcapybaraを最新版にする
$ bundle update selenium-webdriver capybaraDeviseのヘルパーメソッドを使えるようにする
これを記述することで、コントローラー、システム、リクエストスペックでDeviseのヘルパーメソッドである、sign_inが使えるようになる。
また、リクエストスペックの場合、Deviseのヘルパーメソッド使うために、少し設定が必要。spec/rails_helper.rb# その他の設定 RSpec.configure do |config| # その他の設定 config.include Devise::Test::ControllerHelpers, type: :controller config.include Devise::Test::IntegrationHelpers, type: :system config.include RequestSpecHelper, type: :request endリクエストスペックの追加設定
新しいファイルを作って設定を記述。spec/support/request_spec_helper.rbmodule RequestSpecHelper include Warden::Test::Helpers def self.included(base) base.before(:each) { Warden.test_mode! } base.after(:each) { Warden.test_reset! } end def sign_in(resource) login_as(resource, scope: warden_scope(resource)) end def sign_out(resource) logout(warden_scope(resource)) end private def warden_scope(resource) resource.class.name.underscore.to_sym end end
- 投稿日:2019-10-04T13:04:15+09:00
[初学者]よく起こるエラー〜ActiveRecord::PendingMigrationError 〜
目的
学習の備忘録と初学者の参考資料として投稿
ActiveRecord::PendingMigrationErrorというエラー
MVCの一角であるモデルを作成するため、ターミナルで・・・・
<ターミナル> $ rails g model ○○○○(モデル名) ・・・ちょっくら確認してみるか $ rails sエラー発生!!!!
ActiveRecord::PendingMigrationError〜、あちゃ〜
忘れてた
解決方法
<ターミナル> $ rails db:migrate>>解決!!
そもそもマイグレーションとは
SQLを書くことなくRubyでデータベース内にテーブルを作成することができる機能。わざわざ面倒くさい事をマイグレーションファイルが行ってくれている。なので絶対忘れずにマイグレーションしよう!!
[参考]ちなみによく見る『schema.rb』とは
マイグレーションした際に作成されるファイルで、テーブルのカラムやそのデータ型などマイグレーションした結果が書かれている。マイグレーションするたびに最新の状態へ更新。実際ファイルを見てみるといままでのマイグレーションの履歴が見れる。
まとめ
モデルを作成したら、すぐにマイグレーション実行しよう!!
ちなみにマイグレーションファイルを勝手に削除すると大変な事に・・・・・
消さないでね
今後も学習で気づきや参考になるものがあれば、アップしていきます。
もし参考になったらいいね!!よろしくお願いします
- 投稿日:2019-10-04T12:36:35+09:00
Rails6 のちょい足しな新機能を試す89(Event object編)
はじめに
Rails 6 に追加された新機能を試す第89段。 今回は、
Event object
編です。
Rails 6 では、ActiveSupport::Notifications.subscribe
で、ブロックパラメータを1つにすると、そのブロックパラメータにActiveSupport::Notifications::Event
オブジェクトが設定されるようになりました。
これで、ブロックの中でActiveSupport::Notifications::Event.new
を使う必要がなくなりました。Ruby 2.6.4, Rails 6.0.0 で確認しました。
$ rails --version Rails 6.0.0今回は、User の CRUD を作り、各コントローラのアクションを実行したときの情報を取得してログに出力してみます。
プロジェクトを作る
rails new rails_sandbox cd rails_sandbox
User の CRUD を作る
name をもつ User の CRUD を作ります
bin/rails g scaffold User nameSubscribe する
config/initializers/notification_subscriber.rb
を作成します。
ActiveSupport::Notifications.subscribe
を呼び出すときに、ブロックパラメータをevent
1つだけにします。config/initializers/notification_subscriber.rbActiveSupport::Notifications.subscribe 'process_action.action_controller' do |event| action = "#{event.payload[:controller]}##{event.payload[:action]}" Rails.logger.info("#{action} cpu_time=#{event.cpu_time} duration=#{event.duration} allocations=#{event.allocations}") endUser の登録などをしてログを確認する
実際に
rails server
を実行してブラウザから http://localhost:3000/users にアクセスします。以下のようにコンソールに出力されているはずです。
... UsersController#index cpu_time=833.013591 duration=840.9244199865498 allocations=1237733試したソース
試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try089_subscriber_event参考情報
- 投稿日:2019-10-04T11:52:01+09:00
ゲストログイン機能の実装方法(ポートフォリオ用)
ポートフォリオ用のWebサイトに必須と言われるゲストログイン機能。
特に
Devise
のconfirmable
を使用している場合は,ゲストログイン機能が無いと,新規登録が面倒という理由でポートフォリオを見てすらもらえない可能性が高くなります。ところが,普通のWebサイトには絶対に導入しないためか,記事が見当たりませんでした。そこで,私のたどりついた実装方法を備忘録としてアップしておきます。
ゲストログイン機能の実装方法
以下,
Devise
を使用している前提とします。config/routes.rbpost '/homes/guest_sign_in', to: 'homes#guest_sign_in'app/controllers/homes_controller.rbclass HomesController < ApplicationController def guest_sign_in user = User.find_or_create_by(email: 'guest@example.com') do |user| user.password = SecureRandom.urlsafe_base64 user.confirmed_at = Time.now # Devise の confirmable を使用している場合は必須 end sign_in user redirect_to root_path, notice: 'ゲストユーザーとしてログインしました。' end endapp/views/homes/index.html.erb<%= link_to 'ゲストログイン(閲覧用)', homes_guest_sign_in_path, method: :post %>
ポイントは
Devise
のsign_in
メソッドを利用することです。ゲストユーザーをあらかじめ作成する手間を省くため,
find_by
ではなく,find_or_create_by
を利用しています。パスワードを把握する必要はないので,ランダム文字列にしています。備考
メールアドレス
,パスワード
を変更できるように設定する場合は,ゲストユーザーのメールアドレスなどを変更されないように注意する必要があります。実は,最初,
devise/sessions/new.html.erb
の真似をして,hidden_field
を加えて……という手順で実装しようとしました。ところが,この方法ではuser_email
,user_password
などのidセレクタが重複するエラーが出てしまいます。そこで,sign_in
メソッドの存在を思い出し,このような実装方法にたどりつきました。おまけ
ゲストユーザーのデータを定期的に初期化するため,実際にはこのような形式で実装しました。
name
カラムはニックネーム
,content
は自己紹介文
の想定です。app/controllers/homes_controller.rbclass HomesController < ApplicationController def guest_sign_in new_user_check = false user = User.find_or_create_by(email: 'guest@example.com') do |user| user.password = SecureRandom.urlsafe_base64 user.confirmed_at = Time.now new_user_check = true end # 新規ユーザー作成時と,前回のゲストログインor更新から30分以上経過している場合はデータを初期化 data_initialization(user) if new_user_check || Time.now - user.updated_at > 1800 sign_in user redirect_to root_path, notice: 'ゲストユーザーとしてログインしました。' end private def data_initialization(user) user_params = { # ゲストログイン時にデータを初期化するかどうか判断できるようにupdated_atを更新 updated_at: Time.now, name: 'ゲスト', content: 'ゲストユーザーの各種データは30分後に初期化されます。ご了承下さい。', } user.update(user_params) end end
- 投稿日:2019-10-04T11:45:21+09:00
mysql2がldflagsのオプション使ってもbundle installできない
以前試した奴がうまくいかなかった。
以前解決した方法はこちら。
Rails 5.2.3でmysql2がbundle isntallできない
下記のようになってconfigの中身を書き換えて、何度もやっていると当然っちゃ当然ですがリプレイスされただけの結果しか生まれない
You are replacing the current local value of build.mysql2, which is currently "--with-ldflags=-L/usr/local/opt/openssl/lib"ほう?お前、別件エラーだな?
どうやらまたmysql2の別のエラーのようです。
散々探し回ってようやく見つけた答えがこちらです。mysql2 (>= 0.3.13, < 0.5)を
mysql2 (~> 0.4.10)にバージョンをあげてあげることです。
githubの方にもissueが上がっておりました。
Support mysql2 0.4.x and 0.5.x #32310
- 投稿日:2019-10-04T11:33:10+09:00
【Rspec】railsでテスト用のDB作成
railsでテスト用のDBを作成します。
database.ymlにtest用のDBを記述します。
database.ymldefault: &default adapter: postgresql encoding: unicode pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> development: <<: *default database: sample_development test: <<: *default database: sample_testtest環境を指定して、migrationしてあげます。
$ bundle exec rake db:migrate RAILS_ENV=test以上です。
- 投稿日:2019-10-04T10:31:11+09:00
[Rails] GraphQLのディレクトリを整理してみた
ディレクトリ
. ├── enum_types # Enumを定義する。 ├── resolvers # queryのresolveを書く。 │ └── sample_resources ── fetch_sample # 〇〇_resourceでディレクトリを分ける。 ├── mutations # mutationのresolveを書く。 │ └── sample_resources ── update_sample # 〇〇_resourceでディレクトリを分ける。 ├── objective_types # ModelのType定義を行う。 ├── input_types # 主に単体ModelのCRUDを行う際の引数を定義する。 ├── scalar_types # Date, i18n, JSON, Image, etc... のデータ型を定義する。 ├── loaders # graphql-batchで生成されるディレクトリ。 ├── query_types │ ├── query_type │ └── mutation_type └── api_schema.rbこれだと大体混乱しない。
使っているGem
面倒臭いところ
- EnumとかをModelと重複して、書くのはだるすぎる。
- まじでModelからObjectiveTypeとかを自動生成したい。
気になること
- エラーハンドリング(現在は、Schemaに定義して、errorを返している。)
- リソース管理(現在は、GraphQLRubyのProのPundit Integrationを使っている。)
- セキュリティ周り
以上の3点がいつも気になる。
うまくやってる人いたら教えてくださーい!!
- 投稿日:2019-10-04T08:58:23+09:00
railsでプロジェクトを丸々コピーする
- 投稿日:2019-10-04T05:13:15+09:00
Rails 6.0 の DatabaseSelector middleware を理解する ①使ってみる
このシリーズについて
- Rails 6.0 の DatabaseSelector middleware を理解する ①使ってみる (この記事)
- DatabaseSelector を使ってみて、挙動を確認する。
- Rails 6.0 の DatabaseSelector middleware を理解する ②読んでみる (次回公開)
- DatabaseSelector のコードリーディングをして、仕組みを理解する。
目的
ActiveRecord::Middleware::DatabaseSelector
のコードリーディングを通して、その仕組みを理解して使えるようになる。- DatabaseSelector を知ることで、 Ruby on Rails 6.0 の複数データベース対応について理解する。
要約
- HTTP GET / HTTP HEAD では
:reading
- それ以外は
:writing
:writing
を使って数秒間は HTTP メソッドにかかわらず:writing
を使わせるサンプルコード
https://github.com/takeyuweb/rails6-multidb-sample
- PostgreSQL の非同期レプリケーション
- Rails 6.0 の Multiple DBs 機能
- DatabaseSelector を使った HTTP Method による接続先自動切り替え
確認環境
- Ruby on Rails 6.0
- Docker 19.03.2 on WSL2
DatabaseSelector
DatabaseSelector は、Railsガイドの3 コネクションの自動切り替えを有効にする が詳しいです。
今回はサンプルアプリを作り、実際に手元で動かして、挙動を確認してみました。
DatabaseSelector を設定する
ロールと接続先
まずは複数データベース接続設定を書きます。
1 アプリケーションのセットアップ - Active Record で複数のデータベース利用
app/modles/application_record.rbclass ApplicationRecord < ActiveRecord::Base self.abstract_class = true connects_to database: { writing: :primary, reading: :primary_readonly } endhttps://github.com/takeyuweb/rails6-multidb-sample/blob/master/app/models/application_record.rb
Role:
:reading
:writing
Railsにより提供される。変更もできるが、基本このまま。
Specification::primary
:primary_readonly
はdatabase.yml
に記載した接続先名。:animal
:replica
など、わかりやすいようにつけられます。
詳しくは Railsガイドconfig/database.ymldefault: &default adapter: postgresql encoding: unicode # For details on connection pooling, see Rails configuration guide # https://guides.rubyonrails.org/configuring.html#database-pooling pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> development: # 仮にこれを `hoge:` にすると `schema.rb` は `hoge_schema.rb` になる。 primary: <<: *default database: MyApp_development host: <%= ENV.fetch('DATABASE_HOST_PRIMARY') { 'db' } %> username: <%= ENV.fetch('DATABASE_USER') { 'postgres' } %> password: <%= ENV.fetch('DATABASE_PASSWORD') { 'password' } %> primary_readonly: <<: *default database: MyApp_development host: <%= ENV.fetch('DATABASE_HOST_READONLY') { 'db' } %> username: <%= ENV.fetch('DATABASE_USER') { 'postgres' } %> password: <%= ENV.fetch('DATABASE_PASSWORD') { 'password' } %> replica: truehttps://github.com/takeyuweb/rails6-multidb-sample/blob/master/config/database.yml
DatabaseSelector を有効にする
DatabaseSelector ミドルウェアを使うには、次のようにします。
少し冗長に思えますが、これによりカスタマイズの余地が生まれています。(次回解説します)config.active_record.database_selector = { delay: 2.seconds } config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::SessionDatabaseSelector の挙動を確認する
サンプルアプリ を起動して、どこにどんなクエリが飛ぶか見てみます。
HTTP GET / HTTP HEAD では :reading
GET http://localhost:3000/posts/1/edit
をみてみます。
このとき、ログから
:reading
に指定したレプリカを使用したことがわかります。app_1 | Started GET "/posts/1/edit" for 172.21.0.1 at 2019-10-03 19:17:58 +0000 app_1 | Cannot render console from 172.21.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1 pg_primary_1 | 2019-10-03 19:17:58.241 UTC [27] LOG: statement: SELECT 1 app_1 | Processing by PostsController#edit as HTML app_1 | Parameters: {"id"=>"1"} pg_readonly_1 | 2019-10-03 19:17:58.243 UTC [28] LOG: statement: SELECT 1 pg_readonly_1 | 2019-10-03 19:17:58.243 UTC [28] LOG: execute a1: SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2 pg_readonly_1 | 2019-10-03 19:17:58.243 UTC [28] DETAIL: parameters: $1 = '1', $2 = '1' app_1 | Post Load (0.4ms) SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]] app_1 | ↳ app/controllers/posts_controller.rb:67:in `set_post' app_1 | Rendering posts/edit.html.erb within layouts/application app_1 | Rendered posts/_form.html.erb (Duration: 1.2ms | Allocations: 747) app_1 | Rendered posts/edit.html.erb within layouts/application (Duration: 1.5ms | Allocations: 867) app_1 | Completed 200 OK in 8ms (Views: 5.4ms | ActiveRecord: 0.4ms | Allocations: 7126)HTTP GET / HTTP HEAD 以外では :writing
では
POST http://localhost:3000/posts
ではどうでしょうか?
記事を作成してみます。このときのログは次のように、先ほどの GET とは異なり、
:writing
に設定したプライマリを使ったことがわかります。app_1 | Started POST "/posts" for 172.21.0.1 at 2019-10-03 19:21:22 +0000 app_1 | Cannot render console from 172.21.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1 pg_primary_1 | 2019-10-03 19:21:22.039 UTC [27] LOG: statement: SELECT 1 app_1 | Processing by PostsController#create as HTML app_1 | Parameters: {"authenticity_token"=>"BHkgkW4WTj5JAdB4DZ6PmS2ZEq9bDHmSzZ3900V4roTY6CDKHUhd3savRJCag0oaXOlQKlVyd8BPsl9VFhTZQg==", "post"=>{"title"=>"Hello Rails 6.0", "body"=>""}, "commit"=>"Create Post"} pg_primary_1 | 2019-10-03 19:21:22.042 UTC [27] LOG: statement: BEGIN app_1 | (0.3ms) BEGIN app_1 | ↳ app/controllers/posts_controller.rb:30:in `block in create' pg_primary_1 | 2019-10-03 19:21:22.043 UTC [27] LOG: execute <unnamed>: INSERT INTO "posts" ("title", "body", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" pg_primary_1 | 2019-10-03 19:21:22.043 UTC [27] DETAIL: parameters: $1 = 'Hello Rails 6.0', $2 = '', $3 = '2019-10-03 19:21:22.041603', $4 = '2019-10-03 19:21:22.041603' app_1 | Post Create (0.9ms) INSERT INTO "posts" ("title", "body", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["title", "Hello Rails 6.0"], ["body", ""], ["created_at", "2019-10-03 19:21:22.041603"], ["updated_at", "2019-10-03 19:21:22.041603"]] app_1 | ↳ app/controllers/posts_controller.rb:30:in `block in create' pg_primary_1 | 2019-10-03 19:21:22.045 UTC [27] LOG: statement: COMMIT app_1 | (0.5ms) COMMIT app_1 | ↳ app/controllers/posts_controller.rb:30:in `block in create' app_1 | Redirected to http://localhost:3000/posts/2 app_1 | Completed 302 Found in 6ms (ActiveRecord: 1.7ms | Allocations: 2959):writing へのクエリ発行直後は HTTP GET でも :writing
次のログは、記事を更新後、リダイレクト先で更新した記事を表示したときのログです。
ログから、 GET でも
:reading
ではなく:writing
を使用したことがわかります。非同期レプリケーションの場合、プライマリでの更新直後、レプリカへの変更が反映されていない可能性があり、これはユーザーに対して期待と異なる挙動を示すため、それを防ぐための措置だと思います。
app_1 | Started PATCH "/posts/2" for 172.21.0.1 at 2019-10-03 19:26:39 +0000 app_1 | Cannot render console from 172.21.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1 pg_primary_1 | 2019-10-03 19:26:39.776 UTC [62] LOG: statement: SELECT 1 app_1 | Processing by PostsController#update as HTML app_1 | Parameters: {"authenticity_token"=>"BDy9M4hAOSryBYPDpx3vu/bx03w9GatkTFO2GsBH0GneYILF6Y+pmk7UxF/3WVnr3Cosup5/EDRM3zVS10WdZA==", "post"=>{"title"=>"Hello Rails 6.0!", "body"=>""}, "commit"=>"Update Post", "id"=>"2"} pg_primary_1 | 2019-10-03 19:26:39.779 UTC [62] LOG: statement: SHOW search_path pg_primary_1 | 2019-10-03 19:26:39.780 UTC [62] LOG: execute a1: SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2 pg_primary_1 | 2019-10-03 19:26:39.780 UTC [62] DETAIL: parameters: $1 = '2', $2 = '1' app_1 | Post Load (0.8ms) SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]] app_1 | ↳ app/controllers/posts_controller.rb:67:in `set_post' pg_primary_1 | 2019-10-03 19:26:39.783 UTC [62] LOG: statement: BEGIN app_1 | (0.4ms) BEGIN app_1 | ↳ app/controllers/posts_controller.rb:44:in `block in update' pg_primary_1 | 2019-10-03 19:26:39.786 UTC [62] LOG: execute <unnamed>: UPDATE "posts" SET "title" = $1, "updated_at" = $2 WHERE "posts"."id" = $3 pg_primary_1 | 2019-10-03 19:26:39.786 UTC [62] DETAIL: parameters: $1 = 'Hello Rails 6.0!', $2 = '2019-10-03 19:26:39.783058', $3 = '2' app_1 | Post Update (0.7ms) UPDATE "posts" SET "title" = $1, "updated_at" = $2 WHERE "posts"."id" = $3 [["title", "Hello Rails 6.0!"], ["updated_at", "2019-10-03 19:26:39.783058"], ["id", 2]] app_1 | ↳ app/controllers/posts_controller.rb:44:in `block in update' pg_primary_1 | 2019-10-03 19:26:39.788 UTC [62] LOG: statement: COMMIT app_1 | (0.4ms) COMMIT app_1 | ↳ app/controllers/posts_controller.rb:44:in `block in update' app_1 | Redirected to http://localhost:3000/posts/2 app_1 | Completed 302 Found in 12ms (ActiveRecord: 2.8ms | Allocations: 3467) app_1 | app_1 | app_1 | Started GET "/posts/2" for 172.21.0.1 at 2019-10-03 19:26:39 +0000 app_1 | Cannot render console from 172.21.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1 pg_primary_1 | 2019-10-03 19:26:39.801 UTC [62] LOG: statement: SELECT 1 app_1 | Processing by PostsController#show as HTML app_1 | Parameters: {"id"=>"2"} pg_primary_1 | 2019-10-03 19:26:39.803 UTC [62] LOG: execute a1: SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2 pg_primary_1 | 2019-10-03 19:26:39.803 UTC [62] DETAIL: parameters: $1 = '2', $2 = '1' app_1 | Post Load (0.5ms) SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]] app_1 | ↳ app/controllers/posts_controller.rb:67:in `set_post' app_1 | Rendering posts/show.html.erb within layouts/application app_1 | Rendered posts/show.html.erb within layouts/application (Duration: 0.3ms | Allocations: 94) app_1 | Completed 200 OK in 7ms (Views: 4.8ms | ActiveRecord: 0.5ms | Allocations: 6362)更新系クエリでない場合も、:writing を強制するか?
レプリカへの変更が反映されていない可能性への対応であるならば、
:writing
へのクエリ発行直後であっても、そのクエリがINSERT
などの更新系クエリでなければ:reading
でよいのではないか?そう考えて試してみます。ログによると、 PATCH により
:writing
がSELECT
に使われたことがわかりますが、変更がなかったためUPDATE
は発行されませんでした。
しかし、それでも次の GET では:writing
を使っています。このことから、更新系クエリでない場合も「
:writing
を使ったら、しばらく:reading
」と考えて良いようです。app_1 | Started PATCH "/posts/2" for 172.21.0.1 at 2019-10-03 19:36:14 +0000 app_1 | Cannot render console from 172.21.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1 pg_primary_1 | 2019-10-03 19:36:14.234 UTC [91] LOG: statement: SELECT 1 app_1 | Processing by PostsController#update as HTML app_1 | Parameters: {"authenticity_token"=>"gcEcnplYdoDMYrET0VxW3hBpLPqEtETt56OSj+QPHZ9bnSNo+JfmMHCz9o+BGOCOOrLTPCfS/73nLxHH8w1Qkg==", "post"=>{"title"=>"Hello Rails 6.0!", "body"=>""}, "commit"=>"Update Post", "id"=>"2"} pg_primary_1 | 2019-10-03 19:36:14.236 UTC [91] LOG: statement: SHOW search_path pg_primary_1 | 2019-10-03 19:36:14.236 UTC [91] LOG: execute a1: SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2 pg_primary_1 | 2019-10-03 19:36:14.236 UTC [91] DETAIL: parameters: $1 = '2', $2 = '1' app_1 | Post Load (0.4ms) SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]] app_1 | ↳ app/controllers/posts_controller.rb:67:in `set_post' app_1 | Redirected to http://localhost:3000/posts/2 app_1 | Completed 302 Found in 4ms (ActiveRecord: 0.7ms | Allocations: 1312) app_1 | app_1 | app_1 | Started GET "/posts/2" for 172.21.0.1 at 2019-10-03 19:36:14 +0000 app_1 | Cannot render console from 172.21.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1 pg_primary_1 | 2019-10-03 19:36:14.247 UTC [91] LOG: statement: SELECT 1 app_1 | Processing by PostsController#show as HTML app_1 | Parameters: {"id"=>"2"} pg_primary_1 | 2019-10-03 19:36:14.249 UTC [91] LOG: execute a1: SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2 pg_primary_1 | 2019-10-03 19:36:14.249 UTC [91] DETAIL: parameters: $1 = '2', $2 = '1' app_1 | Post Load (0.4ms) SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]] app_1 | ↳ app/controllers/posts_controller.rb:67:in `set_post' app_1 | Rendering posts/show.html.erb within layouts/application app_1 | Rendered posts/show.html.erb within layouts/application (Duration: 0.3ms | Allocations: 94) app_1 | Completed 200 OK in 9ms (Views: 7.5ms | ActiveRecord: 0.4ms | Allocations: 6364)HTTP GET でも :writing 使いたいとき
実務ではいろいろあるものです。
コネクションを手動で切り替えることで、明示的に:writing
を使うことはできます。# GET /posts/1 # GET /posts/1.json def show ActiveRecord::Base.connected_to(role: :writing) do @post = Post.lock.find(params[:id]) @post.touch end endapp_1 | Started GET "/posts/2" for 172.21.0.1 at 2019-10-03 20:34:20 +0000 app_1 | Cannot render console from 172.21.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1 pg_primary_1 | 2019-10-03 20:34:20.236 UTC [35] LOG: statement: SELECT 1 app_1 | Processing by PostsController#show as HTML app_1 | Parameters: {"id"=>"2"} pg_primary_1 | 2019-10-03 20:34:20.239 UTC [35] LOG: execute <unnamed>: SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2 FOR UPDATE pg_primary_1 | 2019-10-03 20:34:20.239 UTC [35] DETAIL: parameters: $1 = '2', $2 = '1' app_1 | Post Load (0.5ms) SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2 FOR UPDATE [["id", 2], ["LIMIT", 1]] app_1 | ↳ app/controllers/posts_controller.rb:14:in `block in show' pg_primary_1 | 2019-10-03 20:34:20.240 UTC [35] LOG: statement: BEGIN app_1 | (0.3ms) BEGIN app_1 | ↳ app/controllers/posts_controller.rb:15:in `block in show' pg_primary_1 | 2019-10-03 20:34:20.242 UTC [35] LOG: execute <unnamed>: UPDATE "posts" SET "updated_at" = $1 WHERE "posts"."id" = $2 app_1 | Post Update (0.5ms) UPDATE "posts" SET "updated_at" = $1 WHERE "posts"."id" = $2 [["updated_at", "2019-10-03 20:34:20.240568"], ["id", 2]] pg_primary_1 | 2019-10-03 20:34:20.242 UTC [35] DETAIL: parameters: $1 = '2019-10-03 20:34:20.240568', $2 = '2' app_1 | ↳ app/controllers/posts_controller.rb:15:in `block in show' pg_primary_1 | 2019-10-03 20:34:20.243 UTC [35] LOG: statement: COMMIT app_1 | (0.3ms) COMMIT app_1 | ↳ app/controllers/posts_controller.rb:15:in `block in show' app_1 | Rendering posts/show.html.erb within layouts/application app_1 | Rendered posts/show.html.erb within layouts/application (Duration: 0.3ms | Allocations: 90) app_1 | Completed 200 OK in 10ms (Views: 4.3ms | Allocations: 8429)次回
DatabaseSelector のコードリーディングを通して、なぜ今回のような挙動になったか?その仕組みを紐解きます。
参考