20200809のRailsに関する記事は22件です。

[Rails]deviseを使ったウィザード形式での新規登録機能の実装

はじめに

フリマのコピーサイトを作る際に、構造をやや難しくしてしまったために苦労したので記録として残そうと思いました。もっと綺麗にかけるなどご指摘あればお願いします。

実装方法

開発環境

  • Ruby 2.5.1
  • Rails 5.0.7.2
  • devise

前提

DB設計

usersテーブル

Column Type Options
name string null: false, unique: true, index:true
email 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.rb
Rails.application.routes.draw do
  devise_for :users, controllers: { registrations: 'users/registrations' }
  root to: 'items#index'
end

モデルにアソシエーションを記述する

  • UserProfileSending_destinationそれぞれのモデルにアソシエーションを記述
    ※バリデーションは省略しています。
user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_one :profile
  accepts_nested_attributes_for :profile
  has_one :sending_destination
end
profile.rb
class Profile < ApplicationRecord
  belongs_to :user, optional: true
end
sending_destination.rb
class SendingDestination < ApplicationRecord
  belongs_to :user, optional: true
end

optional: trueは外部キーがnullであることを許可するオプションです。

newアクションと対応するビューを編集する(1ページ目)

  • users/registrations_controller.rbにnewアクションを記述
users/registrations_controller.rb
class 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.rb
class 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.rb
Rails.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.rb
class 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への代入にかなり四苦八苦しましたが、おかげでしっかり仕組みなどを考えることができました。細かな説明はしていませんが、アウトプットも兼ねて記事にしてみたので少しでも参考になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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:vue

Vue.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!"と表示されていれば、
導入と読み込みは上手くいっています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
end
class CreateArticles < ActiveRecord::Migration[6.0]
  def change
    create_table :articles do |t|
      t.string   :title
      t.string   :content
    end
  end
end
$ rake db:migrate

reference型のカラムの作成

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
end

reference型のカラムの追加

class AddReferenceColumn < ActiveRecord::Migration[6.0]
  def change
    add_reference :articles, :user, foreign_key: true
  end
end

この際にもforeign_key: trueを忘れずに。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初歩中の初歩!railsでのモデル・コントローラー作成方法

モデル作成方法

rails g モデル名(単数)

マイグレーション型一覧
・string : 文字列
・text : 長い文字列
・integer : 整数
・float : 浮動小数
・decimal : 精度の高い小数
・datetime : 日時
・timestamp : タイムスタンプ
・time : 時間
・date : 日付
・binary : バイナリデータ
・boolean : Boolean

class 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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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

以上。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コンテナ上のデバッグ環境構築 - 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;
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.rb
require 'active_record/connection_adapters/abstract_mysql_adapter'

module ActiveRecord
  module ConnectionAdapters
    class AbstractMysqlAdapter
      NATIVE_DATABASE_TYPES[:string] = { :name => "varchar", :limit => 191 }
    end
  end
end

2.データベースをリセット&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テーブルに反映する事ができた

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.rb
Rails.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:
    ~

上記のような形式で日本語を設定してけば良いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FlipperでFeature Flagを導入する

railsで作ったapiに、feature flag(機能フラグ)を導入したいという要件があり、flipperというgemを使いました。備忘として残します。
環境は、rails6.0.3 apiモードです。

feature flag(機能フラグ)とは

アプリケーションの各種機能の有効無効を、コードを変更したりブランチを切り替えて再デプロイすることなく実行中のプログラムの外部から切り替えることを目的とします。

具体的には、、

  • あるapiは、特定のユーザや、特定の条件に該当するユーザのみ使わせる
  • どのapiの使用を、どのユーザに許可するかは、コードを変更しなくても、railsコンソール上で切り替えたり、外部からweb UIやapiで切り替えられるようにしたい

といったことを実現できます。

gemの導入手順

  • まず、Gemfileに下記を設定します。
Gemfile
gem 'flipper'                 # flipper本体
gem 'flipper-active_record'   # フラグを永続化するため

※フラグの永続化については「アダプタについて」のところで書きます

  • インストールします。
$ bundle install
$ rails g flipper:active_record
$ rails db:migrate
  • 次に、config/initializers ディレクトリにflipper.rbを追加
config/initializers/flipper.rb
require 'flipper'
require 'flipper/adapters/active_record'

Flipper.configure do |config|
  config.default do
    adapter = Flipper::Adapters::ActiveRecord.new # アダプタをActiveRecordアダプタとする

    Flipper.new(adapter)
  end
end

feature 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でも何処に書いてもいいです。

全てのユーザで有効化する

console
user = User.find(...)

# 有効化前は無効
Flipper.enabled?(:hoge)
=> false
Flipper.enabled?(:hoge, user)
=> false

# 有効化
Flipper.enable(:hoge)

# (ユーザに関係なく)有効になる
Flipper.enabled?(:hoge)
=> true
Flipper.enabled?(:hoge, user)
=> true

ユーザ単位で有効化する

特定のユーザのみでフラグを有効化したい場合

console
user = 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)
=> false

group単位で有効化する

特定の条件に該当するユーザたち(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に対してフラグを有効化

console
user  = 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)
=> true

console上ではなく、initializersで、フラグを有効化してしまいたい場合の書き方

config/initializers/flipper.rbでフラグの有効化まで行いたい場合は次のように書きます。

config/initializers/flipper.rb
require '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本体には、メモリアダプタというアダプタが内蔵されているので、
gem flipper-active_recordなどを入れなくても、下記のような設定が可能です。

config/initializers/flipper.rb
require '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も用意されています。

まだ、そういった機能は使っていませんが、必要に応じて、他の機能も調べたり試したりしてみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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に下記を設定します。
Gemfile
gem 'flipper'                 # flipper本体
gem 'flipper-active_record'   # フラグを永続化するため

※フラグの永続化については「アダプタについて」のところで書きます

  • インストールします。
$ bundle install
$ rails g flipper:active_record
$ rails db:migrate
  • 次に、config/initializers ディレクトリにflipper.rbを追加
config/initializers/flipper.rb
require 'flipper'
require 'flipper/adapters/active_record'

Flipper.configure do |config|
  config.default do
    adapter = Flipper::Adapters::ActiveRecord.new # アダプタをActiveRecordアダプタとする

    Flipper.new(adapter)
  end
end

feature 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でも何処に書いてもいいです。

全てのユーザで有効化する

console
user = User.find(...)

# 有効化前は無効
Flipper.enabled?(:hoge)
=> false
Flipper.enabled?(:hoge, user)
=> false

# 有効化
Flipper.enable(:hoge)

# (ユーザに関係なく)有効になる
Flipper.enabled?(:hoge)
=> true
Flipper.enabled?(:hoge, user)
=> true

ユーザ単位で有効化する

特定のユーザのみでフラグを有効化したい場合

console
user = 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)
=> false

group単位で有効化する

特定の条件に該当するユーザたち(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に対してフラグを有効化

console
user  = 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)
=> true

console上ではなく、initializersで、フラグを有効化してしまいたい場合の書き方

config/initializers/flipper.rbでフラグの有効化まで行いたい場合は次のように書きます。

config/initializers/flipper.rb
require '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本体には、メモリアダプタというアダプタが内蔵されているので、
gem flipper-active_recordなどを入れなくても、下記のような設定が可能です。

config/initializers/flipper.rb
require '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も用意されています。

まだ、そういった機能は使っていませんが、必要に応じて、他の機能も調べたり試したりしてみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 外部キーの定義 外部キーを追加したいとき

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

リファクタリングしてみた①

バージョン

・ruby 2.5.7
・Rails 5.2.4.3

ポートフォリオのコードをリファクタリング

まず、現状はというと下記の通り。ひどい、、。ほぼ同じ事を書いているアクション名が8つ存在している。まず、そのdate1からdate8のアクションをリファクタリングしてみる。
スクリーンショット 2020-08-09 14 27 08
スクリーンショット 2020-08-09 14 27 26
スクリーンショット 2020-08-09 14 27 40

それぞれの処理で分ける処理は何か?同じ処理は何か?の視点で考える

まずこのdate1からdate8のアクションの処理内容を大まかに分類すると、
date1は新しくレコードをnewする処理。
date2からdate7は新しくレコードをnewする処理+前のページから渡ってきたカラムを受け取って、レコードを保存する処理。
date8は前のページから渡ってきたカラムを受け取って、レコードを保存する処理+また別のレコードを保存する処理。
なので、date2〜date7はまとめれそうだと考えた。

同じ処理をまとめるにはどうしたらいいか

今回の場合、リファクタリングしたくてもそれを阻んでいたのはこのpageカラムの存在。
date2~7はpageの値が変わっているだけで、後の処理はredirect先が違うくらいでコードは一緒だった。
スクリーンショット 2020-08-09 14 53 04

ページ遷移するごとに値を増やしていく

pageの値を自分で書くのではなく、ページが変わるごとに値が+1ずつされるようにコードかけばいいのではと考えた。
その為にまずdate1で@conto_page = 1とインスタンス変数を作る。
スクリーンショット 2020-08-09 15 12 13
次に、ビュー側で値を受取り、form_withでconto_page: @conto_pageでdate2に@conto_pageを渡す。
スクリーンショット 2020-08-09 15 18 46
最後に、受け取ったparams[:conto_page].to_i == 8 ならdate8に行くようにredirectして、それ以外ならconto_pageが+1ずつ増えて行く処理をして
スクリーンショット 2020-08-09 15 15 10

リファクタリング完成!

無事にdate3~date7のアクションは消す事が出来ました。
さっきよりコード量が4割ほど削減が出来ました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.js
const 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.js
const 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.js
module.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.rb
module 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
end

React側の設定

これまでの設定から、ストラクチャ設計は以下の通りにする。

./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.scss

Railsのヘルパーで作ったIDにDOMでReactコンポーネントを作成しなければならないため、
関数を作ってコンポーネントを作成できるようにします。

SPAとは違い、ページ毎に使用するオブジェクトは異なるため、
ビルドのコスト等を考慮して、コンテナー毎にReducerが指定できるようにします。

app/frontend/rails_container.js
import { 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.js
import 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.tsx
import 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.js

hard-source-webpack-pluginについて

今回RailsにWebpackerを使わないで、標準のWebpackで設定する方法を選んだのは、
コードが膨らんできてビルド時間が遅くなってしまう問題を抱えていたからでした。

Railsのアセットパイプラインから切り離して体感的にはビルドが早くなったのですが、あと一押し欲しい感じでした。
hard-source-webpack-pluginはキャッシュを作成することでビルド時間の短縮を図れるため、使用しました。
watchモードで開発する際などにキャッシュが効いていると、毎回のようにビルドされていたのが短縮されるのでとても便利です。

たまにキャッシュが悪さして上手くビルドができなくなる時があるらしいので、
その場合はnode_modules/.cache/hard-source/以下のキャッシュデータを削除する方法が推奨されています。
(今回はyarn cache-clearでコマンドを用意しました。)

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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:migrate

Userクラスにバリデーションを追加

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_blankblank?に該当する場合はバリデーションをスキップ

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?savecontext: コンテキスト名と指定することができる。

class User < ApplicationRecord
  validates :address, presence: true, on: [:create, :account_setup] # :onは複数指定も出来る
end

user = User.new
user.save(context: :account_setup)

条件付きバリデーション

:ifオプション、:unlessオプションで条件が指定できる。
引数にはシンボル、ProcArrayが使える。

class User < ApplicationRecord
  validates :address, presence: true, if: :within_japan?

  def within_japan?
    country == 'japan'
  end
end

procを使う

条件付きバリデーションをワンライナーで書くことも出来るので便利

validates :address, length: { maximum: 255 }, if: proc {|u| u.address.present? }

参考

Railsガイド Active Recordの基礎
Railsガイド Active Record バリデーション

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.rb
class 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.rb
class ChangeColumnToArticle < ActiveRecord::Migration[5.2]
  def change
  end

そして、変更前の記載と変更後の記載を加えます。
ここの記述は冒頭に記載したものと同じになります。

日付_change_column_to_article.rb
class 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文字を超える文字列は扱えません。単純なことでこれが開発環境と本番環境での違いによるエラーの発生の理由でした。(現在の知識・語彙力ではこれが限界です。いずれもっと詳しく解説を加えられたらと思います!)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【超初心者的】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: "本当に削除しますか?" } %>

これでブラウザからデータベースのデータを新規削除出来るようになりました。

ここまでの設定でページ間のつながりが出来て、アプリっぽくなったのではないでしょうか。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails c Rollbackエラー原因を探る

はじめに

開発初期段階で、usersテーブルのレコードを作成したくてrails consoleを使用。しかし、Rollbackのエラーが出てしまい保存ができない。その上、Rollbackの原因も明記されていない。これでは原因を探すのが手探りになってしまう。ということで、原因を楽に探す方法はないものかとググった結果ありました!なので、その方法の備忘録として書き記します。

方法

やり方はとてもシンプル!createメソッドの後ろに!をつけるだけ!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Mongoidでembeds_manyの子要素の数を条件付き取得する

概要

以前に【Rails】Mongoidでembeds_manyの子要素の数を取得するの記事で、MongoDBにて配列要素の項目数を取得する方法を記載しました。今回は配列の項目数を取得するときに条件付きで取得する方法を書きます。

サンプル例題

前回の記事とほぼ同じですが、今回はCommentクラスに日付項目(created_at)を追加し、日付の条件で絞るような例題にします。

article.rb
class Article
  include Mongoid::Document
  field :_id, type: String
  field :title, type: String
  field :contents, type: String
  embeds_many :comments
end
comment.rb
class 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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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.rb
flash[:success] = User.inspect # こんな原因

解決法

flash などhelper に渡すデータを減らす。(session に入れているため)

また、
session storeを変更するなど。

参考

https://stackoverflow.com/questions/9473808/cookie-overflow-in-rails-application

https://site-builder.wiki/posts/4587

http://kgmx.hatenablog.com/entry/2014/08/14/092710

https://qiita.com/kazuph/items/feb135cda2c6ece24574

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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.」
0808-5.PNG

方法

0.5. Herokuにクレジットカードを登録する

 Cloudinaryと連携するためには、クレジットカードを登録する必要がある。(無料で行える)

こちらからログインをする。以下の手順に従ってクレジットカードを登録する。

1. Account Settingsをクリック

0809-5.PNG

2. Billingタブをクリックし、Add Credit Cardをクリック

0809-7.PNG

3. カード情報を登録する

0809-6.PNG

1. HerokuとCloudinaryを連携する

 HerokuとCloudinaryを連帯するためのアドオン(追加機能)を追加する。Herokuのブラウザからでもコマンドからでも追加することができる。

コマンドで追加する場合

ターミナル
$ heroku addons:add cloudinary:starter

ブラウザから追加する場合

こちらから登録する。

1. Install Cloudinaryをクリック

0809-3.PNG

2. Add to provisio toにサービス名を入力し、Provision add-onをクリック

0809-4.PNG

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.でハマる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む