- 投稿日:2020-08-09T23:15:55+09:00
[Rails]deviseを使ったウィザード形式での新規登録機能の実装
はじめに
フリマのコピーサイトを作る際に、構造をやや難しくしてしまったために苦労したので記録として残そうと思いました。もっと綺麗にかけるなどご指摘あればお願いします。
実装方法
開発環境
- Ruby 2.5.1
- Rails 5.0.7.2
- devise
前提
- deviseは導入済み
- deviseのデフォルト状態でのログイン機能は実装済み
- 1ページ目 → 2ページ目 → トップページ(ログイン)と遷移する
- テーブルは、
users
profiles
sending_destinations
の3つ- 1ページ目は1つのフォームで
Userモデル
とProfileモデル
の二つを扱っている
※2つのモデルに同時に値を送る方法はこちらの記事を参考にしています。
【Rails】deviseのフォームで2つのモデルに同時に値を送る方法(例: UserモデルとProfileモデル)DB設計
usersテーブル
Column Type Options name string null: false, unique: true, index:true string null: false, unique: true, index:true password string null: false profilesテーブル
Column Type Options first_name string null: false family_name string null: false first_name_kana string null: false family_name_kana string null: false introduction string null: true year integer null: false month integer null: false day integer null: false sending_destinations(住所)テーブル
Column Type Options first_name string null :false family_name string null: false first_name_kana string null: false family_name_kana string null: false post_code string null: false prefecture string null: false city string null:false house_number string null: false building_name string - phone_number string unique: true, null: true user_id references null: false, foreign_key: true コード
controllerの作成
- devise管理下の
users
コントローラーを作成するターミナル$ rails g devise:controllers users
- どのコントローラーを参照するのかルーティングを設定
routes.rbRails.application.routes.draw do devise_for :users, controllers: { registrations: 'users/registrations' } root to: 'items#index' endモデルにアソシエーションを記述する
User
、Profile
、Sending_destination
それぞれのモデルにアソシエーションを記述
※バリデーションは省略しています。user.rbclass User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable has_one :profile accepts_nested_attributes_for :profile has_one :sending_destination endprofile.rbclass Profile < ApplicationRecord belongs_to :user, optional: true endsending_destination.rbclass SendingDestination < ApplicationRecord belongs_to :user, optional: true end
optional: true
は外部キーがnullであることを許可するオプションです。newアクションと対応するビューを編集する(1ページ目)
- users/registrations_controller.rbにnewアクションを記述
users/registrations_controller.rbclass Users::RegistrationsController < Devise::RegistrationsController # 省略 def new @user = User.new @user.build_profile end # 省略 end
- newアクションに対応するregistrations/new.html.hamlを編集
devise/registrations/new.haml.html= form_for(@user, url: user_registration_path) do |f| = render "devise/shared/error_messages", resource: @user = f.text_field :nickname = f.email_field :email = f.password_field :password = f.password_field :password_confirmation = f.fields_for :profile do |p| = p.text_field :family_name = p.text_field :first_name = p.text_field :family_name_kana = p.text_field :first_name_kana = p.select :year = p.select :month = p.select :day = f.submit "次へ"実際はdivやlabel、classの記載がありますが、簡素に書いています。
2つのモデルを扱うためにfields_for
を利用しています。以下の記事を参考にしています。
【Rails】deviseのフォームで2つのモデルに同時に値を送る方法(例: UserモデルとProfileモデル)
【Rails】1つのform_forで複数モデルへデータ登録をする方法createアクションを編集
- 1ページ目で入力した情報のバリデーションチェック
- 1ページで入力した情報をsessionに保持させる
- 次の住所情報登録で使用するインスタンスを生成、当該ページへ遷移する 以上の3点がcreateアクションでやることになります。
users/registrations_controller.rbclass Users::RegistrationsController < Devise::RegistrationsController # 省略 def create @user = User.new(sign_up_params) @user.build_profile(sign_up_params[:profile_attributes]) unless @user.valid? flash.now[:alert] = @user.errors.full_messages render :new and return end session["devise.regist_data"] = {user: @user.attributes} session["devise.regist_data"][:user]["password"] = params[:user][:password] session[:profile_attributes] = sign_up_params[:profile_attributes] @sending_destination = @user.build_sending_destination render :new_sending_destination end # 省略 protected # 省略 end
valid?
メソッドでパラメータがバリデーションに違反しないかどうかチェックします。
ページが遷移しても情報が消えることが無いように、クライアント側で保持をさせておく機能session
を用いています。
sessionにハッシュオブジェクトの形で情報を保持させるために、attributesメソッドを用いてデータを整形しています。また、paramsの中にはパスワードの情報は含まれていますが、attributesメソッドでデータ整形をした際にパスワードの情報は含まれていません。そこで、パスワードを再度sessionに代入する必要があります。
build_sending_destination
で今回生成したインスタンス@user
に紐づくsending_destination
モデルのインスタンスを生成します。
そして、住所情報を登録させるページを表示するnew_sending_destination
アクションのビューへrenderします。new_sending_destinationアクションと対応するビューを編集する(2ページ目)
- 住所登録をするページを表示する
new_sending_destination
アクションのルーティングの設定- 住所を登録する
create_sending_destination
アクションのルーティングの設定routes.rbRails.application.routes.draw do devise_for :users, controllers: { registrations: 'users/registrations' } devise_scope :user do get 'sending_destinations', to: 'users/registrations#new_sending_destination' post 'sending_destinations', to: 'users/registrations#create_sending_destination' end root to: 'items#index' end
- 該当するビューファイルであるnew_sending_destination.html.hamlを作成
devise/registrations/new_sending_destination.html.haml= form_for @sending_destination do |f| = render "devise/shared/error_messages", resource: @sending_destination = f.text_field :family_name = f.text_field :first_name = f.text_field :family_name_kana = f.text_field :first_name_kana = f.text_field :post_code = f.text_field :prefecture = f.text_field :city = f.text_field :house_number = f.text_field :building_name = f.number_field :phone_number = f.submit "登録する"create_sending_destinationアクションを編集する
- 2ページ目で入力した住所情報のバリデーションチェック
- バリデーションチェックが完了した情報と、sessionで保持していた情報とあわせ、ユーザー情報として保存する
- sessionを削除する
- ログインをする
users/registrations_controller.rbclass Users::RegistrationsController < Devise::RegistrationsController # 省略 def create_sending_destination @user = User.new(session["devise.regist_data"]["user"]) @profile = @user.build_profile(session[:profile_attributes]) @sending_destination = SendingDestination.new(sending_destination_params) unless @sending_destination.valid? flash.now[:alert] = @sending_destination.errors.full_messages render :new_sending_destination and return end @user.build_sending_destination(@sending_destination.attributes) @user.save @profile.save session["devise.regist_data"]["user"].clear session[:profile_attributes].clear sign_in(:user, @user) redirect_to root_path end protected def sending_destination_params params.require(:sending_destination).permit( :first_name, :family_name, :first_name_kana, :family_name_kana, :post_code, :prefecture, :city, :house_number, :building_name, :phone_number ) end end
@user
と@prfile
それぞれのインスタンスにsessionで保持した情報を代入しています。
2ページ目の住所情報をvalid?
でチェックします。
build_sending_destinationを用いて送られてきたparamsを、保持していたsessionが含まれる@user
に代入します。そしてsave
メソッドを用いてテーブルに保存します。
clear
メソッドを用いてsessionを削除します。
sign_in(:user, @user)
でログインし、redirect_to root_path
でトップページに遷移します。おわりに
以上の方法で実装できます。1ページ目で
fields_for
を用いたことでsessionへの代入にかなり四苦八苦しましたが、おかげでしっかり仕組みなどを考えることができました。細かな説明はしていませんが、アウトプットも兼ねて記事にしてみたので少しでも参考になれば幸いです。
- 投稿日:2020-08-09T21:43:06+09:00
RailsにVue.jsを導入する準備
はじめに
バージョン
Ruby '2.6.5'
Rails '6.0.3'
Vue.js '2.6.11'Railsは6.0以降。
Vue.jsの導入
Webpackerを用いてインストール
ターミナルrails webpacker:install:vueVue.jsを読み込む
app/view/layouts/application.html.erb<!DOCTYPE html> <html> <head> <title>Title</title> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag 'application', media: 'all' %> <%= javascript_pack_tag 'application' %> <%# 以下の一行を追加 %> <%= javascript_pack_tag 'hello_vue' %> </head> <body> <%= yield %> </body> </html>localhost:3000へアクセスして、"Hello Vue!"と表示されていれば、
導入と読み込みは上手くいっています。
- 投稿日:2020-08-09T21:08:11+09:00
Rails reference型 作成 追加
はじめに
今回はrailsでのマイグレーション時にreference型を扱うことがことがあったのでまとめる。
外部キーとは
reference型を扱うにあたって外部キーについて知っておかなくてはならない。
外部キーとは、リレーショナルデータベース(RDB)で、テーブルのある列に、別のテーブルの特定の列に含まれる項目しか入力できないようにする制約。また、その際に指定する列
http://e-words.jp/w/%E5%A4%96%E9%83%A8%E3%82%AD%E3%83%BC.html#:~:text=%E5%A4%96%E9%83%A8%E3%82%AD%E3%83%BC%E3%81%A8%E3%81%AF%E3%80%81%E3%83%AA%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%8A%E3%83%AB,%E3%82%92%E7%94%A8%E3%81%84%E3%81%A6%E8%A8%AD%E5%AE%9A%E3%81%A7%E3%81%8D%E3%82%8B%E3%80%82今回はarticlesテーブルにreference型のuser_idという外部キーを設定する。
前提
sampleとしてuserテーブルとariticleテーブルを準備する。
class CreateUsers < ActiveRecord::Migration[6.0] def change create_table :users do |t| t.string :name t.string :email end end endclass CreateArticles < ActiveRecord::Migration[6.0] def change create_table :articles do |t| t.string :title t.string :content end end end$ rake db:migratereference型のカラムの作成
reference型で作成すると
user
そのまま追加されるわけではなくuser_id
というカラム名で追加される。また、index:true
を追記することなく自動でインデックスをはってくれるというメリットもある。class CreateArticles < ActiveRecord::Migration[6.0] def change create_table :articles do |t| t.string :title t.string :content t.references :user, foreign_key: true end end end*
foreign_key: true
をつけないと外部キー制約はつけることができない。
foreign_key: true
を書き忘れたことで設定できないこtがあった。
add_foreign_key
を使っての追加も可能。class CreateArticles < ActiveRecord::Migration[6.0] def change create_table :articles do |t| t.string :title t.string :content end add_foreign_key :articles, :users end endreference型のカラムの追加
class AddReferenceColumn < ActiveRecord::Migration[6.0] def change add_reference :articles, :user, foreign_key: true end endこの際にも
foreign_key: true
を忘れずに。
- 投稿日:2020-08-09T20:18:44+09:00
初歩中の初歩!railsでのモデル・コントローラー作成方法
モデル作成方法
rails g モデル名(単数)マイグレーション型一覧
・string : 文字列
・text : 長い文字列
・integer : 整数
・float : 浮動小数
・decimal : 精度の高い小数
・datetime : 日時
・timestamp : タイムスタンプ
・time : 時間
・date : 日付
・binary : バイナリデータ
・boolean : Booleanclass CreateExercises < ActiveRecord::Migration[6.0] def change create_table :テーブル名(複数形) do |t| t.migrate型 :カラム名(単数) t.string :part t.text :url t.integer :level t.timestamps end end endここまで設定できたらお決まりの
rails db:migrate == 20200809105002 CreateExercises: migrating ================================== -- create_table(:exercises) -> 0.0244s == 20200809105002 CreateExercises: migrated (0.0245s) =========================こんな感じの表示がでれば成功
コントローラー作成
rails g controller コントローラー名 アクション名※コントローラー名は大文字始まりの複数形
下記のような表示がでれば成功Running via Spring preloader in process 65910 create app/controllers/exercises_controller.rb route get 'exercises/index' invoke erb create app/views/exercises create app/views/exercises/index.html.erb invoke test_unit create test/controllers/exercises_controller_test.rb invoke helper create app/helpers/exercises_helper.rb invoke test_unit invoke assets invoke scss create app/assets/stylesheets/exercises.scss
- 投稿日:2020-08-09T20:12:34+09:00
【rails】DEPRECATION WARNING: Leaving `ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer` ... を直す方法
以下のエラーが出た場合
DEPRECATION WARNING: Leaving `ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer` set to false is deprecated. SQLite databases have used 't' and 'f' to serialize boolean values and must have old data converted to 1 and 0 (its native boolean serialization) before setting this flag to true. Conversion can be accomplished by setting up a rake task which runs ExampleModel.where("boolean_column = 't'").update_all(boolean_column: 1) ExampleModel.where("boolean_column = 'f'").update_all(boolean_column: 0) for all models and all boolean columns, after which the flag must be set to true by adding the following to your application.rb file: Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true (called from instance_eval at /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/activesupport-5.2.4.3/lib/active_support/lazy_load_hooks.rb:71) Started GET "/" for 122.26.15.2 at 2020-08-09 10:47:48 +0000 Cannot render console from 122.26.15.2! Allowed networks: 127.0.0.1,以下を追加
application.rb(省略) Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true以上。
- 投稿日:2020-08-09T18:13:00+09:00
コンテナ上のデバッグ環境構築 - DockerでRailsチュートリアルのローカル開発環境構築 -
はじめに
Dockerでローカル開発環境構築を行い、Railsチュートリアルを再走しております
- Railsチュートリアル最新版(2020.8.6現在)に対応のRails 6
- Dockerを使用し、開発環境の再現が可能
- なるべくローカル環境にインストールしない
今回はRailsチュートリアルの7章に相当する部分で、
Dockerコンテナ上で開発を行っている場合にデバッグを行う内容を紹介します第一回
DockerでRailsチュートリアルのローカル開発環境構築(Rails 6 + PostgreSQL + Webpack) - Qiita第二回
DockerでRailsチュートリアルのローカル開発環境構築 - RSpec導入 & CircleCIでHerokuデプロイ- - Qiita第3回
DockerでRailsチュートリアルのローカル開発環境構築 - WebpackでBootstrapとFont Awesomeを導入 - - Qiita個人開発アプリ
mdClip <オンラインmarkdownエディタ>Dockerのコンテナ上で操作する場合はターミナルのコマンドを適宜
$ docker-compose run app ...もしくは
$ docker-compose exec app ...で置き換えてください。
Dockerコンテナ上の開発環境でRailsのデバッグを行う
方法1 Railsのコンテナにアタッチ -> デタッチ
参考サイト
Docker と Rails 5.2 の開発環境でデバッグを行えるようにする - ココナラよもやまブログRailsのコンテナにアタッチする
$ docker attach $(docker-compose ps -q app)これでデバッグコンソールが使用できます
コンテナからデタッチする
デバッグを
quit
で抜けると
コンテナが終了してしまうのでrails server
も落ちる
これではすごく使い勝手が悪いなと思ったらコンテナからデタッチ(
Control P
->Control Q
)するだけなら、コンテナを終了しなくて済むとのことただ、VS Codeのコンソールで操作していると
Control P
,Control Q
は他のショートカットに割り当てられないこれはコンテナでタッチキーを置き換えることで解決方法とのことです
参考記事
docker で Ctrl-p 2回押し問題 (detach-keys の問題) を解決するには - Qiita
~/.docker/config.json
に設定を記述しますvi ~/.docker/config.json{ # ...省略 "detachKeys": "ctrl-e" }私は
control + e
でデタッチできるように設定しました方法2 VS Codeでデバッグ用のコンテナを動かす
VS codeのデバッグ機能から、デバッグ用のコンテナを立ててデバッグする方法もあるようです
Developing inside a Container using Visual Studio Code Remote Development
シームレスで便利そうだと思ったのですが
ここまででちょっと時間を使ってしまったのでまた別の機会に追記します自分用メモ
Get started with development Containers in Visual Studio Code
VS Code - Remote Containers で Docker Compose で動いている Rails アプリに接続する - さめたコーヒー
Troubleshoot
いずれもBootstrapのバージョンが異なることに起因します
あまり時間を掛けて調べる意味もない気がするのでBootstrapのバージョンは合わせた方がいいかもしれません
Formが左に寄る
#修正前 col-md-offset-3 #修正後 offset-md-3$state-danger-textがないよ
$text-danger: #dc3545; # ...中略... .field_with_errors { @extend .has-error; .form-control { color: $text-danger; } }.has-errorがないよ
.has-error
->:invalid
$text-danger: #dc3545; # ...中略... .field_with_errors { @extend :invalid; .form-control { color: $text-danger; } }
- 投稿日:2020-08-09T17:58:44+09:00
devise導入後migrateをしたらmax key length is 767 bytesエラーが。
今回のエラー
ユーザー認証のdeviseを導入しようと
rails db:migrateを実行したらこんなエラーが
== 20200809082251 DeviseCreateUsers: migrating ================================ -- create_table(:users) -> 0.0154s -- add_index(:users, :email, {:unique=>true}) rails aborted! StandardError: An error has occurred, all later migrations canceled: Mysql2::Error: Specified key was too long; max key length is 767 bytes . . . . .解決方法
1.mysql.rbを新規作成
https://qiita.com/terufumi1122/items/9ea764618eba01144e09
こちらのQiitaの記事を参考にmysql.rbを新規作成して下記のように記述config/initializer/mysql.rbrequire 'active_record/connection_adapters/abstract_mysql_adapter' module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter NATIVE_DATABASE_TYPES[:string] = { :name => "varchar", :limit => 191 } end end end2.データベースをリセット&migrate実行
この状態で
rails db:migrate
を実行すると== 20200809082251 DeviseCreateUsers: migrating ================================ -- create_table(:users) rails aborted! StandardError: An error has occurred, all later migrations canceled: Mysql2::Error: Table 'users' already exists . . .userテーブルはすでにありますよ!と言われてしまうため一度データベースをリセットして再度
rails db:migrate
を実行する必要がある。リセットをマイグレーション実行を同時に行えるのが下記のコマンドrails db:migrate:resetこちらを実行すると
Dropped database 'training_app_development' Dropped database 'training_app_test' Created database 'training_app_development' Created database 'training_app_test' == 20200809082251 DeviseCreateUsers: migrating ================================ -- create_table(:users) -> 0.0110s -- add_index(:users, :email, {:unique=>true}) -> 0.0093s -- add_index(:users, :reset_password_token, {:unique=>true}) -> 0.0070s == 20200809082251 DeviseCreateUsers: migrated (0.0274s) =======================見事、deviseの設定をusersテーブルに反映する事ができた
- 投稿日:2020-08-09T16:09:36+09:00
Rails のエラーメッセージを日本語で表示出来るようにする。
概要
Railsではエラーメッセージの表示をする際に英語になっています。
日本語でエラーメッセージを表示するやり方について簡単に説明します。日本語でエラーメッセージを表示する為には
日本語用の翻訳ファイルを作成する必要があります。自分で作成することも可能ですが、
今回はGitHubのrails-I18nリポジトリに既に作成されている物を使う方法について説明します。1.翻訳ファイルをGitHubからダウンロード
※「・github から直接コピーする」「・wgetでファイルごとコピーする」
↓2つのやり方を説明していますがどちらの手法を取っていただいても構いません。・github から直接コピーする
https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/ja.yml
上記のURLにある内容をコピーして、自身のプロジェクトに '/locale/ja.yml' を作成して内容をペーストしてください。・wgetでファイルごとコピーする
command$ wget https://raw.githubusercontent.com/svenfuchs/rails-i18n/master/rails/locale/ja.yml -P config/locales/上記のコマンドをコピーしてターミナルで実行してあげればダウンロードされます。
2.日本語を使う為に設定変更
デフォルトで日本語をつかうよ。ということを設定ファイルに記述します。
config/initializers/locale.rbRails.application.config.i18n.default_locale = :ja以上で日本語の設定が終わりました。
日本語の設定方法について
日本語を設定するやり方を説明します。
ダウンロードしたja.ymlに追加します。
ja.yml--- ja: activerecord: errors: messages: ~ # ここから追加↓ attributes: user: name: 名前 email: メールアドレス admin: 管理者権限 password: パスワード password_confirmation: パスワード(確認用) created_at: 登録日時 updated_at: 更新時間 # ここまで追加↑ date: ~上記のような形式で日本語を設定してけば良いです。
- 投稿日:2020-08-09T16:03:09+09:00
FlipperでFeature Flagを導入する
railsで作ったapiに、feature flag(機能フラグ)を導入したいという要件があり、flipperというgemを使いました。備忘として残します。
環境は、rails6.0.3 apiモードです。feature flag(機能フラグ)とは
アプリケーションの各種機能の有効無効を、コードを変更したりブランチを切り替えて再デプロイすることなく実行中のプログラムの外部から切り替えることを目的とします。
具体的には、、
- あるapiは、特定のユーザや、特定の条件に該当するユーザのみ使わせる
- どのapiの使用を、どのユーザに許可するかは、コードを変更しなくても、railsコンソール上で切り替えたり、外部からweb UIやapiで切り替えられるようにしたい
といったことを実現できます。
gemの導入手順
- まず、Gemfileに下記を設定します。
Gemfilegem 'flipper' # flipper本体 gem 'flipper-active_record' # フラグを永続化するため※フラグの永続化については「アダプタについて」のところで書きます
- インストールします。
$ bundle install $ rails g flipper:active_record $ rails db:migrate
- 次に、
config/initializers
ディレクトリにflipper.rb
を追加config/initializers/flipper.rbrequire 'flipper' require 'flipper/adapters/active_record' Flipper.configure do |config| config.default do adapter = Flipper::Adapters::ActiveRecord.new # アダプタをActiveRecordアダプタとする Flipper.new(adapter) end endfeature flagの使い方
たとえば、フラグ
:hoge
があって、このtrue/falseに応じて挙動を切り替えたい箇所がある場合、そこにFlipper.enabled?(:hoge)
を埋め込みます。
(ユーザー単位で有効/無効が設定されているフラグの場合は、Flipper.enabled?(:hoge, user)
)例1
ログイン中ユーザに対して、フラグ
:hoge
が有効な場合だけ、特定のコードを実行するif Flipper.enabled?(:hoge, current_user) # 実行したいコード end例2
ログイン中ユーザに対して、フラグ
:hoge
が有効な場合だけ、特定のactionを実行可能とするclass FugaController < ApplicationController before_action -> { require_feature(:hoge, current_user) }, only: :show def show # 省略 head :ok end end class ApplicationController < ActionController::API private def require_feature(flag_name, user) return if Flipper.enabled?(flag_name, user) raise Forbidden, '実行権限がありません' end endフラグの有効化
フラグ
:hoge
を有効化するには下記のようにします。
※rails consoleで直接叩いても、rake taskでも、controllerでも何処に書いてもいいです。全てのユーザで有効化する
consoleuser = User.find(...) # 有効化前は無効 Flipper.enabled?(:hoge) => false Flipper.enabled?(:hoge, user) => false # 有効化 Flipper.enable(:hoge) # (ユーザに関係なく)有効になる Flipper.enabled?(:hoge) => true Flipper.enabled?(:hoge, user) => trueユーザ単位で有効化する
特定のユーザのみでフラグを有効化したい場合
consoleuser = User.find(...) other_user = User.find(...) # 有効化前は無効 Flipper.enabled?(:hoge) => false Flipper.enabled?(:hoge, user) => false # userに対して有効化 Flipper.enable_actor(:hoge, user) # 有効化したuserのみ有効になっている Flipper.enabled?(:hoge) => false Flipper.enabled?(:hoge, user) => true Flipper.enabled?(:hoge, other_user) => falsegroup単位で有効化する
特定の条件に該当するユーザたち(group)のフラグを一括で有効かしたい場合
まず、groupを予め定義しておく必要があります。
config/initializers/flipper.rb
に以下のような定義を書きました。(他に良い場所が思いつかなかったのですが、もし別案あったら追記します)config/initializers/flipper.rb# groupを定義します Flipper.register(:admins) do |actor, context| actor.respond_to?(:admin?) && actor.admin? end次に、groupに対してフラグを有効化
consoleuser = User.find(...) admin = User.find(...) # 有効化前は無効 Flipper.enabled?(:hoge, user) => false Flipper.enabled?(:hoge, admin) => false # groupに対してフラグを有効化 Flipper.enable_group(:hoge, :admins) # groupに属するuserのみ有効になる Flipper.enabled?(:hoge, user) => false Flipper.enabled?(:hoge, admin) => trueconsole上ではなく、initializersで、フラグを有効化してしまいたい場合の書き方
config/initializers/flipper.rb
でフラグの有効化まで行いたい場合は次のように書きます。config/initializers/flipper.rbrequire 'flipper' require 'flipper/adapters/active_record' Flipper.configure do |config| config.default do adapter = Flipper::Adapters::ActiveRecord.new # アダプタをActiveRecordアダプタとする Flipper.new(adapter) end end # groupを定義します Flipper.register(:admins) do |actor, context| actor.respond_to?(:admin?) && actor.admin? end # フラグの有効化 if ActiveRecord::Base.connection.data_source_exists? 'flipper_features' Flipper.enable_group(:hoge, :admins) end最後3行がフラグの有効化をしているところです。
ifブロックはハックなのですが、initializerでフラグを有効化しようとすると、アダプタ用のテーブルがセットアップされていない場合、「そんなテーブルはない」と言われてしまうため、このようなifブロックで囲んでおく必要があります。アダプタについて
initializerでは、flipperで設定したフラグの値を保持するために使うアダプタを設定します。
アダプタには、Redis
や、Active Record
などが指定できます。flipper本体には、メモリアダプタというアダプタが内蔵されているので、
gemflipper-active_record
などを入れなくても、下記のような設定が可能です。config/initializers/flipper.rbrequire 'flipper' Flipper.configure do |config| config.default do adapter = Flipper::Adapters::Memory.new # アダプタをメモリアダプタとする Flipper.new(adapter) end endただし、これだとフラグが永続化されません
つまり、consoleなどでフラグを有効化をしても、有効なのはコンソール上だけで、apiなどからフラグを参照した場合は、無効のままになってしまいます。
公式でも、アダプタには「アクティブレコードアダプタのような永続的な物を強く推奨します」と書かれていました。
どんなアダプタがあるか
こちらのページにある通り、以下のようなadapterがサポートされているようです
- ActiveRecord adapter
- ActiveSupportCacheStore adapter
- Cassanity adapter
- Http adapter
- memory adapter
- Moneta adapter
- Mongo adapter
- PStore adapter
- read-only adapter
- Redis adapter
- Sequel adapter
- Community Sup
公式ドキュメントには、あるアダプタを使っていて途中から、別のアダプタに乗り換えたい時の手順についても書かれています。
さいごに
flipperをかんたんに紹介しました。
他に、ユーザのうち指定の割合だけフラグを有効化するという機能(A/Bテストやカナリアリリースを想定したような機能)もあるようです。
また、フラグの確認や設定のためのwebインターフェースや、apiも用意されています。
まだ、そういった機能は使っていませんが、必要に応じて、他の機能も調べたり試したりしてみようと思います。
- 投稿日:2020-08-09T16:03:09+09:00
【Rails】FlipperでFeature Flagを導入
railsで作ったapiの利用可否を切り替えられるように、feature flag(機能フラグ)を導入したいということで、flipperというgemを使いました。
環境は、rails6.0.3 apiモードです。feature flag(機能フラグ)とは
アプリケーションの各種機能の有効無効を、コードはそのままに、再デプロイもせずに、実行中のプログラムの外部から切り替えるための仕組みです。
少し細かく書くと、
- ある機能の使用可否を判定するフラグを用意でき、
- フラグのON/OFFは、ユーザ全体だけでなく、特定のユーザや、ユーザのグループ単位でも行える
- フラグのON/OFFは、コードの変更なしに、railsコンソール上や、外部からweb UIやapiで行える
といったことを実現するものです。
gemの導入手順
- まず、Gemfileに下記を設定します。
Gemfilegem 'flipper' # flipper本体 gem 'flipper-active_record' # フラグを永続化するため※フラグの永続化については「アダプタについて」のところで書きます
- インストールします。
$ bundle install $ rails g flipper:active_record $ rails db:migrate
- 次に、
config/initializers
ディレクトリにflipper.rb
を追加config/initializers/flipper.rbrequire 'flipper' require 'flipper/adapters/active_record' Flipper.configure do |config| config.default do adapter = Flipper::Adapters::ActiveRecord.new # アダプタをActiveRecordアダプタとする Flipper.new(adapter) end endfeature flagの使い方
たとえば、フラグ
:hoge
があって、このtrue/falseに応じて挙動を切り替えたい箇所がある場合、そこにFlipper.enabled?(:hoge)
を埋め込みます。
(ユーザー単位で有効/無効が設定されているフラグの場合は、Flipper.enabled?(:hoge, user)
)例1
ログイン中ユーザに対して、フラグ
:hoge
が有効な場合だけ、特定のコードを実行するif Flipper.enabled?(:hoge, current_user) # 実行したいコード end例2
ログイン中ユーザに対して、フラグ
:hoge
が有効な場合だけ、特定のactionを実行可能とするclass FugaController < ApplicationController before_action -> { require_feature(:hoge, current_user) }, only: :show def show # 省略 head :ok end end class ApplicationController < ActionController::API private def require_feature(flag_name, user) return if Flipper.enabled?(flag_name, user) raise Forbidden, '実行権限がありません' end endフラグの有効化
フラグ
:hoge
を有効化するには下記のようにします。
※rails consoleで直接叩いても、rake taskでも、controllerでも何処に書いてもいいです。全てのユーザで有効化する
consoleuser = User.find(...) # 有効化前は無効 Flipper.enabled?(:hoge) => false Flipper.enabled?(:hoge, user) => false # 有効化 Flipper.enable(:hoge) # (ユーザに関係なく)有効になる Flipper.enabled?(:hoge) => true Flipper.enabled?(:hoge, user) => trueユーザ単位で有効化する
特定のユーザのみでフラグを有効化したい場合
consoleuser = User.find(...) other_user = User.find(...) # 有効化前は無効 Flipper.enabled?(:hoge) => false Flipper.enabled?(:hoge, user) => false # userに対して有効化 Flipper.enable_actor(:hoge, user) # 有効化したuserのみ有効になっている Flipper.enabled?(:hoge) => false Flipper.enabled?(:hoge, user) => true Flipper.enabled?(:hoge, other_user) => falsegroup単位で有効化する
特定の条件に該当するユーザたち(group)のフラグを一括で有効かしたい場合
まず、groupを予め定義しておく必要があります。
config/initializers/flipper.rb
に以下のような定義を書きました。(他に良い場所が思いつかなかったのですが、もし別案あったら追記します)config/initializers/flipper.rb# groupを定義します Flipper.register(:admins) do |actor, context| actor.respond_to?(:admin?) && actor.admin? end次に、groupに対してフラグを有効化
consoleuser = User.find(...) admin = User.find(...) # 有効化前は無効 Flipper.enabled?(:hoge, user) => false Flipper.enabled?(:hoge, admin) => false # groupに対してフラグを有効化 Flipper.enable_group(:hoge, :admins) # groupに属するuserのみ有効になる Flipper.enabled?(:hoge, user) => false Flipper.enabled?(:hoge, admin) => trueconsole上ではなく、initializersで、フラグを有効化してしまいたい場合の書き方
config/initializers/flipper.rb
でフラグの有効化まで行いたい場合は次のように書きます。config/initializers/flipper.rbrequire 'flipper' require 'flipper/adapters/active_record' Flipper.configure do |config| config.default do adapter = Flipper::Adapters::ActiveRecord.new # アダプタをActiveRecordアダプタとする Flipper.new(adapter) end end # groupを定義します Flipper.register(:admins) do |actor, context| actor.respond_to?(:admin?) && actor.admin? end # フラグの有効化 if ActiveRecord::Base.connection.data_source_exists? 'flipper_features' Flipper.enable_group(:hoge, :admins) end最後3行がフラグの有効化をしているところです。
ifブロックはハックなのですが、initializerでフラグを有効化しようとすると、アダプタ用のテーブルがセットアップされていない場合、「そんなテーブルはない」と言われてしまうため、このようなifブロックで囲んでおく必要があります。アダプタについて
initializerでは、flipperで設定したフラグの値を保持するために使うアダプタを設定します。
アダプタには、Redis
や、Active Record
などが指定できます。flipper本体には、メモリアダプタというアダプタが内蔵されているので、
gemflipper-active_record
などを入れなくても、下記のような設定が可能です。config/initializers/flipper.rbrequire 'flipper' Flipper.configure do |config| config.default do adapter = Flipper::Adapters::Memory.new # アダプタをメモリアダプタとする Flipper.new(adapter) end endただし、これだとフラグが永続化されません
つまり、consoleなどでフラグを有効化をしても、有効なのはコンソール上だけで、apiなどからフラグを参照した場合は、無効のままになってしまいます。
公式でも、アダプタには「アクティブレコードアダプタのような永続的な物を強く推奨します」と書かれていました。
どんなアダプタがあるか
こちらのページにある通り、以下のようなadapterがサポートされているようです
- ActiveRecord adapter
- ActiveSupportCacheStore adapter
- Cassanity adapter
- Http adapter
- memory adapter
- Moneta adapter
- Mongo adapter
- PStore adapter
- read-only adapter
- Redis adapter
- Sequel adapter
- Community Sup
公式ドキュメントには、あるアダプタを使っていて途中から、別のアダプタに乗り換えたい時の手順についても書かれています。
さいごに
flipperをかんたんに紹介しました。
他に、ユーザのうち指定のパーセントの任意のユーザのフラグを有効化するという機能(A/Bテストやカナリアリリースを想定したような機能)もあるようです。
また、フラグの確認や設定のためのwebインターフェースや、apiも用意されています。
まだ、そういった機能は使っていませんが、必要に応じて、他の機能も調べたり試したりしてみようと思います。
- 投稿日:2020-08-09T16:01:55+09:00
Railsのデータの型について
Railsのデータの種類について
データ型のカラムの種類を何を使っていいのだろうかとよく考える事があったので備忘録。
migrationに追加できるカラム
型 説明 用途 string 文字(255文字まで) 名前やメールアドレスなど短い文字 text 長い文章 ブログの記事や投稿される内容 integer 整数(4byte) 通常使用する範囲の整数で保存したいとき smallint 整数(2byte) 狭範囲の整数で保存するとき bigint 整数(8byte) かなり大きな整数で保存する可能性があるとき float 浮動小数点数 小数点を含めた数値を保存したいとき numeric 固定長整数 桁の大きな数を桁数などを指定して保存したいとき boolean 真か偽か trueかfalseで保存したいとき time 時刻型 時刻を「12:00:00」の形で保存したいとき date 日付型 日付を「2001-01-01」の形で保存したいとき datetime 日時型 日時を「2001-01-01 01:01:00」の形で保存したいとき json json型 JSONとして保存したいとき binary バイナリ文字列型 画像ファイルなどバイナリデータとして保存したいとき references 外部キーの定義 外部キーを追加したいとき 以上
- 投稿日:2020-08-09T15:29:19+09:00
リファクタリングしてみた①
バージョン
・ruby 2.5.7
・Rails 5.2.4.3ポートフォリオのコードをリファクタリング
まず、現状はというと下記の通り。ひどい、、。ほぼ同じ事を書いているアクション名が8つ存在している。まず、そのdate1からdate8のアクションをリファクタリングしてみる。
それぞれの処理で分ける処理は何か?同じ処理は何か?の視点で考える
まずこのdate1からdate8のアクションの処理内容を大まかに分類すると、
date1は新しくレコードをnewする処理。
date2からdate7は新しくレコードをnewする処理+前のページから渡ってきたカラムを受け取って、レコードを保存する処理。
date8は前のページから渡ってきたカラムを受け取って、レコードを保存する処理+また別のレコードを保存する処理。
なので、date2〜date7はまとめれそうだと考えた。同じ処理をまとめるにはどうしたらいいか
今回の場合、リファクタリングしたくてもそれを阻んでいたのはこのpageカラムの存在。
date2~7はpageの値が変わっているだけで、後の処理はredirect先が違うくらいでコードは一緒だった。
ページ遷移するごとに値を増やしていく
pageの値を自分で書くのではなく、ページが変わるごとに値が+1ずつされるようにコードかけばいいのではと考えた。
その為にまずdate1で@conto_page = 1とインスタンス変数を作る。
次に、ビュー側で値を受取り、form_withでconto_page: @conto_pageでdate2に@conto_pageを渡す。
最後に、受け取ったparams[:conto_page].to_i == 8 ならdate8に行くようにredirectして、それ以外ならconto_pageが+1ずつ増えて行く処理をして
リファクタリング完成!
無事にdate3~date7のアクションは消す事が出来ました。
さっきよりコード量が4割ほど削減が出来ました。
- 投稿日:2020-08-09T15:19:49+09:00
Ruby on RailsにReactとReduxの環境を標準のWebpackで構築する
概要
Ruby on Rails上でReactを利用する場合、通常はGemのWebpackerを利用します。
ただ、アセットパイプライン上でのビルドが遅かったり、
Webpackのカスタマイズ方法が特殊だったりで使いずらいので、
Gemを使わずに設定します。今回は標準のWebpackを導入した上で、Reactとの繋ぎ込みを目的としています。
Rails側から呼び出すコンポーネントを管理したいため、
ストラクチャ設計が分かりやすいReduxのRe-ducksパターンを採用した方法で構築を行っていきます。また、ReactとReduxについての設定方法がSPAを全体としたものが多いですが、
Railsに実装する場合は既存プロジェクトにページ毎にReactを使うことが多いため、
Railsでルーティングを管理する場合での使用方法を書いていきます。スペック
- CentOS7
- Ruby 2.7.1p83
- Ruby on Rails 5.2.4
- node js 13.14.0
- react 16.12.0
今回はWebpackでAssetを用意して、ファイルを読み込むだけの設計になるため、Ruby on Rails6でも同様に設定できるかと思います。
手順
インストール
今回はRuby, Railsを使っている人がReactを実装することを想定しているため、インストール方法を省きます。
NodeJSとYarnをインストールします
curl -sL https://rpm.nodesource.com/setup_8.x | sudo bash - sudo yum install nodejs npm install -g yarn
WebpackやReact、Redux等のパッケージをインストールします。
その他Reduxの運用に必要なパッケージやビルド用のLoaderをインストールしていきます。yarn add webpack webpack-cli webpack-manifest-plugin yarn add react react-redux redux-logger redux-thunk reselect axios yarn add babel-core babel-preset-react babel-preset-es2015 babel-loader yarn add typescript ts-loader yarn add node-sass sass-loader postcss-loader style-loader mini-css-extract-plugin yarn add file-loader expose-loader url-loader yarn add thread-loader hard-source-webpack-plugin
作成されたpackage.jsonにscriptsを書き込みます。
package.json{ ... }, "scripts": { "webpack-dev": "webpack --watch --progress --mode=development --config webpack.config.js", "webpack-build": "webpack --mode=production", "webpack-clear": "rm ./public/assets/*", "cache-clear": "rm ./node_modules/.cache/hard-source/*" } }これでWebpackのビルドができます。
webpack-dev
を利用すれば開発中にwatchした状態で利用できます。yarn webpack-dev
Webpackの設定
まずは必要なモジュールを読み込みます。
webpack.config.jsconst webpack = require('webpack'); const ManifestPlugin = require('webpack-manifest-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');次にパッケージ化するファイルを指定します。
Webpack管理下のディレクトリはapp/frontend/
以下にします。
また、app/frontend/containers/
以下にReactのコンテナー(ルートコンポーネント)を
app/frontend/packs/
以下に通常利用できるJS等を配置します。ビルドしたファイルは
public/assets/
以下に作成します。webpack.config.jsconst path = require('path'); const glob = require('glob'); const glob_path = './app/frontend/{containers,packs}/**/*.{js,jsx,ts,tsx,css,scss,sass}'; const entries = Object.fromEntries(glob.sync(glob_path).map((f) => ([f.split('/').reverse()[0].split('.')[0], f]))); const outputs = { filename: "[name]-[hash].js", path: path.join(__dirname, 'public', 'assets'), publicPath: "/" };loaderとモジュールの設定を行います。
Babel(JS)、Typescript、Sass等のローダーの設定をファイル拡張子によって作成します。ManifestPluginを利用することで、
manifest.json
でファイルを管理でき、
Rails等で利用しやすくなります。
MiniCssExtractPluginではCSSファイルの圧縮を行います。
HardSourceWebpackPluginはビルド時にパッケージ等をキャッシュしてくれるため、ファイルの変更時などにビルドが早くなります。純正のWebpackの設定のため、Webpackerと比べて設定方法については検索しやすいと思います。
webpack.config.jsmodule.exports = { entry: entries, output: outputs, module: { rules: [ { test: /\.(js|jsx)$/, loader: 'babel-loader', query: { presets: ['react', 'es2015'] } }, { test: /\.(ts|tsx)$/, use: [ { loader: 'thread-loader', options: { workers: require('os').cpus().length - 1 } }, { loader: 'ts-loader', options: { transpileOnly: true, happyPackMode: true } } ] }, { test: /\.css$/, use:['style-loader', 'css-loader'] }, { test: /\.scss$/, use: [ { loader: process.env.NODE_ENV !== 'production' ? 'style-loader' : MiniCssExtractPlugin.loader}, { loader: 'css-loader',}, { loader: 'postcss-loader', options: { plugins: function () { return [ require('precss'), require('autoprefixer') ]; } } }, { loader: 'sass-loader'} ] }, { test: /.(png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)$/, use: "url-loader?limit=100000" }, ], }, resolve: { modules:[path.join(__dirname, 'node_modules')], extensions: ['.js', '.jsx', '.ts', '.tsx'] }, plugins: [ new ManifestPlugin({ writeToFileEmit: true }), new MiniCssExtractPlugin({ filename: '[name].css' }), new HardSourceWebpackPlugin(), ], };Typescriptの設定
Typescriptを導入したため、設定ファイルも用意しておきます。
ReduxのAction等でTypescriptの構文など使いたいため、最低限の設定を施しておく。いきなり、Anyタイプを禁止してしまうと、ReactとRedux部分のクラスとか調べるのが大変なため、
noImplicitAny: false
で一旦許可しておく。tsconfig.json{ "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "noImplicitAny": false, "allowJs": true, "skipLibCheck": true, "module": "esnext", "jsx": "react" }, "include": [ "app/frontend" ] }Rails Helperの設定
Rails側からのコンポーネントの呼び出しはWebpacker Gemがやっていてくれていたが、
今回は導入しないため、自分でHelperを用意する。
webpack_asset_path
では、app/frontend/packs/
以下からファイルのパスが取得できる
react_component
では、app/frontend/components/
以下からReactのコンテナーが取得できるapp/helpers/react_helper.rbmodule ReactHelper # /app/frontend/packs/ def webpack_asset_path(file_name) @webpack_manifest ||= JSON.parse(File.read("public/assets/manifest.json") if @webpack_manifest.has_key?(file_name) "/assets#{manifest.fetch(file_name)}" else raise Exception.new("not found #{file_name} in manifest.") end end # /app/frontend/components/* def react_component(name, **props) id_name = "#{name}-container" js_file = "#{name.underscore}_container.js" props[:flash] = flash.presence&.to_h || {} valid_file?(js_file) content_tag :section do concat(content_tag(:div, "", id: id_name, data: {react_props: props})) concat(javascript_include_tag(asset_bundle_path(js_file))) end end endReact側の設定
これまでの設定から、ストラクチャ設計は以下の通りにする。
./app └── frontend ├── rails_container.tsx ├── packs │ └── application_pack.js ├── containers │ └── sample │ └── sample_container.js ├── components │ └── sample │ └── sample_component.tsx ├── reducks │ └── samples │ ├── actions.js │ ├── index.js │ ├── operations.js │ ├── reducers.ts │ ├── selectors.js │ ├── store.js │ └── types.ts └── stylesheets └── application.scssRailsのヘルパーで作ったIDにDOMでReactコンポーネントを作成しなければならないため、
関数を作ってコンポーネントを作成できるようにします。SPAとは違い、ページ毎に使用するオブジェクトは異なるため、
ビルドのコスト等を考慮して、コンテナー毎にReducerが指定できるようにします。app/frontend/rails_container.jsimport { render } from 'react-dom'; import { Provider } from 'react-redux'; import { createStore, combineReducers } from 'redux'; import logger from 'redux-logger'; import thunk from 'redux-thunk'; type Component = React.FC<any> | React.ComponentClass<any>; type Reducers = { [key: string]: any }; // ./app/helpers/react_helper.rb: ReactHelper.react_component(name, **props) export default function railContainer(name: string, component: Component, reducers: Reducers) { const app: HTMLElement | null = document.getElementById(name + '-container'); if (app) { const store = createStore( combineReducers(reducers), applyMiddleware(logger, thunk) ); const reactProps: object = JSON.parse(app.dataset.reactProps || "{}"); render( <Provider store={store}> {React.createElement(component, {railsProps: reactProps})} </Provider>, app ); } else { console.log("not found "+name+" container"); } }コンテナーで使用する、コンポーネントとReducerを読み込んで、先ほど作成した関数でコンテナーを作成します。
app/frontend/containers/sample/sample_containers.jsimport railsContainer from "../../lib/rails_container"; import { SamplesReducer } from "../../reducks/samples/reducers"; import { SampleComponent } from "../../components/sample_component"; railsContainer("SampleComponent", SampleComponent, { samples: SamplesReducer, });railsContainerで設定したコンポーネントにはPropsの中に
railsProps
が入っています。
Rails側からHashで渡したものがJS側のObjectで取得できます。Re-ducksパターンにおいてOperationsはActionを呼び出す役割なので、
dispatch(mountSamples(props.railsProps))
でStoreにデータを設定できます。
useEffect(()=>{}, [])
を使うことでコンポーネント起動時に実行されるため、
コンテナーを呼び出す際にRailsで渡した値を読み込むことができます。
(componentDidMount
と同じです。)app/frontend/components/sample/sample_component.tsximport React, { useEffect } from 'react'; import { useDispatch, useSelector } from "react-redux"; import { mountSamples } from "../../reducks/samples/operations"; import { getSamplesState } from "../../reducks/samples/selectors"; import { Samples } from "../../reducks/samples/types"; import { ChildComponent } from "./child_component"; type Props = { railsProps: { samples: Samples, }, } const SampleComponent: React.FC<Props> = (props) => { const dispatch = useDispatch(); const selector = useSelector(state => state); const samples = getSamplesState(selector); useEffect(() => { dispatch(mountSamples(props.railsProps)); }, []); return ( <ChildComponent/> ) } export default SampleComponent;補足
React Hooks, Re-ducksパターンについて
上記ではReact Hooksを使った実装をしていますが、
Re-ducksパターンやReact Hooksの説明はトラハックさんのYoutubeが分かりやすいと思います。
https://www.youtube.com/watch?v=FBMA34gUsgw&list=PLX8Rsrpnn3IWavNOj3n4Vypzwb3q1RXhrビルドされたファイル
yarn webpack-build
等でビルドすると、public/assets/
以下にapp/frontend/packs/
app/frontend/containers
以下に配置したファイルがビルドされていることが分かると思います。この2つのディレクトリが必ずJSのプログラムの開始位置になると覚えておくと、コードの管理が楽になるかと思います。
public/assets/ ├── manifest.json ├── application_pack-1732212f4623eefd2c49.js └── sample_container-1732212f4623eefd2c49.jshard-source-webpack-pluginについて
今回RailsにWebpackerを使わないで、標準のWebpackで設定する方法を選んだのは、
コードが膨らんできてビルド時間が遅くなってしまう問題を抱えていたからでした。Railsのアセットパイプラインから切り離して体感的にはビルドが早くなったのですが、あと一押し欲しい感じでした。
hard-source-webpack-pluginはキャッシュを作成することでビルド時間の短縮を図れるため、使用しました。
watchモードで開発する際などにキャッシュが効いていると、毎回のようにビルドされていたのが短縮されるのでとても便利です。たまにキャッシュが悪さして上手くビルドができなくなる時があるらしいので、
その場合はnode_modules/.cache/hard-source/
以下のキャッシュデータを削除する方法が推奨されています。
(今回はyarn cache-clear
でコマンドを用意しました。)参考
- 投稿日:2020-08-09T14:55:41+09:00
Active Recordのバリデーション機能
バリデーション
データベースに書き込まれる前に実行される、モデルの状態の検証(バリデーション)のこと。
使うには
Active Recordのモデルを作成する。 下記のように
ApplicationRecord
のサブクラスを作成する。class User < ApplicationRecord validates :name, presence: true endまた、
ActiveModel::Validations
モジュールを追加すると、クラスオブジェクトをActive Recordスタイルで検証できる。class User include ActiveModel::Validations attr_accessor :name validates :name, presence: true end基本の使い方
Userクラスを作成、テーブルを作成します。
rails generate model User name:string email:string age:integer rails db:migrateUserクラスにバリデーションを追加
class User < ApplicationRecord validates :name, presence: true # 追記 end
!
が末尾に付く破壊的メソッド(save!など)では、レコードが無効な場合に例外が発生する。user = User.new user.save # => false user.save! # => ActiveRecord::RecordInvalid (バリデーションに失敗しました: Nameを入力してください)
valid?
メソッドでバリデーションを手動でトリガすることも出来る。
オブジェクトでエラーが発生した場合はfalse
を返し、そうでない場合はtrue
を返す。
invalid?
はvalid?
の逆で、バリデーションをトリガし、オブジェクトでエラーが発生した場合はtrue
を返す。user = User.new user.valid? # => false user.errors.messages # => {:name=>["を入力してください"]} user.invalid? # => trueバリデーションヘルパー
クラス定義の内側で直接使える定義済みのヘルパーで、共通のバリデーションルール。
バリデーションが失敗するたびに、オブジェクトのerrorsコレクションにエラーメッセージが追加され、そのメッセージは、バリデーションが行われる属性に関連付けられる。
Railsガイドのバリデーションヘルパーを読むほうが早いデス。validates :name, presence: true validates :name, length: { maximum: 20 } validates :name, length: { in: 6..20 } validates :age, numericality: { only_integer: true }共通のバリデーションオプション
:allow_nil
と:allow_blank
:allow_nil
はnilの場合はバリデーションをスキップ
:allow_blank
はblank?
に該当する場合はバリデーションをスキップvalidates :address, length: { maximum: 255 }, allow_nil: true validates :address, length: { maximum: 255 }, allow_blank: true
:on
バリデーション実行のタイミングを指定できる。
validates :address, presence: true, on: :create
:on
とカスタムコンテキスト
valid?
、invalid?
、save
にcontext: コンテキスト名
と指定することができる。class User < ApplicationRecord validates :address, presence: true, on: [:create, :account_setup] # :onは複数指定も出来る end user = User.new user.save(context: :account_setup)条件付きバリデーション
:if
オプション、:unless
オプションで条件が指定できる。
引数にはシンボル、Proc
、Array
が使える。class User < ApplicationRecord validates :address, presence: true, if: :within_japan? def within_japan? country == 'japan' end endprocを使う
条件付きバリデーションをワンライナーで書くことも出来るので便利
validates :address, length: { maximum: 255 }, if: proc {|u| u.address.present? }参考
- 投稿日:2020-08-09T14:46:54+09:00
【Rails】summernote 本番環境で画像保存ができない時の解決方法
はじめに
某スクールの学習過程ポートフォリオ作成の際に、Gem 'summernote'を用いて
リッチテキストエディタを実装しました。開発環境上では画像を含めたテキストの投稿が可能でしたが、
デプロイ後の本番環境にて画像を投稿しようとすると下記エラーが発生し、投稿保存がされませんでした。(0.5ms) ROLLBACK Completed 500 Internal Server Error in 46ms (ActiveRecord: 12.9ms) ActiveRecord::ValueTooLong (Mysql2::Error: Data too long for column 'article_content' at row 1: INSERT INTO `articles` (`admin_id`, `article_title`, `article_content`, `article_image_id`, `created_at`, `updated_at`) VALUES (1, '観葉植物の置き場所はどこがいいか', '<p><img src=\"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QB+RXhpZgAATU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAABJADAAIAAAAUAAAAUJKGAAcAAAASAAAAZKACAAQAAAABAAA...(以下長すぎるので省略します)解決方法
【前提】
・テーブル名はArticleとしてます。(Article=記事の意)。この部分は任意の名前にしてください。①エラー文の解釈
まず、ターミナルに表示されていたActiveRecord::ValueTooLong (Mysql2::Error: Data too long for column...)このエラー文が解決への糸口になります。
簡潔にいうと、「データが長すぎ」と言われています。
投稿文を保存するarticle_contentのデータ型をtextにしていましたが、これでもまだ足りないと言われているわけです。
代表的なデータ型として短文(1〜255文字)はstring型、長文()はtext型を使うという程度の知識でした。②変更すべき点
上記内容で、「データが長すぎて保存できない」という問題点がわかりました。
そうしたら収まるように大容量用のデータ型に変更してあげればいいだけです!
下記記述が修正を加えたマイグレーションファイルになります。(date)_change_column_to_article.rbclass ChangeColumnToArticle < ActiveRecord::Migration[5.2] def up change_column :articles, :article_content, :text, null: false, limit: 4294967295 end def down change_column :articles, :article_content, :text, null: false end end※以下、修正内容についての詳細です。
デプロイの際にMySQL
MySQLにあるデータ型は以下の通りです。
データ型 解説 CHAR 255Bまでの固定長文字列 VARCHAR 64KBまでの可変長文字列 TINYTE 255Bまでの可変長文字列 TEXT 64KBまでの可変長文字列 MEDIUMTEXT 1.6MBまでの可変長文字列 LONGTEX 4.3GBまでの可変長文字列 この中のLONGTEXTを使用する事にしました。(最大にしておけば足りなくなることはまずないだろうという考え)
ただし前述した通り、Railsの学習をした際にstringとtextくらいしか学習した記憶がなかったため設定方法がわかりませんでした。調べた結果、結構簡単でリミットをつけるだけでした。
まずカラムに変更を加えるため、以下のようにします。ターミナル$ rails g migration ChangeColumnToArticleすると、以下の内容でマイグレーションファイルが作成されます。
日付_change_column_to_Article.rbclass ChangeColumnToArticle < ActiveRecord::Migration[5.2] def change endそして、変更前の記載と変更後の記載を加えます。
ここの記述は冒頭に記載したものと同じになります。日付_change_column_to_article.rbclass ChangeColumnToArticle < ActiveRecord::Migration[5.2] ※変更を加えた記述 def up change_column :articles, :article_content, :text, null: false, limit: 4294967295 end ※変更前の記述 def down change_column :articles, :article_content, :text, null: false end endリミットを書き加えることで、LONGTEXT型にします。
なお、4294967295と言う数字は4294967295バイト(4.3GB)でLONGTEXT型の上限です。
limitの範囲を16777216 ~ 4294967285にするとLONGTEXT型になるそうです。ターミナル$ rails db:migrate変更を反映させて終了です。
そもそも、なぜ開発環境では使えて、本番環境でエラーが出たのか
本番環境ではMySQLを使用していて、MySQLのtext型には最大長が設定されており、半角で65,535文字を超える文字列は扱えません。単純なことでこれが開発環境と本番環境での違いによるエラーの発生の理由でした。(現在の知識・語彙力ではこれが限界です。いずれもっと詳しく解説を加えられたらと思います!)
- 投稿日:2020-08-09T12:48:31+09:00
【超初心者的】Railsアプリ作成 Part.3 ビューの設定(リンク編)
ここではビューをもう少しアプリっぽくしていきます。
詳細ページへのリンク
一覧ページの商品名をリンクにして、クリックすると商品詳細ページが表示されるようにします。
<td><%= menu.name %></td>
を書き換えます。app/views/menus/index.html.erb<td><%= link_to menu.name, menu %></td>http://localhost:3000/menus にアクセスしてみてください。商品名がリンクになっていると思います。また、クリックするとその商品の詳細(show)ページに遷移します。
リンクの仕組み
リンクの書式は下記のとおりです。
link_to 表示される文字, リンク先
「表示される文字」には文字を書いても構いませんが、データベースに登録されたデータがたくさんある場合、いちいち手入力するのは現実的ではありません。そのため「menu.name」でデータベースから商品名を読み込んでいます。
なお、前のPartで作成したindexビューでは、繰り返し処理を利用しています。下記に注目してください。
app/views/menus/index.html.erb<% @menus.each do |menu| %> <tr> <td><%= link_to menu.name, menu %></td> <td><%= menu.price %></td> <td><%= menu.description %></td> </tr> <% end %>
<% @menus.each do |menu| %>
と<%end>
で囲まれた部分が、繰り返し処理されます。この処理のおかげで、データが10件あっても100件あっても少ないコード量ですべてのデータを表示させることが出来ます。また、リンク先についてですが、ここでルーティングについて思い出してください。
ターミナルPrefix Verb URI Pattern Controller#Action menus GET /menus(.:format) menus#index POST /menus(.:format) menus#create new_menu GET /menus/new(.:format) menus#new edit_menu GET /menus/:id/edit(.:format) menus#edit menu GET /menus/:id(.:format) menus#show PATCH /menus/:id(.:format) menus#update PUT /menus/:id(.:format) menus#update DELETE /menus/:id(.:format) menus#destroyリンク先の指定には、Prefixのキーワードが指定出来ます。先述のリンク設定では「menu」を指定しているので、商品IDが「1」の商品であれば、「menus/1」へのリンクになります。おかげで商品名をクリックするとその商品の詳細ページに遷移することが出来ます。わざわざIDを調べる必要もありません。めちゃくちゃ簡単ですね。
編集ページへのリンク
詳細ページに編集ボタンを作成します。こちらもルーティングのPrefixで指定します。
app/app/views/menus/show.html.erb<%= link_to "編集", edit_menu_path %>詳細ページに追加されたリンクをクリックすると編集ページに遷移します。内容を変更して更新ボタンを押すと、ふたたび詳細ページに遷移します。変更した内容で表示されます。なお、編集ページの更新ボタンを押した際は、ルーティングの「PATCH」が呼び出されます。
新規作成ページへのリンク
一覧ページに新規登録ページへのリンクを作成します。やはりこちらもルーティングのPrefixで指定します。
app/views/menus/index.html.erb<%= link_to "新規作成", new_menu_path %>これでブラウザからデータベースにデータを新規登録出来るようになりました。
削除ボタン(リンク)
詳細ページに削除用のリンクを作成します。こちらは書き方が少し特殊です。
「method: :delete」と書くことで、ルーティングの「DELETE」を呼び出すことが出来ます。app/views/menus/index.html.erb<%= link_to "削除", @menu, method: :delete, data: { confirm: "本当に削除しますか?" } %>これでブラウザからデータベースのデータを新規削除出来るようになりました。
ここまでの設定でページ間のつながりが出来て、アプリっぽくなったのではないでしょうか。
- 投稿日:2020-08-09T10:22:11+09:00
Rails c Rollbackエラー原因を探る
- 投稿日:2020-08-09T09:53:22+09:00
Railsでのカラムの追加
カラムの追加
新しいカラムの追加(基本)
$rails g migration (行う処理+テーブル名)(追加したいカラムとデータ型など)
$rails g migration AddColumnToUsers name:string age:integer
新しいカラムにインデックスを追加
$rails g migration AddColumnToUsers name:string:index
カラムの削除
$rails g migration RemoveNameFromUsers name:string
テーブルの作成と同時にカラムの作成
$rails g migration CreateUsers name:string age:integer
- 投稿日:2020-08-09T02:10:56+09:00
[Rails][React]Module not found: Error: Can't resolve '../bundles/HelloWorld/components/HelloWorld' 解決法
起こったこと
既存のRails プロジェクトに
React on Rails
を入れる。
rails generate react_on_rails:install
を実行するもどうやら Webpackerのbuild が通らない。
ERROR in ./app/javascript/packs/hello-world-bundle.js Module not found: Error: Can't resolve '../bundles/HelloWorld/components/HelloWorld' in '/home/vagrant/dev/rails-proj/app/javascript/packs' @ ./app/javascript/packs/hello-world-bundle.js 2:0-69 5:14-24 ...解決法
どうやら別途、以下コマンドで設定ファイル生成が必要だったよう。
$ bin/rails webpacker:install:react Copying babel.config.js to app root directory force babel.config.js Copying react example entry file to /home/vagrant/dev/rails/udr-tinder/app/javascript/packs create app/javascript/packs/hello_react.jsx Updating webpack paths to include .jsx file extension insert config/webpacker.yml Installing all react dependencies run yarn add react react-dom @babel/preset-react prop-types babel-plugin-transform-react-remove-prop-types from "." ..参考
https://www.chrisblunt.com/rails-and-react-fixing-module-not-found-errors-for-jsx-files/
https://github.com/shakacode/react_on_rails/
https://shakacode.gitbooks.io/react-on-rails/content/docs/tutorial.html
https://www.botreetechnologies.com/blog/how-to-add-react-js-to-your-ruby-on-rails-app-with-webpacker
- 投稿日:2020-08-09T00:40:25+09:00
【Rails】Mongoidでembeds_manyの子要素の数を条件付き取得する
概要
以前に【Rails】Mongoidでembeds_manyの子要素の数を取得するの記事で、MongoDBにて配列要素の項目数を取得する方法を記載しました。今回は配列の項目数を取得するときに条件付きで取得する方法を書きます。
サンプル例題
前回の記事とほぼ同じですが、今回はCommentクラスに日付項目(created_at)を追加し、日付の条件で絞るような例題にします。
article.rbclass Article include Mongoid::Document field :_id, type: String field :title, type: String field :contents, type: String embeds_many :comments endcomment.rbclass Comment include Mongoid::Document field :user_id, type: String field :post_comment, type: String field :created_at, type: DateTime embedded_in :article end対応方法
MongoDBでaggregateのクエリ時に使用できる$filter句を使います。これで配列要素を、条件に一致したものみで抽出できます。
sample.rb# コレクションの取得 db = Mongoid::Clients.default articleCollection= db[:article] # 日付条件(30日前) before30day = Time.now.utc - 30.days # aggregateの実行 articleCollection.aggregate([ { "$project" => { _id: 1, title: 1, contents: 1, # 30日以内のもののみでカウント comments_count: { "$size": { '$filter' => { 'input' => '$comments', 'as' => 'comments', 'cond' => { '$gte' => ['$$comments.created_at', before30day] } } } } } } ]).to_a
- 投稿日:2020-08-09T00:35:33+09:00
[Rails]ActionDispatch::Cookies::CookieOverflow 解決法
起こったこと
Rails で 突然出るエラー。
ActionDispatch::Cookies::CookieOverflow
主な原因としては
session 変数に多くのデータを保持する場合、
や
flash など view 関連のhelper で Objectやtextなど多くの文字列
を渡している場合、
に出るよう。(CookieStore: session 情報を全てsecret_key_baseで暗号化し、クライアント側のCookie に保持。)
https://qiita.com/shota_matsukawa_ga/items/a21c5cf49a1de6c9561a
example.rbflash[:success] = User.inspect # こんな原因解決法
flash などhelper に渡すデータを減らす。(session に入れているため)
また、
session storeを変更するなど。参考
https://stackoverflow.com/questions/9473808/cookie-overflow-in-rails-application
https://site-builder.wiki/posts/4587
- 投稿日:2020-08-09T00:02:00+09:00
[Heroku]Cloudinary環境においてCarrierwaveで画像をアップロード
プログラミングの勉強日記
2020年8月9日 Progate Lv.226
前回の記事に引き続きHerokuを扱う。
前回までにRailsアプリケーションをHeroku上で公開することができた。目標
今回はすでにローカルでCarrierWaveを用いて画像の投稿ができるので、それをHeroku上でもできるようにする。
現在、Cloudinary環境においてCarrierwaveで画像をアップロードしようとすると以下のエラーが出てしまう。
「We're sorry, but something went wrong.
If you are the application owner check the los for more information.」
方法
0.5. Herokuにクレジットカードを登録する
Cloudinaryと連携するためには、クレジットカードを登録する必要がある。(無料で行える)
こちらからログインをする。以下の手順に従ってクレジットカードを登録する。
1. Account Settingsをクリック
2. Billingタブをクリックし、Add Credit Cardをクリック
3. カード情報を登録する
1. HerokuとCloudinaryを連携する
HerokuとCloudinaryを連帯するためのアドオン(追加機能)を追加する。Herokuのブラウザからでもコマンドからでも追加することができる。
コマンドで追加する場合
ターミナル$ heroku addons:add cloudinary:starterブラウザから追加する場合
こちらから登録する。
1. Install Cloudinaryをクリック
2. Add to provisio toにサービス名を入力し、Provision add-onをクリック
2. アップローダーファイルの編集
このアップローダーファイルはCarrieawaveを使って画像をアップロードするための設定ファイル。
アップローダー(app/uploaders/image_uploader.rb)#変更前 if Rails.env.production? include Cloudinary::CarrierWave CarrierWave.configure do |config| config.cache_storage = :file end else storage :file end #変更後 #if Rails.env.production? include Cloudinary::CarrierWave #CarrierWave.configure do |config| #config.cache_storage = :file #end #else #storage :file #end感想
Railsのバージョンが5.1.7だったので、バージョンをアップデートしたりmaster.keyを使ってみたり、Gemfileを変えたり、ビューファイルを変えてみたり、様々なことをしたが、クレカの登録が必要だった。
最初にターミナルでheroku addons:add cloudinary:starter
を実行したときに英語の文章をちゃんと読んでいなくてクレジットカードを登録する必要があるのにもかかわらず、それを飛ばしてしまってたのが大きな原因であった。
基本的には、ローカルでCloudinaryとCarrierWaveを用いて画像を投稿する機能ができていれば、上記のようにすればHeroku上で問題なく動いた。
ここまでするのに多くの時間を費やしてしまったが、勉強になった。参考文献
Heroku + Cloudinary環境でCarrierwaveで画像アップロード
We're sorry, but something went wrong.If you are the application owner check the logs for more information.でハマる。