20200916のRubyに関する記事は30件です。

Ruby学習4

メソッドとか色々4

現在、Ruby技術者認定試験silverを取得するべく勉強中です。
言語に対する理解がまだまだなので、基本的な事からアウトプットしていきます。

オブジェクトID

模擬問題から抜粋

foo = [1,2,3]
bar = foo
baz = foo.dup #オブジェクトをコピー

bar[3] = 4
p foo
p bar
p baz

=>[1, 2, 3, 4]
  [1, 2, 3, 4]
  [1, 2, 3]

変数fooに、barの更新が適用される原理とは。

foo = [1,2,3]
bar = foo
baz = foo.dup

p foo.object_id #参照しているオブジェクトidを出力
p bar.object_id
p baz.object_id

=>70275736077180
  70275736077180
  70275736077160

fooとbarは同じオブジェクトを参照しているので、barが更新されるとfooもつられて更新される仕組み。
一方、bazはオブジェクトを複製したものなので、参照する対象が違うというわけ。

splitメソッド

第一引数で指定した文字列を区切り文字として、配列を生成する。

servant = "saber,archer,lancer,rider,assassin,caster,berserker"
p servant.split!(/,/)

=> ["saber", "archer", "lancer", "rider", "assassin", "caster", "berserker"]

第二引数で数値を指定する事で、配列の要素数を指定する事が出来る。

servant = "saber,archer,lancer,rider,assassin,caster,berserker"
p servant.split!(/,/, 3)

=> ["saber", "archer", "lancer,rider,assassin,caster,berserker"]
#3つ目の要素からは区切られずに一纏めにされる。

deleteメソッド(String)

引数で指定した文字をselfから削除する。!をつけると破壊的メソッドになる。

puts "0123456789".delete("0-58") #"-"を文字で挟む事で、範囲指定できる(この場合は0から5までの数値)

=> "679"

To_Be_Continued...

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

【Ruby初歩】while文におけるtrueとbreakの役割について

while文の条件式にtrueを設定することで、「処理が停止されない限り、処理をループさせる」ことができる。

簡単な例
while true #条件式をtrueにすることで、処理が停止されない限り、処理を継続させることができる。
 puts "数字の1または2を入力してください"
  number = gets.to_i #getsメソッドで入力させた文字列を、to_iメソッドで数値に変換し、変数numberへ代入。
 puts "入力された数字" #数値ではなく文字列が入力された場合は、0が表示される。
 puts number
 if number == 1
   puts "入力した数字は#{number}です。" #実行後、2行目に戻る(ループ)
 elsif number == 2
   puts "入力した数字は#{number}です。"  #実行後、2行目に戻る(ループ)
 else
   puts "入力した数字は1または2以外です。終了します。"
   break #1または2以外の数字や文字列を入れた場合は、else文中が実行され、ループが終了(break)する。

 end
end

trueであり続ける限り処理が続くわけだから、どこかで止めなければいけない。
Ctrl + Cをコンソール上で入力すれば強制的に止めることはできるが、コードの記述上でそれを行うためには、breakを設定する必要がある。

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

【Rails】エラーメッセージを日本語化してみよう

ユーザー作成とか、ログインとか、何か投稿するときのエラー文をカッコよく英語のままにするのもありですが、分かりやすく日本語にしたいときありますよね?

そんなあなたにGemを使った解決法でお伝えしていきます。

Gemをインストール

Gemfile
gem 'rails-i18n'
$ bundle install

エラーメッセージを日本語に設定

config/application.rb
module SampleApp
  class Application < Rails::Application

    config.i18n.default_locale = :ja
    config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s]
  end
end

どのコードが、どの日本語に対応させるのかを設定

専用のファイルを使って設定していきます。

$ mkdir config/locales/models
$ touch config/locales/models/ja.yml
ja.yml
ja:
  activerecord:
    models:
      user: ユーザ
    attributes:
      user:
        name: 名前
        email: メールアドレス
        password: パスワード
        password_confirmation: パスワード(再入力)

エラーメッセージを手動追加 errors.add

user.errors.add(:base, "追加エラー")

もちろんエラーを生成してから出ないと、追加できないので、流れとしては以下の通り。

> user = User.new
> user.errors
> user.errors.add(:base, "追加エラー")
> user.errors.full_messages
=> ["追加エラー"]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

《未経験→webエンジニア》実務3日目

この記事の目的

自分がやったこと、知らなかったこと、やるべきことを明確にし
1日あたりの成長速度を速める。

【今日やったこと】

Dockerを使用したローカル環境構築
postmanを使用したAPIテスト

【知らなかったこと】

  1. postmanを使用してAPIテストをしている理由

今回の案件では、フロント側とバックエンド側の製作チームが
完全に別れている。

そのため、バックエンド側だけでテストが出来る様にしている

  1. ログインフォームについて

Device
シンプルに作るには良い。
スクールでも使った!

・Authlogic
https://qiita.com/zaru/items/a451de2b165a9e422d62

メリット
カスタマイズをしやすい!
欲しいところだけ入れるなど。

デメリット
日本語の記事が少ないのが難点。

https://qiita.com/zaru/items/a451de2b165a9e422d62

3.トークンについて

・perishable_token
一時的トークン

仮登録など、一時的に付与するトークン。

・persistence_token
永続的トークン

会員登録後など、永続的に使用するトークン。
パスワードリセットの際には変わるので、そこだけ注意!

Cromは情報を多く覚えているので、
検証時には注意すること!

  1. yarn JSやNPMなど、色々とパッケージになっている フロントをいい感じにしてくれるソフトを、まとめてインストールできる

【明日】やるべきこと、読みたい記事など

Sql文の勉強が必要そう

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

Ruby で明示的に名前を指定して親クラスのメソッドを呼ぶ

ブログ記事からの転載です。

自身の親メソッドを呼ぶ

自身と同じ名前の親メソッドを呼ぶ場合は super が利用できます。

  def value
    "Super#value"
  end
end

class Sub < Super
  def value
    # Super#value を呼ぶ
    super
  end
end

p Sub.new.value
# => "Super#value"

では、異なるメソッドから任意の親メソッドを呼ぶ場合はどうするのでしょうか。

class Super
  def value
    "Super#value"
  end
end

class Sub < Super
  def value
    "Sub#value"
  end

  def value2
    # ここで Super#value を呼び出したい
  end
end

答え: Method#super_method を使う

こういう場合は Method#super_method が利用できます。
次のようにして任意のメソッドオブジェクトを取得し、 #super_method で親のメソッドオブジェクトを取得します。

class Super
  def value
    "Super#value"
  end
end

class Sub < Super
  def value
    "Sub#value"
  end

  def value2
    # value のメソッドオブジェクトを取得し、その親メソッドを参照する
    method(:value).super_method.call
  end
end

p Sub.new.value2
# => "Super#value"

おまけ

以下のように .instance_method を利用して任意のクラスのメソッドを呼び出すこともできます。

class Super
  def value
    "Super#value"
  end
end

class Sub < Super
  def value
    "Sub#value"
  end

  def value2
    Super.instance_method(:value).bind(self).call
  end
end

p Sub.new.value2
# => "Super#value"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby学習3

メソッドとか色々3

現在、Ruby技術者認定試験silverを取得するべく勉強中です。
言語に対する理解がまだまだなので、基本的な事からアウトプットしていきます。

配列の生成

通常の配列の生成はこう

mr_children = ["GIFT", "himawari", "simple"] #角カッコの中に要素を格納
p mr_children

=> ["GIFT", "himawari", "simple"]

%記法を使うと、カンマやクォーテーションが消えて少しスッキリする

mr_children = %w(GIFT himawari simple)
p mr_children

=> ["GIFT", "himawari", "simple"]

補足-要素の追加と挿入

mr_children = %w(GIFT himawari simple)
mr_children << "over" #配列の一番最後に要素を追加
p mr_children

=> ["GIFT", "himawari", "simple", "over"]

mr_children.insert(1, "エソラ") #第一引数で添字を指定し、そこに要素を挿入
p mr_children

=> ["GIFT", "エソラ", "himawari", "simple", "over"]

配列の演算(*)

mr_children = %w(GIFT himawari simple)
p mr_children * 3 #数値を掛けた場合

=> ["GIFT", "himawari", "simple","GIFT", "himawari", "simple","GIFT", "himawari", "simple"]
mr_children = %w(GIFT himawari simple)
p mr_children * ";" #文字列を掛けた場合
=> "GIFT;himawari;simple"
# 指定した文字列を挟み、要素を連結した文字列が返される

正規表現とscanメソッドメモ

s = "To be or not to be, that is the question." #変数sに文字列を定義
hash = Hash.new(0) #Hashクラスのインスタンス(中身は空)を生成
s.scan(/\w+/) {|i| hash[i] += 1}
#マッチした文字列を配列で取得。配列の値をブロック変数iに渡し、hashのキーとして定義。対応する値に数値の1を足す。
p hash

=> {"To"=>1, "be"=>2, "or"=>1, "not"=>1, "to"=>1, "that"=>1, "is"=>1, "the"=>1, "question"=>1}
# 単語毎の出現回数を割り出した。

これ→/\w+/ のメモ
\w => 英数字、アンダーバー
+ => 直前の文字が一回以上繰り返す場合にマッチ
つまり、「英数字かアンダーバーを単語単位でマッチさせる」記述。

scanメソッドは正規表現にマッチした文字列を配列で返す。ハッシュのキーに格納される前はどういう形になっているかというと、

s = "To be or not to be, that is the question."
p s.scan(/\w+/)

=> ["To", "be", "or", "not", "to", "be", "that", "is", "the", "question"]

単語で切り分けられた刺身みたいな状態。

To_Be_Continued...

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

Rails フォロー機能

①Model(3ステップ)

○必要なモデル(2つ)を作成

 1. USER (deviseで作成)
 2. Relationships
  rails g model Relationship follower_id:integer followed_id:integer

 create_table "relationships", force: :cascade do |t|
   t.integer  "follower_id"
   t.integer  "followed_id"
   t.datetime "created_at",  null: false
   t.datetime "updated_at",  null: false
 end

○アソシエーション

各モデルに追記。

Relationshipモデル [ Relationships.rb ]

belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"

*class_name: "モデル名" で、指定したモデルを参照した際のモデル名を変更できる。
:point_up:Userモデルを「Follower」と「Followed」に分けるイメージ。

Userモデル [ User.rb ]

    # フォローしている
    has_many :follower, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy
    # フォローされてる
    has_many :followed, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy


    #フォローしている人
    has_many :follower_user, through: :followed, source: :follower
    #フォローされている人
    has_many :following_user, through: :follower, source: :followed

:point_up:Relationshipモデルも「follower」と「followed」に分けられる
:point_up:foreign_keyを追加することで、それぞれRelationshipモデルを通して取得可能となる

:point_up:それぞれsourceにしているモデルを通して
*through: :モデル名 指定したモデルを通して取得する
*source: :モデル名  関連するモデルを指定する

○メソッド定義(3つ) [ User.rb ]

  # 1. followメソッド = フォローする
  def follow(user_id)
   follower.create(followed_id: user_id)
  end

  # 2. unfollowメソッド = フォローを外す
  def unfollow(user_id)
   follower.find_by(followed_id: user_id).destroy
  end

  # 3. followingメソッド = 既にフォローしているかの確認
  def following?(user)
   following_user.include?(user)
  end

:point_up:htmlで表示を条件分岐する際に使います。
 引数にuserが含まれていた場合、trueを返す=trueのときは「フォローを外す」を表示
*.include?(引数)メソッド: 対象の文字列に引数で指定した文字列が含まれているか検索して真偽値を返す

②Controller

○メソッド定義: Relationships Controller

def create #フォローする Userモデルで定義したfollowメソッド
    current_user.follow(params[:user_id])
    redirect_to request.referer #遷移前のURLを取得してリダイレクト
end

def destroy #フォローを外す Userモデルで定義したunfollowメソッド
    current_user.unfollow(params[:user_id])
    redirect_back(fallback_location: root_path)
end

def follower #follower一覧
    user = User.find(params[:user_id])
    @users = user.following_user
    # .follower_userメソッド :Userモデルで定義済
end

def followed #followed一覧
    user = User.find(params[:user_id])
    @users = user.follower_user
    # .follower_userメソッド :Userモデルで定義済
end

③Routing

create(follow)、destroy(unfollow)とfollows,followersの一覧用を追加

resources :users
 resource :relationships, only:[:create, :destroy]

 get 'follows' => 'relationships#follower'
 get 'followers' => 'relationships#followed'
end

④View

・フォローボタン(follow/unfollow)

フォローボタンを表示させたい部分にリンクを追加

 <% if current_user != user %>
  <% if current_user.following?(user) %>
    <%= link_to 'フォロー外す', user_relationships_path(user.id), method: :delete, class: "btn btn-default" %>
   <% else %>
    <%= link_to 'フォローする', user_relationships_path(user.id), method: :POST , class: "btn btn-primary"%>
   <% end %>
 <% end %>

・follower.follows一覧

relationshipsのなかに、follower.html.erb と follows.html.erbファイルを作成し、
各ファイルに下記の通り記載

<% if @users.count > 0 %> の条件分岐でUserがいる場合はeach文で表示し、
いない場合は ユーザーはいません を表示

 <% if @users.count > 0 %>
   <table class="table">
    <thead>
      <tr>
        <th>name</th>
       </tr>
     </thead>
     <tbody>
      <% @users.each do |user| %>
        <tr>
           <td><%= @user.name %></td>
           <td>フォロー数:<%= @user.follower.count %></td>
           <td>フォロワー数:<%= @user.followed.count %></td>
           <td>
            <% if current_user != @user %>
              <% if current_user.following?(@user) %>
                <%= link_to 'フォロー外す', user_relationships_path(@user.id), method: :delete, class: "btn btn-default" %>
               <% else %>
                <%= link_to 'フォローする', user_relationships_path(@user.id), method: :POST , class: "btn btn-primary"%>
               <% end %>
            <% end %>
          </td>
          <td><%= link_to "Show", @user %></td>
        </tr>
      <% end %>
    </tbody>
  </table>
<% else %>
  <p>ユーザーはいません</p>
<% end %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]複数行データの標準入力の取得する

自分用メモ。

標準入力で複数行のデータを配列に格納

value = readlines.map(&:chomp)

標準入力で1行に複数データある場合、配列に格納

例)りんご バナナ

line = gets.split
p line
["りんご","バナナ"]

配列に格納されている複数データを1行に出力

データとデータの間は空白あり、特定の記号など指定できる
・空白の場合

puts array.join(' ')
りんご バナナ

・#の場合

puts array.join('#')
りんご#バナナ

参考

https://qiita.com/Hayate_0807/items/2e9705091b181a104621

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

デフォルトで今日の日付日時を秒以下を省いて表示する方法

【概要】

1.結論

2.どのように記載するか

3.ここから学んだこと

1.結論

(任意の変数).toISOString().slice(0, -8)を使用する

2.どのように記載するか

前提として、

new.html.erb
<%= f.datetime_field :study_date ,id:"datetime_field"%>

で設定しています。

datetime_fieldメソッドは
=***.toISOString().slice(0, -8);をすることにより、
今日の年、月、時間、分までをデフォルトに表記することができます。

***.html.erb
<script>
  var today_time = new Date();
   now.setMinutes(today_time.getMinutes() - now.getTimezoneOffset()); #----❶
    document.getElementById('datetime_field').value = today_time.toISOString().slice(0, -8); #----❷
</script>

と記載しました。
html.erbに記述しておりますので"script"で囲み、更新するとconsoleで変数の定義がされているとエラーがでたのでvarにしています。❶❷までについては下記のURLで触れられている通り表示される時刻はUTC表記(❷)になるので、前もってUTCの差分(❶)に合わせています。

参考にしたURL:
HTML5 に現在時刻を設定するには

3.ここから学んだこと

自分は最初に

****.html.erb
<f.date_field :****, value:Time.now.strftime("%Y-%m-%d")>

を使用していました。value:Time.now.strftime("%Y-%m-%d")を入れてあげるとページが表示された段階で現在の日付が自動で入力されます。

かなり参考にしたURL:
Railsのdate_fieldにてデフォルト値を設定する

しかし、f.datetime_fieldの場合は、valueに上記のものを入れたり、Time.nowにすると

The specified value "2020 9/16 12:05" does not conform to the required format.  The format is "yyyy-MM-ddThh:mm" followed by optional ":ss" or ":ss.SSS".

と出てしまいました。
基準通りの記述を満たせていないということで、認識されていないということでした。なので上記の方法で行いましたが、slice(0,-1)をあまり理解できていないことに気づきました。slice(start,end)はstartから始まる文字~endの手前の数分だけを取り出すので、上記のように書くことで”ss.SSS(秒以下の時間)”を省くことができました。

参考にしたURL:
sliceメソッド


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

デフォルトで今日の日付日時を表示する方法:秒以下を除去

【概要】

1.結論

2.どのように記載するか

3.ここから学んだこと

1.結論

(任意の変数).toISOString().slice(0, -8)を使用する

2.どのように記載するか

前提として、

new.html.erb
<%= f.datetime_field :study_date ,id:"datetime_field"%>

で設定しています。

datetime_fieldメソッドは
=***.toISOString().slice(0, -8);をすることにより、
今日の年、月、時間、分までをデフォルトに表記することができます。

***.html.erb
<script>
  var today_time = new Date();
   now.setMinutes(today_time.getMinutes() - now.getTimezoneOffset()); #----❶
    document.getElementById('datetime_field').value = today_time.toISOString().slice(0, -8); #----❷
</script>

と記載しました。
html.erbに記述しておりますので"script"で囲み、更新するとconsoleで変数の定義がされているとエラーがでたのでvarにしています。❶❷までについては下記のURLで触れられている通り表示される時刻はUTC表記(❷)になるので、前もってUTCの差分(❶)に合わせています。

参考にしたURL:
HTML5 に現在時刻を設定するには

3.ここから学んだこと

自分は最初に

****.html.erb
<f.date_field :****, value:Time.now.strftime("%Y-%m-%d")>

を使用していました。value:Time.now.strftime("%Y-%m-%d")を入れてあげるとページが表示された段階で現在の日付が自動で入力されます。

かなり参考にしたURL:
Railsのdate_fieldにてデフォルト値を設定する

しかし、f.datetime_fieldの場合は、valueに上記のものを入れたり、Time.nowにすると

The specified value "2020 9/16 12:05" does not conform to the required format.  The format is "yyyy-MM-ddThh:mm" followed by optional ":ss" or ":ss.SSS".

と出てしまいました。
基準通りの記述を満たせていないということで、認識されていないということでした。なので上記の方法で行いましたが、slice(0,-1)をあまり理解できていないことに気づきました。slice(start,end)はstartから始まる文字~endの手前の数分だけを取り出すので、上記のように書くことで”ss.SSS(秒以下の時間)”を省くことができました。

参考にしたURL:
sliceメソッド


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

(Ruby on Rails6) フォームから投稿内容の反映

マシンスペック

・バージョン 10.15.3
・Ruby ruby 2.6.3p62
・Rails 6.0.3.2

まえがき

前記事では、データーベースのidを取得したデータベースの表示する方法を記録しました。こちらでは、フォームから投稿したデータベースの内容を、ビューに反映させる方法を忘却録として記録します。
ルーディングなどが複雑になっていて、難しい‥

フォームから投稿内容の反映

こちらでは、ビューでのフォーム制作から、進めます。

フォーム画面の作成

簡単なフォームをビューに制作します。

app/views/任意/任意.htme.erb
<div class="form">
  <label>title: </label>
  <input type="text">
  <br>
  <label>text: </label>
  <textarea></textarea>
  <input type="submit" value="投稿">
</div>

アクションの作成

先のフォーム作成↓で
submit を設定しました。
フォームで入力したデータを、データーベースに保存せさるには、submitをクリックするとアクション(コントローラー)を経由させる必要があります。

app/views/任意/任意.htme.erb
  <input type="submit" value="投稿">

以下を、routesに記入しましょう

config/routes.rb
 
  post "コントローラー名/アクション名" => "コントローラー名#アクション名"

例↓

config/routes.rb
 
  post "posts/create" => "posts#create"

要素について

config/routes.rb
  get "コントローラー名/アクション名" => "コントローラー名#アクション名"
  post "コントローラー名/アクション名" => "コントローラー名#アクション名"

↑のroutesでは get・post があります。
ページのルーティング設定であれば get で問題ありません。
しかし、 フォームの値を取得 する場合は post にする必要があります。

submitからアクションへの送信

フォームへデータを送信する場合は form_tagメソッド を設定しなければいけません。

(form_tagメソッド) 設定前↓

app/views/任意.html.erb
<div class="form">
  <label>title: </label>
  <input type="text">
  <br>
  <label>text: </label>
  <textarea></textarea>
  <input type="submit" value="投稿">
</div>

(form_tagメソッド) 設定後↓

app/views/任意.html.erb
<%= form_tag("/コントローラー名/アクション名") do %>
 <div class="form">
   <label>title: </label>
   <input type="text">
   <br>
   <label>text: </label>
   <textarea></textarea>
   <input type="submit" value="投稿">
 </div>
<% end %>

私は <%= form_tag("/posts/create") do %> ‥ <% end %> で設定します。
また、form_tagメソッド は ↓になります。

app/views/任意.html.erb
<%= form_tag("/コントローラー名/アクション名") do %>
処理
<% end %>

リダイレクト設定

先までの作業で
・フォームの作成
・フォームからアクションへの送信
を行いました。
ここでは、送信したデータを表示させるため、該当(一覧)ページへリダイレクトするように設定します。

リダイレクトを設置するには redirect_toメソッド を使用します。

app/controllers/任意_controller.rb


def create
    redirect_to("/コントローラー名/アクション名")
end

例↓

app/controllers/任意_controller.rb


def create
    redirect_to("/posts/index")
end

入力データをデーターベースへ送信・保存させる

先に設定した、フォームを確認します。

app/views/任意.html.erb
<%= form_tag("/コントローラー名/アクション名") do %>
 <div class="form">
   <label>title: </label>
   <input type="text">
   <br>
   <label>text: </label>
   <textarea></textarea>
   <input type="submit" value="投稿">
 </div>
<% end %>

こちらで記述している↓の2つは、今のままだと入力データを送信することができません。
※2つ(input・textarea)

app/views/任意.html.erb
<%= form_tag("/コントローラー名/アクション名") do %>
   <input type="text">
   <textarea></textarea>
<% end %>

送信をできるようにするにはname属性 を設定します。

↓ name属性の設定

app/views/任意.html.erb
<%= form_tag("/コントローラー名/アクション名") do %>
   <input name="title" type="text">
   <textarea name="content"></textarea>
<% end %>

name属性の設定は、データーベース作成時のカラム名を設定してください。
私は "title と text" と設定しています。

name属性を設定して、送信することにより name属性の値をキーとしたハッシュ がアクションへ送信されます。

次では、"name属性"とアクションコントローラーを紐付けましょう。

name属性の設定と変数"params"

先のフォームに、name属性を設定しコントローラーのアクションへ送信をできるようにしました。
そして、フォームで送信されたデータを受け取るために、コントローラーに params を設定します。
params は、 フォームデータ や link_to などの値を受け取るパラメーターです。
上記のViewsでタグに"name"でデータを紐付けたので、params と合致します。

app/controllers/任意_controller.rb


def create
    params[:content]
    params[:title]
    redirect_to("/コントローラー名/アクション名")
 end

データーベースに投稿を保存する。

rails console でインスタンス化して制作したことを思い出しましょう。
先の params の箇所に Postインスタンス を作成するように入力します。

app/controllers/任意_controller.rb


  def create
    @post = Post.new(content: params[:content])
    @post.save 
    @post = Post.new(title: params[:title])
    @post.save 
    redirect_to("/posts/index")
  end

@post.saveを忘れないようにしましょう。
ここまで、できたら投稿データが表示されるでしょう。

rails6_view-date.png

title と text が反映されています。

以上のファイルは、githubに公開しています。興味を持った方はダウンロードをしてみてください
Github

あとがき

以上が、フォームから投稿内容の反映でした。
なかなかアプリケーションらしい、仕組みが作れてテンションが上がります。
投稿機能ができたので、次からは"削除・編集"など仕組みを加えて行きたいです。

参考リンク

書籍: たのしいRuby 第6版

私のリンク

また、Twitter・ポートフォリオのリンクがありますので、気になった方は
ぜひ繋がってください。プログラミング学習を共有できるフレンドが出来るととても嬉しいです。

Twitter
Portfolio
Github

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

【Rails】この記事を「devise 名前 ログイン」で調べたあなたへ贈る

この記事を「devise 名前 ログイン」で調べたあなたへ贈る

はじめに

yukiと申します。DMMWEBCAMPにお世話になって、今はWEBエンジニアをしつつ、自分で仲間を集めてサービス開発したり、プログラミングの家庭教師したり毎日エンジニアライフをエンジョイしています。

きっとあなたは「devise 名前 ログイン」でRailsのgem、Deviseをを用いて名前とパスワードを使ってログインする方法を調べに来てくれたんだと思います。

そんなあなたのために、今日はできるだけ関連記事の中で一番わかりやすく、そしてあなたにとってプラスになるように「なぜそれをやるのか」という部分まで解説します。

よければ最後まで頑張ってご覧ください。

なぜなら、あなたは過去の私なのだから・・・

記事の対象者

  • deviseを使ってログインする方法が知りたい方
  • Railsの環境構築は終わっており、deviseでmailとpasswordでのログイン実装をした経験がある方

修正方法

それでは、早速方法と理由について述べます。
順番通りに、かつ見落としがないように気をつけてください。
また、名前とパスワードでログインすることを以下「名前ログイン」と呼びます。

Deviseをインストールして、初期設定をする

まずは、大前提としてdeviseの設定をしていきましょう。

Gemfileにdeviseを追記し、インストールする

:white_check_mark: Gemfileの一番下に以下の記述をして

gem 'devise'

:white_check_mark:bundle installを実行してください。
これでプロジェクトの中でdeviseを扱えるようになりました。

deviseの初期設定

bundle installが終わったのちに、deviseをセットアップしていきます。

:white_check_mark:コマンドライン(ターミナルなどコマンドを実行する場所)でrails g devise:installを実行してください。

この初期設定はデバイスの設定をするファイルを作るために必要です。
これを作っておけば、後で名前ログインするために必要な設定をできるようになります。

ログインするために必要なUserテーブルを作成する

次に、そもそも名前ログインに必要なユーザーのデータを保存するテーブルを作成します。

:white_check_mark:コマンドラインでrails g devise Userを実行してください。

これは、rails g model Userとはちょっと違っていて、deviseの機能を通じてUserのモデルやらマイグレーションファイルやらを作ってくれます。
なぜそんなことをするかというと、必要なカラム(emailなどの情報を入れる場所)を自動的に作ってくれるからです。

Userのモデルを作るマイグレーションファイルに、nameカラムを追加する

先程のコマンドを実行すると、db/migrate/のなかに、マイグレーションファイルができていると思います。rails db:migrateを行えばdeviseの機能でログインする際に最低限必要なUserテーブルができるのですが、今回は「名前ログイン」をできるようにしたいので、nameカラムを追加していきます。
理由は単純で、ユーザーの名前を入れておく場所が初期設定のままだと存在していないからです。

こんな感じで追記してあげてください。
※ファイル名は若干各々で作成日時によって変わります。

db/migrate/2020......devise_create_users.rb
# 中略
      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at

      t.string :name # ここに追加したよ!!!!!!!
      t.timestamps null: false
# 中略

:white_check_mark:上のコードを追記してrails db:migrateを実行

これでnameカラムを持つUserテーブルが作成できました。

新規登録の際に、名前を登録できるように必要なviewファイルを可視化させる

これで保存する準備はできたので、新規登録画面でnameの情報を送れるようにしていきます。
しかし、今の状態だとdeviseの新規登録やログインに使う画面を編集することができません。なぜなら、ファイルが見えないからです。

実際に編集できるようにするために
:white_check_mark:下記のコマンドをコマンドラインで実行しましょう。

rails rails g devise:views

これで、app/views以下にdeviseのフォルダ一式が作成され、deviseの新規登録やログイン画面を書き換えられるようになりました。

肝心のファイルはどれかといいますと、

  • 登録はregistrationsの中のnew.html.erb
  • ログインはsessionsの中のnew.html.erb

です。

新規登録の画面に、nameを贈るフォームを追加する

現在、新規登録の画面からはemailしか送れない状態になっています。

:white_check_mark:これをちょこっと編集して、名前の情報を送れるようにしましょう。

app/views/users/registrations/new.html.erb
<h2>Sign up</h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= render "users/shared/error_messages", resource: resource %>

<!-- ここから追加 --!>
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name, autofocus: true, autocomplete: "name" %>
  </div>
<!-- ここまで追加 --!>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <--! これより下は省略 !-->

ここまでできたら

:white_check_mark:rails sを実行して、localhost:3000/users/sign_upにアクセスしてください。名前のフォームもできていたらOKです。

ただし・・・

新規登録の際に、nameの情報を送って良いように許可する

上の手順を行った際に、多分新規登録には成功してRailsの初期画面が現れますが、コマンドラインにこのような不穏な文字が出ているはずです。

Unpermittted parameter: :name

これは:nameという値が送信を許可されていないよ。という意味です。
この記事を読んでいる人はきっと「ストロングパラメーター」という言葉を聞いたことがあると思いますが、まさにそれです。

標準では、:nameの値を贈ることが許可されていないので、これを許可してあげましょう。

:white_check_mark: app/controllers/application_controller.rbを編集します。以下のように追記してください。

app/controllers/application_controller.rb
application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller? 

  private

    def configure_permitted_parameters
      devise_parameter_sanitizer.permit(:sign_up,keys:[:email])
    end
end

こちらのファイルは、アプリケーション全体に関わるコントローラーだと思っていただいて大丈夫です。ここに書いたものがアプリケーションの処理を実行する際に適用されます。

before_action :configure_permitted_parameters, if: :devise_controller?は、わかりやすくいいますと、「もし、deviseの処理を行う場合、configure_permitted_parametersというものを実行してね」という意味です。

じゃあconfigure_permitted_parametersとは何かというと、その下に定義されているメソッドです。devise_parameter_sanitizer.permit(:sign_up,keys:[:name])というやつですね。

こちらもわかりやすくざっくり解説すると、「deviseゥ、sign_upする時は:nameという値を送信するのも許可しろォ!だが、その他不正な値だけは許可しないィ!」という意味です。
※emailとかはデフォルトで許可されているので大丈夫です。

:white_check_mark:以上を設定できたら、もう一度新規登録を行ってみましょう。
もう、コマンドラインにさっきのエラーメッセージは出ていないはずです。

ログアウトできねえよ!って方は、cookieを削除するとできます。
MACの場合、URLの横の「i」ボタンを押してみてください。

ログインの画面の、emailを送るフォームを、nameを送るフォームに変更する

:white_check_mark:新規登録の画面からnameを登録できるようになったので、当然ログイン画面も修正していきましょう。

app/views/users/sessions/new.html.erb
<h2>Log in</h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= render "users/shared/error_messages", resource: resource %>

<!-- ここから追加 --!>
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name, autofocus: true, autocomplete: "name" %>
  </div>
<!-- ここまで追加 --!>

<!-- ここから削除 --!>
  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>
<!-- ここまで削除 --!>

  <--! これより下は省略 !-->

emailに関するフォームを削除して、nameのフォームを追加しました。

:white_check_mark:では、これで先程登録した名前とパスワードでログインしてみましょう。

−−−
−−

はい、できません。いらっとしますね。
コマンドラインを見てみると、またUnpermitted parameters: :nameが出ています。
では、こちらを修正していきましょう。

ログインに使う値をemailからnameに変更する

今回は、新規登録の時と同様にapplication_controllerに何かを登録すれば良いかというと、実は違います。今回ここで弾かれる要因を直すには、deviseの設定をしているファイルを見に行く必要があります。

思い出してください。rails g devise:installした時に作ったファイルですね。

:white_check_mark:では、config/initializers/devise.rbを開いてください。

VSCcodeを使っている方は、cmd(ctrl) + pを押すとファイル名で検索できますよ。

ウジャウジャといろんな設定が書いてあるファイルが開きますが、大丈夫です。
cmd + fで文字列を検索できるのですが、config.auまで入力してみてください。

# config.authentication_keys = [:email]という記述が見つかるはずです。
deviseはここで、オーセンティケーションに使う鍵(つまりは認証ですね、ログインに使う値だと思ってください)を設定しているので、ここのコメントアウトを外して、emailをnameに変えてあげましょう。

# config.authentication_keys = [:name]ということですね。

では、これでログインできるか試してみましょう!
きっと、エラーが出ずに成功できると思います。

まとめ

  • deviseのインストールと設定をしっかりしよう
  • userのマイグレーションファイルにnameカラムを追加しよう
  • 新規登録時nameの値が送れるように許可しよう
  • ログイン時nameを使ってログイン(正式には認証してもらえるように)できるようにdeviseを設定しよう

といった感じです。

これからも勉強する際は、ぜひ「なんでその記述を書くと思い通りに動くのか」突き詰めて考えてみてください。

応援しています。何か困ったことがあれば、DM待っています〜。

こんな記事も書いてますという紹介

【卒業生】DMMWEBCAMPに通おうと思っている人に伝えたいこと

会社の紹介

私は現在、株式会社ダイアログという物流×ITの会社に勤務しております。
2020年9月現在、エンジニアの募集はしていませんが、他にも様々な職種を募集しているので、Wantedlyのページをご覧ください。いつか自分のQiitaきっかけで応募してくださる方がいたら、嬉しいなと思います。

インタビュー記事(入社後の感想など)

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

第2回 SmartHRのライブラリkijiを使ってe-Govを動かす(e-Gov公開資料編)

一般ソフトウェア開発事業者等が、e-Gov外部連携APIを利用したソフトウェアサービスを開発するための流れ及び公開資料を紹介します。

1.開発の流れ

一般ソフトウェア開発事業者等(以下、開発事業業者)は、e-Gov(総務省)にソフトウェア開発の申込を行う必要があります。

①ソフトウェア開発の申込を行う
②e-Govから検証環境利用申込に関する書類が送られてくる
③開発事業者は必要事項(利用者ID等)記入の上、検証環境利用申込書をe-Govに送る
④e-Govから検証環境に関する資料が送られてくる
⑤開発事業者はソフトウェアサービスを開発する
⑥開発事業者は最終確認試験の申込を行う
⑦開発事業者は最終確認試験結果を提出する
⑧e-Govからの合格判定によりソフトウェアサービスが使用可能になる

③にて、開発事業者は検証環境利用申込書に次の内容を記載します。

No 検証環境利用申込書に記入する内容
1 法人団体名
2 ソフトウェア名
3 利用者ID(最大3名まで申請可能)
4 検証環境で利用する外部連携APIを〇で指定する。利用者ID登録、利用者認証、一括申請については必須となる。

④にて、e-Govから検証環境に関する資料が送られてきます。開発事業者は、これらの情報を厳重に管理する必要があります。

No 検証環境に関する資料
1 検証環境利用通知書(検証環境URL、ソフトウェアID、利用者ID、Basic認証用ID及びパスワード)
2 APIテスト用データ情報(ソフトウェアIDだけが使える補正通知一覧取得API、部分補正API、補正再提出API向けのテストデータ)
3 最終確認試験テスト仕様書兼成績書

検証環境URLについては、本番環境とは異なるリクエストURLとなります。このタイミングでは、検証環境利用申込書に記載した利用者IDがe-Govに登録されており、この利用者IDに紐づいて次の証明書が管理されています。

証明書の状態 証明書ファイル名
有効 e-GovEE01_sha2.pfx
有効期間切れ e-GovEE04_sha2.pfx
失効 e-GovEE05_sha2.pfx

2.公開資料について

e-Govから開発事業者向けに用意された資料がダウンロードできます。ここには、利用ガイド、規約、申込書、仕様書、検証環境テスト用電子証明書等があります。

No 利用ガイド、申込様式等
1 外部連携API API概要
2 外部連携API 利用ガイド
3 外部連携API 各種申込書(一式)
4 外部連携API 利用規約
5 外部連携API 個人情報の取扱について
No 仕様書、申請書XML構造定義
1 外部連携API API(Version 1)仕様書
2 外部連携API 情報セキュリティ要求仕様書
3 外部連携API 申請データ仕様共通データ仕様書
4 対象手続一覧
5 手続情報一覧/提出先一覧(一式)
6 申請書XML構造定義【社会保険関係手続】(一式)
7 申請書XML構造定義【雇用保険関係手続】(一式)
8 申請書XML構造定義【労働保険適用徴収関係手続】(一式)
No 検証環境テスト用手続
1 APIテスト用手続一覧
2 APIテスト用手続一覧(別紙 APIテスト用手続ステータス遷移一覧)
3 申請書XML構造定義【APIテスト用手続き】
No 検証環境テスト用電子証明書
1 電子証明書

ここでは、仕様書、検証環境テスト用手続、電子証明書について紹介します。

2.1 仕様書

(1)API(Version 1)仕様書

APIを利用して電子申請を行うためのリクエストURIとそのパラメタ、httpリクエストボディに指定する送信XML及びe-Govが返却するレスポンスボディ(応答XML)に関する仕様について記載されています。

(2)情報セキュリティ要求仕様書

開発事業者がソフトウェアを開発する上で遵守すべきセキュリティ要求事項について記載されています。

(3)申請データ仕様共通データ仕様書

全手続共通のデータ仕様及び各手続毎の個別仕様について記載されています。
構成管理XML、構成情報XML、申請書XMLについて、XMLのタグ構造が定義されています。

(4)申請データ仕様及び申請書XML構造定義書

各手続毎の申請データに関する個別仕様が記載されています。

2.2 検証環境テスト用手続

検証環境にて、外部連携APIに係るテストを行う際に利用できるテスト用手続に関する公開仕様です。

No 申請書XML構造定義【APIテスト用手続き】
1 APIテスト用手続一覧
2 APIテスト用手続一覧(別紙 APIテスト用手続ステータス遷移一覧)
3 申請書XML構造定義【APIテスト用手続】
  • 検証環境では、正常系及びエラー系合わせて、標準形式38種類、個別署名形式45種類の労働・社会保険関係の手続が用意されています
  • 各手続には、手続に関する条件(署名有無、添付書類、提出先、取り下げ等)、到達以降の可能処理等が定義されています
  • 各手続は32の様式パターンのいずれかに属しており、様式パターンの内容に従ってデータ項目を設定する必要があります

「APIテスト用手続き一覧」では、テスト用手続について次の内容を確認できます。

  • 各手続の申請書様式は、APIに係るテストを行う上で必要なテストケースを網羅できるようにパターン化し、適切な手続き情報を設定して整備しています。
  • 公文書発出対象の手続、コメント通知対象の手続については、それぞれサンプルの公文書及びコメント内容が通知されます。
  • 添付書類として許容しているファイル形式の拡張子は、「doc」、「xls」、「pdf」、「docx」、「xlsx」、「txt」です。
  • 手続識別子の上3桁が「950」の手続については、申請後のステータスが自動的に遷移されます。

「APIテスト用手続一覧(別紙 APIテスト用手続ステータス遷移一覧)」では、ステータスの自動遷移について確認できます。以下2例ほど紹介します。

  • 標準形式のうち、手続識別子=950A010002010000については、手続名「健康保険・厚生年金保険事業所関係変更(訂正)届/電子申請」であり、公文書が発出されます。ソフトウェアサービスが一括申請を行うと(到達)→(審査中)→(審査終了)と自動的に遷移し、(審査終了)になった時に公文書を取得する事ができます。
  • 標準形式のうち、手続識別子=950A010700007000については、手続名「健康保険・厚生年金保険被保険者資格取得届、船員保険・厚生年金保険被保険者資格取得届/電子申請」であり、公文書は発出されません。ソフトウェアサービスが一括申請を行うと(到達)→(審査中)と自動的に遷移します。さらにソフトウェアサービスが、到達番号を指定して取下げ依頼を行うと、(審査中-取下げ処理中)→(手続終了-取下げ済み)に自動遷移します。

2.3 電子証明書

検証環境で使用するSHA-256版の電子証明書について説明します。

No ファイル名 説明
1 e-GovEE01_sha2.pfx 利用者IDに予め設定される有効な証明書
2 e-GovEE02_sha2.pfx 有効な証明書
3 e-GovEE03_sha2.pfx 有効な証明書
4 e-GovEE04-1_sha2.pfx 利用者IDに予め設定される有効期限の切れた証明書
5 e-GovEE04-2_sha2.pfx 有効な証明書
6 e-GovEE05_sha2.pfx 利用者IDに予め設定される失効している証明書

電子証明書のサフィックスは証明書のフォーマットを表します。「.pfx」は、PKCS#12(PFX)形式の鍵と証明書のフォーマットであり、サーバの鍵を証明書チェーン全体と一緒に格納して保護できる形式です。Microsoft社製品でよく使われる形式です。

opensslコマンドを使って、証明書の内容を確認します。途中でパスワードが求められた場合、「gpkitest」と入力します。

opensslの実行結果
>openssl pkcs12 -info -in e-GovEE01_sha2.pfx
Enter Import Password:(「gpkitest」と入力)
MAC Iteration 2000
MAC verified OK
PKCS7 Data
Shrouded Keybag: pbeWithSHA1And3-KeyTripleDES-CBC, Iteration 2000
Bag Attributes
    localKeyID: 01 00 00 00
    friendlyName: le-d39d0281-d8cb-4396-b002-b4366119c8e4
    Microsoft CSP Name: Microsoft Enhanced Cryptographic Provider v1.0
Key Attributes
    X509v3 Key Usage: 10
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQITLitdILmV8cCAggA
MBQGCCqGSIb3DQMHBAiXSoB8We54iwSCBMiWf80URxShHlnQltb0fsWst7cAZF2D
/FnXM/8RRrMuK561dB5f+7ImMTtJobdOYOEpeO8sqxhGGeEnivFPMhvrsdGKMZul
nCoQBk0ug0iG2vnlQZAP6RPhbo9u4H3ZtTxJeO4DnYdJJj681vxlkEwhQ+Xv7arV
ZkDAmP20E3tKsobuIKNoLDF6HvrsW6k3bSeYcQqjm71BUAZe9DR2JnBNilbnN7gn
NXtsl8B2bGj8/bATsF37elW/zRkm+35vhcV4jLOEJY67ZF9uVAmSgjGXDuEo3pWO
jrX3t5ErsDXXObjw35cVVTvXUZl6s98p2QC3D/hThUf+MBE+pjO3AnNdb0Pjikmf
Fx1VSaeRW1vpf6/oOdwBUGPJuUXyZm1t93/AiYMOG5r9UFLRoFoHKYeCUte+BO2o
N6M2wUSgekzQejrZeT6ATvRpA15yQ6laAFw6LwPRbJ3zykCch2UAPJaspXgUzdfA
9jGzGuq9xIOgeVe+Gi2OdLBk+hAc0qF+zkrwuhswWT3vSSNQ2/mlfl3kI7MPzcvB
82mzC0aPCts8oG0JVL3m/WpnfO6XFnB5GRB10mo7NyiecjHOEPCfsjOcsTr/Bl1N
EPkh4Rt+o315l4UFtIw8nfK8xiw9ag6Lc8n3IUy9+fUWe/MmZxsvKfMyUe8x8RJA
gubv1R8RUqPg+rq6rCydlPE3RlMwro5ooF5sblWANPpNJwyTOr1CEJHTyWzzdMnz
pdbVj0Tz9P3YvR9+KUmv5FPO+Ke/G0KLVbvg14OUfTr5xFEihUrYPOWtRlGlGOyT
le+rGFAzIsRTJvFU6RGsktGTXyzmPBxY2YPuTnDnkdUCLl8UIcXUPMeWRSIh5YC0
zpPgwLlDdy0dnDSIYo5j2yjhYCYcx0MkkZpkn896RcFkK/QnX5L+EvCz6GIZVxAi
yEzD6lFs4Uamm7E/6MbfyoxxbfIOq8BKoufhSbu/QzUlVw/f1+HXjROaTjB2zRuo
GmuWMugdhEMkKl1R4MTpIhoORdQNYGRWcJfbvSZx6gAmTDMoYkt5h05+haNyXY3t
VkLIlHnpRcQR+JgFghyp0j49kmgyKjRZ6XyCgRnUazE9GOalCZLRZuFQEIlgjNRQ
i5f+LI1UNULxD3PJR8A6AMXiasfk94t4qlpH5xc2HVC44Hrk/gX9vfIYBodw43qX
/r0Z2H/27Qte0IRAZ3jzBV46wN2Y0fqKVQNFchIIJleSebd/ka317ICI8gphf+Js
7Y//jUfl03X7oB2GviVHNzvtjJy0AgAlHkltEgswSbWmd3e+lsCtRpnpe4f1qj76
nYTy0AYtvcZwsE7EaCDYGz/KGQVJjorKVgK12N27catPUcDX3NPMhv9YVe1rcVhT
Iku2X6b0c+YmDSGs05m0LUp8HMN/RUAVg+77atjBT72yqABzV2OcZDc9vHCZfYg2
hO/NSfITPB/Jun/gd4VgjH8sO9Eupa/DgZKQ7b858Md6kjK37iKu8cxvG9VfizFj
/msgnKpZmB0lXnvb7hQUgbeOXC0HP1VswZqq1JBzvFICcFo+4KZQquig2SJytR5o
bFxOWp1zXMllQWSunt1aMnmun/B68BU6SiNXNjnTn+gAd+i3avys8X1VLZBFAwKF
6wY=
-----END ENCRYPTED PRIVATE KEY-----
PKCS7 Encrypted data: pbeWithSHA1And40BitRC2-CBC, Iteration 2000
Certificate bag
Bag Attributes
    localKeyID: 01 00 00 00
subject=/C=JP/O=DemoMin1/OU=CA1/CN=Ichiro Madoguchi
issuer=/C=JP/O=DemoMin1/OU=CA1
-----BEGIN CERTIFICATE-----
MIIEizCCA3OgAwIBAgIEWCsMRzANBgkqhkiG9w0BAQsFADAuMQswCQYDVQQGEwJK
UDERMA8GA1UECgwIRGVtb01pbjExDDAKBgNVBAsMA0NBMTAeFw0xOTEwMjUwMTE2
NTZaFw0yNDEwMjQxNDU5NTlaMEkxCzAJBgNVBAYTAkpQMREwDwYDVQQKDAhEZW1v
TWluMTEMMAoGA1UECwwDQ0ExMRkwFwYDVQQDDBBJY2hpcm8gTWFkb2d1Y2hpMIIB
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsD81VFFmGM26HYdeGrGzbPba
deBzU4WxIE1r6qeZLxjz7DiV42tjo8QFulWZk6hmCeb5j9ChKp+BA/9Yj6usccKH
ZrPQmbcXUZkXRXz9z/7CLxp8b1zAzJakZ0g7o9iU3a3TpyeP6V+GFtkO8YqfthEs
JsYzx82dUi85Bm4l5pnztozPdXRZd2VJpdwzGqtqw6N9PKzMNWux5C0jklkaPeP5
P6NHLfeN51+phkY2xkdJlry629PeKjZAwN0Z8Xu0JdubCvA95UctoZgBoOjpgDtW
ImV3o9hDpwu+1tMlEBhNZIuKOr2G38qRZ9iJUyoK+XhbzyBUu0A2cwCj0MshbwID
AQABo4IBlDCCAZAwDgYDVR0PAQH/BAQDAgbAMBEGCWCGSAGG+EIBAQQEAwIGQDBO
BgNVHRIERzBFpEMwQTELMAkGA1UEBhMCSlAxHjAcBgNVBAoMFeaooeaTrOawkemW
k+iqjeiovOWxgDESMBAGA1UECwwJ77yj77yh77yRMB8GA1UdIAEB/wQVMBMwEQYP
AoM4ho4xCAEBAAKMmyVkMGgGA1UdEQRhMF+kXTBbMQswCQYDVQQGEwJKUDEeMBwG
A1UECgwV5qih5pOs5rCR6ZaT6KqN6Ki85bGAMRIwEAYDVQQLDAnvvKPvvKHvvJEx
GDAWBgNVBAMMD+eqk+WPo+OAgOS4gOmDjjBQBgNVHR8ESTBHMEWgQ6BBpD8wPTEL
MAkGA1UEBhMCSlAxETAPBgNVBAoMCERlbW9NaW4xMQwwCgYDVQQLDANDQTExDTAL
BgNVBAMMBENSTDEwHwYDVR0jBBgwFoAUFNPnlvRSx8XFOnRlLumW95h4I18wHQYD
VR0OBBYEFD6UH9Exye9dEpz9MHJ3sbUDpoZSMA0GCSqGSIb3DQEBCwUAA4IBAQBb
CKyFGsqMv6+HkrY0OK+4v40PJQAa/KbOC3JTKooLfNCNXTiTwtWAl1sGN+Ow8pIp
8Yvj16VcYpi8zO4TmNe8NT+u/e2OvBXwJ9OxOs9UNI2m/mXGcSSJ7eXMR3aVCniD
U7IaQeicquQttLP9IOk9Ao1W+BM35y5bITA/BMO5tzgaimp4G484QtF/XLi40rGh
aZAHGEfvl0abJXPumjajhnGv7SCkjw4+9qdz5Dtp6kl+GVshQgo6ofpEWhVzdhfq
KhNy8dNRL7C/gOTYm+M9SAFk9syL5xKXRyMUGDOheypiJrW/QyOjrxs6cFa5VqaZ
WcRIq8yVPwABCpGG/hjU
-----END CERTIFICATE-----
  • 「-----BEGIN ENCRYPTED PRIVATE KEY-----」から「-----END ENCRYPTED PRIVATE KEY-----」で囲まれている部分が秘密鍵です。
  • 「-----BEGIN CERTIFICATE-----」から「-----END CERTIFICATE-----」で囲まれている部分が証明書(公開鍵)です。
  • subjectは証明される人、issuerは証明する人を意味します。
  • subject=/C=JP/O=DemoMin1/OU=CA1/CN=Ichiro Madoguchi、issuer=/C=JP/O=DemoMin1/OU=CA1であり、テスト向けの自己署名証明書なので、発行者(issuer)と主体者(Subject)が同じ組織です。

e-Govでは、ソフトウェア開発事業者が検証環境利用申し込み時に希望した利用者IDに紐づけて、3つの証明書を管理した状態になっています。有効な証明書については、通常テストで利用します。また、有効期限の切れた証明書及び失効している証明書については、e-Govで管理する証明書を更新するテストで利用します。

証明書の状態 証明書ファイル名
有効 e-GovEE01_sha2.pfx
有効期間切れ e-GovEE04_sha2.pfx
失効 e-GovEE05_sha2.pfx

3.最終確認試験について

最終確認試験については、ソフトウェアサービスとe-Gov間のデータ送受信がe-Govの仕様に従って問題なく出来ているかを確認します。ソフトウェアサービスのアプリケーションとしての機能を確認するものではありません。

SmartHRのライブラリkijiでは、RSpecを利用して最終確認試験を自動化しています。

No API名称 確認するための条件 HTTPレスポンスコード
1 利用者ID登録 追加する利用者IDを指定 "201"であること
2 利用者認証 e-Govに登録済みの利用者IDを指定 "200"であること
3 一括申請 一括申請ファイルを指定 "202"であること
4 送信案件一覧情報取得 (ID指定) 一括申請時の送信番号を指定 "200"であること
5 送信案件一覧情報取得 (日付指定) 取得対象期間を指定 "200"であること
6 申請案件一覧情報取得 一括申請時の送信番号を指定 "200"であること
7 状況照会 申請案件一覧情報取得で取得した到達番号を指定 "200"であること
8 取下げ 取下げ可能な到達番号の取下げデータを指定 "200"であること
9 補正通知一覧取得 状況照会の補正可能な到達番号を指定 "200"であること
10 補正(再提出) 補正(再提出)可能な到達番号を指定 "202"であること
11 補正(部分補正) 補正(部分補正)可能な到達番号を指定 "202"であること
12 補正(補正申請) 補正(補正申請)可能な到達番号を指定 "202"であること
13 公文書・コメント一覧取得 状況照会の公文書メッセージが設定された到達番号を指定 "200"であること
14 公文書取得 取得期限内の通知番号の公文書を指定 "200"であること
15 公文書取得完了 公文書取得で取得した公文書を指定 "200"であること
16 公文書署名検証 公文書取得で取得した公文書を指定 "200"であること
17 コメント通知取得 取得期限内の通知番号のコメント通知を指定 "200"であること
18 コメント通知取得完了 コメント通知取得で取得したコメント通知を指定 "200"であること
19 電子納付対応金融機関一覧取得 "200"であること
20 電子納付情報一覧取得 納付可能な到達番号を指定 "200"であること
21 証明書識別情報追加 新規登録する証明書を指定 "200"であること
22 証明書識別情報更新 更新登録する証明書を指定 "200"であること
23 証明書識別情報削除 削除する証明書を指定 "200"であること
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

第1回 SmartHRのライブラリkijiを使ってe-Govを動かす(e-Gov仕様編)

1.はじめに

クラウド人事労務ソフトウェアサービスであるSmartHRに組み込まれているライブラリkijiは、Rubyで作成されたOSSであり、誰でも入手・改造・再頒布が可能です。
SmartHR kiji

本記事では、ライブラリkiji(以下、kiji)を使って署名付きxmlを生成し、curlコマンドでe-Gov電子申請システム(以下、e-Gov)に送信する事を試みます。対話的にe-Govを動かすので、e-Govの仕様を深く理解できると考えています。さらにはe-Govの問題点を定義し、マイナポータルでの改善の可能性について考察します。
なお、ここで動作確認したe-Govとは、ソフトウェア開発事業者向けに公開されている検証環境である点に留意する必要があります。

今回は全4回のうちの1回目として、e-Gov仕様を紹介します。2回目はe-Gov公開資料、3回目は実行環境構築、4回目は実機確認と続きます。

動作環境については、windows10、ruby 2.6.5p114 (2019-10-01 revision 67812) [x64-mingw32]、curl 7.58.0 (x86_64-pc-win32) libcurl/7.58.0 WinSSL zlib/1.2.11です。

2.想定する読者

次のような読者を想定しています。

  • ソフトウェアサービスを開発するためにe-Govの仕様を理解したい人
  • 普段から電子申請を利用している人で、e-Govの仕様を理解したい人
  • xml署名を体験してみたい人
  • kijiを動かしてみたい人

3.e-Gov外部連携APIとは

電子申請とは、紙によって行われる申請や届出などの行政手続を、インターネットを利用して自宅や会社のパソコンから行えるようにするものです。e-Govでは、ソフトウェア開発事業者が開発するソフトウェアサービスに対して、各府省が所管する様々な行政手続に関する電子申請を行うための外部連携APIの仕様を公開しています。

e-Govでは、①~④の流れに沿って、手続申請を処理します。このうち、①④についてのAPI仕様が公開の対象となります。
①利用者は、作成済み申請データをe-Govに送信し、到達確認を受ける
②e-Govは、到達済みの申請データを手続所管府省へ送信する
③手続所管府省は、申請データを審査し、その結果をe-Govに登録する
④利用者は、e-Govに照会し、審査結果や公文書データ等を確認する
システム概要.png

e-Gov外部連携APIとしては、①利用者ID登録API、②利用者認証API、③各種電子申請処理APIの3つに大きく分類できます。ソフトウェアサービスでは、これらの外部連携APIを組み合わせて呼び出す事で、目的とする申請手続を進める事ができます。

API全体処理フロー.png

e-Gov外部連携APIを以下に示します。いくつかのAPIについては、実機確認編にてcurlコマンドで動作を確認します。

NO e-Gov外部連携API 内容 実機確認
1 利用者ID登録 e-Govに利用者IDを登録する
2 利用者認証 e-Govにて利用者を認証する
3 一括申請 複数手続をまとめて一括申請する(各手続毎に到達番号が割り振られる)
4 送信案件一覧情報取得 送信案件一覧を取得する
5 申請案件一覧情報取得 申請案件一覧を取得する
6 状況照会 指定した到達番号の申請に対して状況を照会する
7 取下げ 指定した到達番号の申請を取り下げる
8 補正通知一覧取得 補正通知一覧を取得する
9 補正(再提出) 指定した到達番号の申請に対して再提出を行う
10 補正(部分補正) 指定した到達番号の申請に対して部分補正を行う
11 補正(補正申請) 指定した到達番号の申請に対して補正申請を行う(厚生労働省所管の手続のみ対象)
12 公文書・コメント一覧取得 指定した到達番号の申請に対して公文書・コメント一覧を取得する
13 公文書取得 指定した到達番号の申請の公文書をZIP形式にまとめて返す
14 公文書取得完了 e-Govに対して公文書取得完了を通知する
15 公文書署名検証 公文書の官職証明書および官職署名を取得する
16 コメント通知取得 指定した到達番号の申請のコメント通知を取得する
17 コメント通知取得完了 e-Govに対してコメント通知取得完了を通知する
18 電子納付対応金融機関一覧取得 国庫金の電子納付が可能な金融機関一覧を取得する
19 納付情報一覧取得 指定した到達番号に発行された手数料等の納付情報を取得する
20 電子納付金融機関サイト表示 金融機関のネットバンキングに遷移する
21 証明書識別情報追加 e-Govに証明書識別情報を追加する
22 証明書識別情報更新 e-Govで管理する証明書識別情報を更新する
23 証明書識別情報削除 e-Govで管理する証明書識別情報を削除する

3.1 利用者ID登録API

利用者IDとは、ソフトウェアサービスを利用して電子申請を行うユーザを一意に識別するためのIDです。e-Govでは、各ユーザについて最初の1回のみ、利用者IDを登録します。

ソフトウェアサービスでは、e-Govに対して、秘密鍵で利用者IDを署名したXMLを送信します。e-Govでは、XML署名の検証を行い、利用者IDと公開鍵証明書の識別情報を対応させて登録します。

3.2 利用者認証API

e-Govでは、各種電子申請処理APIを実行する前に利用者認証を行います。

ソフトウェアサービスでは、秘密鍵で利用者IDを署名したXMLを、e-Govに送信します。e-Govでは、利用者IDがシステムに登録済みであること及び利用者IDと証明書識別情報の対応関係を確認した上で、問題がなければアクセスキーを発行します。

これ以降、ソフトウェアサービスでは、各種電子申請処理APIを呼び出す際、HTTPヘッダ部にアクセスキーを設定する必要があります。アクセスキーには有効期間があり、有効期間を経過してアクセスキーが失効した場合、各種APIへのアクセスが許可されません。この場合、再度、利用者認証を行って、新しいアクセスキーを取得する必要があります。

3.3 各種電子申請処理API

ユーザがソフトウェアサービスを利用して電子申請手続を進めるシナリオを考えた場合、関連するAPIを組み合わせる事で大きく5つに整理できます。

まず、ユーザは一括申請から状況照会を行います。その後、状況照会の結果より、必要に応じて申請の取下げ又は補正を行います。さらに手続審査の進捗が進む事で発出される公文書・コメントを取得します。
上記とは別のタイミングで、ユーザは必要に応じて電子納付利用可能金融機関一覧及び納付情報一覧を取得したり、e-Govに登録されている証明書識別情報の保守を行います。

以上の内容をまとめます。

NO シナリオ 電子申請処理API
1 一括申請から状況照会まで 一括申請
送信案件一覧情報取得
申請案件一覧情報取得
状況照会
2 申請の取下げ又は補正の実施 取下げ
補正(再提出、部分補正、補正申請)
3 公文書・コメントの取得 公文書・コメント一覧取得
公文書取得
公文書署名検証
公文書取得完了
コメント通知取得
コメント通知取得完了
4 電子納付利用可能金融機関一覧及び
納付情報一覧の取得
電子納付利用可能金融機関一覧取得
納付情報一覧取得
5 証明書識別情報の保守 証明書識別情報追加
証明書識別情報更新
証明書識別情報削除

4.ソフトウェアサービスに求められる情報セキュリティ対策

ソフトウェアサービスは、e-Gov外部連携APIを利用するために、次の情報セキュリティ対策を行う必要があります。

  • ソフトウェアサービスとe-Govの通信経路については、SSL/TLSで暗号化する
  • ソフトウェアサービスは、e-Govにて送信者自身の真正性及び送信データ改ざんの有無を確認するため、電子申請で送付する申請データ及び添付ファイルに対してデジタル署名を行い、決められた書式に格納した上で、e-Govに送信する
  • ソフトウェアサービスでは、ソフトウェアID、利用者ID、アクセスキーを暗号化・難読化する
  • ソフトウェアサービスを利用する際、利用者のアカウント・パスワードによる主体認証を行う
  • ソフトウェアサービスにて、主体認証情報を保存する場合は、その内容を暗号化する

詳細については、「外部連携API 情報セキュリティ要求仕様書」に記載があります。

5.e-Govのインターフェース

e-Govのインターフェースを入出力という視点から整理します。

分類 内容
入力 各API毎のリクエストURI
URIパラメタ(APIバージョン、送信番号、送信期間、到達番号等)
HTTPヘッダ(アクセスキー、ソフトウェアID、Basic認証用ID及びパスワード)
HTTPリクエストボディ(POSTコマンド時のみ申請データ等を指定)
出力 HTTPレスポンスコード(正常時、エラー発生時)
HTTPレスポンスボディ(応答結果XML)

ここでは、入出力データについて整理をした上で、申請データ及び署名付きxmlについて詳細に説明します。

(1)入力

ソフトウェアサービスは、e-Gov外部連携APIを利用するため、各API毎に定められたリクエストURIにアクセスします。リクエストURIにアクセスする際、HTTPヘッダにアクセスキー及びソフトウェアIDを指定します。また、検証環境のみ、Basic認証用ID及びパスワードを合わせて指定します。

POSTコマンドでリクエストURIにアクセスする場合、リクエストのボディ部に申請データ等を指定して送信することで、e-Govに申請手続を依頼する事ができます。申請データとは、申請データセットをe-Govで受付可能な形式に変換したものです。また、GETコマンドでリクエストURIにアクセスする場合、URIパラメタに必要な情報を指定する事で、e-Govから様々な情報を取得する事ができます。

POSTコマンドを利用するe-Gov電子申請APIをまとめます。これらのAPIについては、リクエストのボディ部に①申請データ、②署名付きxml、③その他のいずれかを指定します。

NO 電子申請処理API 申請データ 署名付きxml その他
1 利用者ID登録 × ×
2 利用者認証 × ×
3 一括申請 × ×
4 取下げ × ×
5 補正(再提出) × ×
6 補正(部分補正) × ×
7 補正(補正申請) × ×
8 公文書署名検証 × ×
9 電子納付金融機関サイト表示 × ×
10 証明書識別情報追加 × ×
11 証明書識別情報更新 × ×
12 証明書識別情報削除 × ×

(2)出力

e-Govは、ソフトウェアサービスの依頼に対して、レスポンスを返します。レスポンスには、レスポンスコード及びレスポンスボディ(応答結果XML)が含まれます。ソフトウェアサービスは、レスポンスに応じて後続の処理を行います。

5.1 申請データセット及び申請データ

「申請データセット」とは、構成管理XML、申請書XML、添付ファイルから成るデータセットであり、一括申請、取下げ申請、補正申請で用いられます。

ソフトウェアサービスでは、e-Govで送信者自身の真正性及び送信データ改ざんの有無を確認するため、構成管理XMLに対して署名を行います。さらに、署名が行われた構成管理XML(署名付きxml)を含む申請データセットをzipファイル化してBase64形式にエンコードします。

「申請データ」とは、zipファイルをBase64形式にエンコードした結果をXMLの<FileData>タグに格納したものであり、e-Govが受付可能な形式です。申請データの形式は、各種API毎に定義されています。詳細については、「e-Gov電子申請システム外部連携API API(Version 1)仕様書」に記載があります。
申請データの書式.png

次に構成管理XML、申請書XML、添付ファイルについて説明します。

(1)構成管理XML

申請書のデータ全体の属性情報を格納する論理単位として、申請書XMLに含まれない申請情報やデータ全体の論理単位の関連などを管理します。

構成管理情報の物理ファイルは「構成管理XML」といい、取下げ依頼以外については、申請データセットに必ず1つ含まれます。

一括申請、取下げ依頼、取下げ申請、補正の各申請毎に構成管理XMLの構造が定義されています。また、後述する個別ファイル署名形式においては、申請書XMLに対する構成管理XML、補正に対する構成管理XML、添付書類に対する構成管理XMLについて、それぞれ構成情報のタグ構造が定義されています。

(2)申請書XML

各行政手続において、申請・届出事項を記入する様式です。e-Govでは、申請・届出事項等を格納するXML様式を「申請書」、その物理ファイルを「申請書XML」といいます。

(3)添付ファイル

各行政手続において、申請・届出事項に応じて申請書に添えて提出することが求められる別添文書です。e-Govでは、申請書に格納しないその他の文書等のことを「添付書類」、その物理ファイルを「添付ファイル」といいます。

5.2 署名付きxmlについて

e-Govでは、公開鍵基盤(PKI:Public Key Infrastructure)に基づいて、本人性の確認や文書の改ざんの有無の検知を行うため、ユーザID毎に使用する電子証明書(公開鍵)をユーザIDに紐づけて管理します。そのため、ソフトウェアサービスでは、申請データに対して、ユーザID毎に管理されている電子証明書に対応する秘密鍵を使って署名付きxmlを作成する必要があります。

署名付きxmlとは、xmlファイルに対して電子署名情報を付与したものです。xml署名については、デジタル署名のXML構文を規定するW3C勧告になっています。

電子署名情報とは、①署名値の計算及びダイジェスト計算に使用するアルゴリズムに関する情報、②文書の改ざんを確認するためのハッシュ値、③ハッシュ値に対する電子署名、④電子証明書(公開鍵)です。電子署名情報の形式については、次のとおりです。
電子署名情報の形式.png

Wikiからxml署名の各タグ要素の説明を転載します。e-Govもこの内容に従っています。

  • SignedInfo要素は署名の対象および使用するアルゴリズムを指定する。SignatureMethodおよびCanonicalizationMethod要素はSignatureValue要素により使用され、改ざんから保護するためにSignedInfoに含まれる。
  • Reference要素のリストは署名されるリソースをURIで指定する。Transforms要素においてハッシュを計算する前にリソースに対し適用する全ての変換を、(DigestMethod要素において)ダイジェスト(ハッシュ)アルゴリズムを、リソースに対して適用した結果(これはDigestValue要素においてBase64エンコードされている)を指定する。
  • SignatureValue要素は署名のBase64エンコードされた値である。この値は、SignedInfo要素をCanonicalizationMethod要素で指定されたアルゴリズムによりシリアル化した後の(SignatureMethodの指定により生成される)署名である。(正規化に関する詳細はXML正規化の節を参照のこと)
  • KeyInfo要素は、署名の受信者に対し署名を検証するのに必要となる鍵を取得できるようにするオプション要素である。一般的には一連のX.509証明書を入れることができる。存在しない場合、受信者はデータの内容から鍵を特定することが求められる。

e-Govの署名付きxmlについては、申請データセットに含まれる署名付きxmlと、HTTPボディ部に直接指定する署名付きxmlの2種類があります。前者は一括申請API等、後者はユーザ登録・認証API等に用いられます。

5.2.1 申請データセットに含まれる署名付きxml

申請データセットに含まれる構成管理XMLに対して署名を付与する場合、①申請データ全体に対する署名を付与する「標準形式」、②申請書・添付ファイルそれぞれに対する署名を付与する「個別ファイル署名形式」の2種類があります。

電子署名情報の形式については、「e-Gov 電子申請システム外部連携 API 申請データ仕様 共通データ仕様書」に記載があります。

(1)標準形式

標準形式の申請データは、次の3ファイルから構成されるデータセットです。

  • 構成管理XML(署名情報を含む)
  • 申請書XML
  • 添付書類

これらのファイルは次の関連を持ちます。
ファイル階層(標準形式).png

ソフトウェアサービスでは、次の手順で署名を行い、申請データを作成します。
①構成管理XMLの構成管理情報からハッシュ値を計算する
②申請書XMLからハッシュ値を計算する
③添付書類からハッシュ値を計算する
④秘密鍵でハッシュ値全体を署名する
⑤秘密鍵に対応する証明書情報(公開鍵)を付与する
⑥データセットをzip形式に固めて、Base64形式でエンコードする
⑦申請データに格納する

署名付きxml(標準形式).png

e-Govでは、受け取った申請データより、次の手順で本人確認及びデータ改ざんの有無を検証します。
①申請データより、データセットのBase64形式エンコード結果を取り出す
②Base64形式デコードを行い、zip形式のデータセットに変換する
③zipを解凍し、構成管理XMLを取得する
④構成管理XMLの証明書とe-Govで管理する証明書を比較し、送信者の真正性を確認する
⑤署名値を証明書(公開鍵)で復号して、ハッシュ値を取り出す
⑤構成管理XML、申請書XML、添付ファイルについて、それぞれハッシュ値を計算する
⑥ハッシュ値を比較して、一致していればデータ改ざんが無いと判定する

なお、標準形式に対応する申請手続については、以下があります。

  • 雇用保険被保険者資格取得届/電子申請
  • 雇用保険被保険者資格喪失届(離職票交付なし)/電子申請
  • 雇用保険被保険者氏名変更届/電子申請
  • 雇用保険の事業所の各種変更届出/電子申請
  • 保険関係成立(継続)/電子申請
  • 雇用保険の事業所設置の届出/電子申請
  • 健康保険・厚生年金保険被保険者資格取得届、船員保険・厚生年金保険被保険者資格取得届/電子申請
  • 健康保険・厚生年金保険被保険者氏名変更(訂正)届、船員保険・厚生年金保険被保険者氏名変更訂正届/電子申請
  • 健康保険・厚生年金保険事業所関係変更(訂正)届/電子申請
  • 健康保険・厚生年金保険新規適用届、船員保険・厚生年金保険新規適用船舶所有者届/電子申請
  • 健康保険・厚生年金保険適用事業所所在地名称変更(訂正)届(管轄内)(管轄外)、船員保険・厚生年金保険船舶所有者氏名(名称)住所(所在地)変更届(管轄内)(管轄外)/電子申請

(2)個別ファイル署名形式

個別ファイル署名形式の申請データは、以下5ファイルから構成されるデータセットです。

  • 構成管理XML
  • 申請書XML
  • 申請書に対する構成情報XML
  • 添付書類
  • 添付書類に対する構成情報XML

これらのファイルは次の関連を持ちます。

ファイル階層(個別ファイル署名形式).png

申請書XMLから計算したハッシュ値に対して署名を行い、申請書に対する構成情報XMLに署名値を付加します。また、添付書類から計算したハッシュ値に対して署名を行い、添付書類に対する構成情報XMLに署名値を付加します。標準形式と異なり、構成管理XMLに署名値が存在しません。

ソフトウェアサービスでは、次の手順で署名を行い、申請データを作成します。

①申請書に対する構成情報XMLの構成管理情報からハッシュ値を計算する
②申請書XMLからハッシュ値を計算する
③それぞれのハッシュ値から秘密鍵で署名値を作成する
④秘密鍵に対応する証明書情報(公開鍵)を付与する
⑤添付書類に対する構成情報XMLの構成管理情報からハッシュ値を計算する
⑥添付ファイルからハッシュ値を計算する
⑦それぞれのハッシュ値から秘密鍵で署名値を作成する
⑧秘密鍵に対応する証明書情報(公開鍵)を付与する
⑨データセットをzip形式に固めて、Base64形式でエンコードする
⑩申請データに格納する

署名付きxml(個別ファイル署名形式).png

e-Govでは、受け取った申請データより、逆の手順で本人確認及びデータ改ざんの有無を検証します。

なお、個別ファイル署名形式に対応する申請手続については、以下があります。「連記式」とは、複数名を記述できる書式です。

  • 保険関係成立(継続)/電子申請
  • 雇用保険被保険者資格取得届(連記式)/電子申請
  • 雇用保険被保険者資格喪失届(連記式)/電子申請
  • 雇用保険被保険者資格喪失届(離職票交付あり)/電子申請
  • 下請負人を事業主とする認可/電子申請
  • 雇用保険被保険者資格喪失届提出後の離職票交付の申請/電子申請
  • 雇用保険高年齢雇用継続給付(高年齢雇用継続基本給付金)の申請/電子申請
  • 健康保険・厚生年金保険被保険者報酬月額算定基礎届(CSVファイル添付方式)/電子申請
  • 国民年金被保険者資格取得・種別変更・種別確認(第3号被保険者該当)届書/電子申請

5.2.2 HTTPボディ部に直接指定する署名付きxml

利用者ID登録、利用者認証、証明書識別情報追加・更新・削除に係るAPIについては、HTTPボディ部に署名付きxmlを直接指定します。

電子署名情報の形式については、「e-Gov電子申請システム外部連携API API(Version 1)仕様書」に記載があります。

これらの署名付きxmlは、<ApplData>タグ全体のハッシュ値を計算して、そのハッシュ値に対して署名を行うという点で共通しています。次のような手順で署名情報を付与します。
①<ApplData>タグ全体でハッシュ値を計算する
②ハッシュ値を秘密鍵で署名する
⑤秘密鍵に対応する証明書情報(公開鍵)を付与する
HTTPボディ部に指定する署名付きxml.png

なお、利用者ID登録、利用者認証の場合、<ApplData>タグに公開鍵証明書を指定しません。

6.電子申請処理の状態遷移について

e-Govでは、申請処理の進捗状況に応じて、メインステートとサブステートに分けて、状態を管理します。

6.1 メインステート

各手続については、進捗状況に応じて(処理中)→(到達)→(審査中)→(審査終了)→(手続終了)の順に状態が遷移します。

各状態は次の意味があります。

状態名 内容
処理中 申請データに対して、各種チェックを行っている状態
到達 申請データが正常に受付された状態
審査中 申請データに対して、府省側で審査を行っている状態
審査終了 申請データに対する府省側の審査が終了している状態
手続終了 申請データに対する手続が終了している状態
  • e-Govは、申請データに対して、各種チェックを行います。各種チェックとは、ファイル構成チェック、XMLスキーマチェック、整合性チェック、形式チェックです。問題なければ、(処理中)から(到達)に遷移します。
  • 府省側の審査が始まると、(到達)から(審査中)に遷移します。
  • 府省側の審査が完了すると、(審査中)から(審査終了)に遷移します。
  • ソフトウェアサービスで公文書を取得した後、e-Govに対して公文書取得完了を通知すると、(審査終了)から(手続終了)に遷移します。

6.2 サブステート

(1)取り上げ

(到達)又は(審査中)において、ソフトウェアからe-Govに手続の取り下げを要求すると、サブステートが(なし)→(取下げ処理中)となり、最終的にはメインステート(手続終了)+サブステート(取下げ済み)となります。

(2)補正

(審査中)において、手続所管府省より申請データに対して補正指示が出た場合、(サブステート)が(なし)→(補正処理待ち)となります。補正指示の内容に応じて、ソフトウェアサービスは次のように後続するシーケンスを切り分ける必要があります。

補正種別が"再提出"の場合、ソフトウェアサービスが補正(再提出)を要求すると、メインステート(手続終了)+サブステート(再提出済み)となります。
補正種別が"部分補正"の場合、ソフトウェアサービスが補正(部分補正)を要求すると、サブステート(なし)となります。

以上を状態遷移図に表現します。

e-Gov手続状態遷移図.png

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

【ポートフォリオ】ブックマーク管理アプリ【Ruby on Rails】

Railsのポートフォリオを作成したので、紹介のため記事を作成しておこうと思います。

↓動画で紹介した内容はこちら↓
Youtubeでのアプリ紹介

↓アプリのURLはこちら↓
アプリのURL

↓GitHubのURLはこちら↓
GitHub

【アプリ開発の背景】
プログラミングの勉強をしている時、色々なサイトを見ますよね!特にQiitaは後から読むためにストックに入れることも多いと思います。
その他サイトにも有益なことが書かれているのでブクマをしますね。後から見ようと思ったらゴチャゴチャして不便でした…。(どこに何が入っているかわからない‥)

その悩みを解決するためにQiitaのストック記事と自分の投稿記事(限定共有含む)を同期させ、自分で好きなURLを追記できるアプリを作成しました。
検索機能もつけてるので、「あの記事どこだっけ‥」の状況を解決できると思います!

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

リーダブルコード読んだのでメモ

未経験エンジニアとして実務に入ったので、リーダブルコードを読んだのでそのメモ。

1章 理解しやすいコード

Point

コードは理解しやすくしなければならない。
コードは他の人が最短時間で理解できるように書かなければいけない。
6ヶ月後の自分がみて、理解しやすいコードかが大事!
コードは短くした方がいいけど、理解するまでにかかる時間を短くすることが大事。

このコードは理解しやすいか?

を一歩下がって自問自答することが大事!

 2章 名前に情報を詰め込む

Point

名前に情報を詰め込む

名前を見ただけで情報を読み取るようにすること

・明確な単語を選ぶ
→空虚な単語は避けるべき。「get」とかはあまり明確な単語ではない。
どこからのget?とわからなくなる。

例えば、getではなく、状況に応じてfetch/downloadなどを使う。

・汎用的な名前を避ける
→tmpやfooなどの空虚な名前は避けよう。
具体的な目的や値を表す名前をつけよう。
戻り値retval×
sum_squares○

なので、tmp/it/retvalなどの汎用的な名前を使うときはそれ相応の理由を用意しよう!
単なる怠慢でこれを使うのはナンセンス!「命名力」!

・抽象的な名前よりも具体的名前を使う
→より適切な名前を使う

・接尾辞を使って情報を追加する
→絶対に知らせないといけない大切な情報の場合「変数名」に追加すればいい。
値の単位など、delay→delay_secsなどより明確に。
セキュリティの面でもそうだ。
password→plaintext_password(passwordはプレインテキストなので、処理前に暗号化すべき)

変数の意味を理解してもらわないといけないところに属性を追加しておく。

・名前の長さを決める
→単純に覚えにくいし、画面占領してしまう、コードの量が増える。
スコープが小さければ短い名前でもいい。
識別子のスコープが大きければ、名前に十分な情報を詰め込んで明確にする必要がある。
省略も行いがちだが、プロジェクト固有の省略形はダメ。新しいメンバーは理解できない。

・名前のフォーマットで情報を伝える
→アンダースコア・ダッシュ・大文字を使って名前に情報を詰め込むこともできる。
どんな規約を使うかどうかはチームで決める。一貫性が大事。

例えば、クラスのメンバ変数にアンダースコアを付けてローカル変数と区別する。

3章 誤解されない名前

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

Docker+Sinatraで簡単なチャットアプリをつくる練習

Rubyの使い方をよく忘れるので、リハビリ用にRubyで簡単なWebアプリを作りました。また忘れた時のために、作る方法を書き残しておきます。

つくるのはNopochatというクソチャットアプリです。
投稿に「~も」というかわいらしい語尾が付くことで、言いづらい内容も心理的安全性を保ったまま送ることができる画期的なチャットですも。

image.png

なお本稿で紹介する内容はあくまでプログラムを書く練習のためのものであり、本番環境で運用することは想定していません。

環境

Dockerを利用します。Rubyのインストールは必要ありません。

  • Docker version 19.03.12
  • docker-compose version 1.26.2

Sinatraの立ち上げ

まず適当なディレクトリを切って、開発を始めます。

$ mkdir sinatra-chat && cd $_

プロジェクトをGitで管理する場合は、GitHubのgitignoreを取ってきてセットするのが良いと思います。

$ git init
$ curl https://raw.githubusercontent.com/github/gitignore/master/Ruby.gitignore -o .gitignore

https://hub.docker.com/_/ruby から使いたいバージョンのRubyイメージを取ってきてください。今回は2.7-slimを使います。

まずGemfileを初期化します。

$ docker run --rm --volume $(pwd):/app --workdir /app ruby:2.7-slim bundle init
$ docker run --rm --volume $(pwd):/app --workdir /app ruby:2.7-slim bundle add sinatra

GemfileGemfile.lockが生成されたのを確認したら、Dockerfileを記述していきます。

Dockerfile
FROM ruby:2.7-slim
WORKDIR /app

COPY Gemfile ./
COPY Gemfile.lock ./
RUN bundle config --local set path 'vendor/bundle'
RUN bundle install

CMD bundle exec ruby index.rb
docker-compose.yml
version: '3'
services:
  app:
    build: .
    volumes:
      - .:/app
      - /app/vendor/bundle
    ports:
      - 127.0.0.1:4567:4567

アプリケーションの本体となるindex.rbを作成します。

index.rb
require 'sinatra'

configure do
  set :bind, '0.0.0.0'
end

get '/' do
  'Hello Sinatra!'
end

http://localhost:4567/ にアクセスし、Hello Sinatra!と表示されたら成功です

ブラウザリロードによる再読み込みの有効化

そのままでは、ファイルを編集してもSinatraサーバを再起動しなければ反映されません。
開発を始める前に、リロードでの再読み込みを有効にすると便利です。

$ docker-compose run --rm app bundle add sinatra-contrib
index.rb
 require 'sinatra'
+require 'sinatra/reloader' if settings.development?

チャット機能の開発

チャット内容は後で永続化するので、とりあえず今は@@chatsというクラス変数に格納していきます。

index.rb
get '/' do
  @@chats ||= []
  erb :index, locals: {
    chats: @@chats.map{ |chat| add_suffix(chat) }.reverse
  }
end

post '/' do
  @@chats ||= []
  @@chats.push({ content: params['content'], time: Time.now } )
  redirect back
end

def add_suffix(chat)
  { **chat, content: "#{chat[:content]}も" }
end

HTMLテンプレートにはerbを使います。views/というディレクトリを切って、そこにindex.erbを格納すると、erb :indexで呼び出すことができます。

views/index.erb
<form action="/" method="post">
  <input name="content" placeholder="投稿" />
  <button type="submit">送信</button>
</form>

<table>
  <% chats.each do |chat| %>
    <tr>
      <td><%= chat[:content] %></td>
      <td><%= chat[:time] %></td>
    </tr>
  <% end %>
</table>

これで最低限、チャットができるようになります。

データベースへの保存

チャット内容をMySQLに保存します。mysql2 Gemをインストールします。

$ docker-compose run --rm app bundle add mysql2

https://hub.docker.com/_/mysql からMySQLの好きなバージョンを取ってきて使います。またappの方にも接続情報を環境変数としてセットします。

docker-compose.yml
 version: '3'
 services:
   app:
     build: .
     volumes:
       - .:/app
       - /app/vendor/bundle
     ports:
       - 127.0.0.1:4567:4567
+    environment:
+      - MYSQL_HOST=db
+      - MYSQL_USER=root
+      - MYSQL_PASS=secret
+      - MYSQL_DATABASE=nopochat_development
+  db:
+    image: mysql:5.7
+    volumes:
+      - .:/app
+      - /var/lib/mysql
+    environment:
+      - MYSQL_ROOT_PASSWORD=secret
index.rb
 require 'sinatra'
 require 'sinatra/reloader' if settings.development?
+require 'mysql2'
(以下略)

mysql2 Gemの利用に必要なパッケージをインストールします。

Dockerfile
 FROM ruby:2.7-slim
 WORKDIR /app
+RUN apt-get update && apt-get install -y \
+  build-essential \
+  libmariadb-dev \
+  && apt-get clean \
+  && rm -rf /var/lib/apt/lists/*
(以下略)

セットした環境変数に基づいてデータベースクライアントを取得するメソッドを定義します。

index.rb
def db_client()
  Mysql2::Client.default_query_options.merge!(:symbolize_keys => true)
  Mysql2::Client.new(
    :host => ENV['MYSQL_HOST'],
    :username => ENV['MYSQL_USER'],
    :password => ENV['MYSQL_PASS'],
    :database => ENV['MYSQL_DATABASE']
  )
end

今回はGET /initializeにアクセスしたらデータベースを初期化する仕様とします(実際の運用ではこのような仕様はあり得ませんが...)

index.rb
get '/initialize' do
  client = Mysql2::Client.new(
    :host => ENV['MYSQL_HOST'],
    :username => ENV['MYSQL_USER'],
    :password => ENV['MYSQL_PASS']
  )
  client.query("DROP DATABASE IF EXISTS #{ENV['MYSQL_DATABASE']}")
  client.query("CREATE DATABASE IF NOT EXISTS #{ENV['MYSQL_DATABASE']} CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci")
  client = db_client
  client.query(<<-EOS
      CREATE TABLE IF NOT EXISTS chats (
        id   INT AUTO_INCREMENT,
        name TEXT,
        content TEXT,
        time DATETIME,
        PRIMARY KEY(id)
    )
    EOS
  )
  redirect '/'
end

chatsというテーブルからデータを出し入れするメソッドを定義します。

index.rb
def chat_push(content, name="名無し")
  db_client.prepare(
    "INSERT into chats (name, content, time) VALUES (?, ?, NOW())"
  ).execute(name, content)
end

def chats_fetch()
  db_client.query("SELECT * FROM chats ORDER BY time DESC")
end

定義したメソッドを使って、GET /POST /を書き換えます。

index.rb
 get '/' do
-  @@chats ||= []
+  chats = chats_fetch
   erb :index, locals: {
-    chats: @@chats.map{ |chat| add_suffix(chat) }.reverse
+    chats: chats.map{ |chat| add_suffix(chat) }
   }
 end

 post '/' do
-  @@chats ||= []
-  @@chats.push({ content: params['content'], time: Time.now } )
+  chat_push(params['content'])
   redirect back
 end

アプリを起動して http://localhost:4567/initialize にアクセスすると、立ち上がります。
これでアプリを再起動しても、いちどチャットした内容が消えることはありません。

ログイン機能

セッションストレージにはDB(MySQL)を利用します。
ユーザ名とパスワードを持つusersテーブルと、セッションを保存するsessionsテーブルを定義します。なお本来passwordは暗号化してハッシュを持つようにします。また、sessionsの定期的な削除処理も必要です。

index.rb
  client.query(<<-EOS
    CREATE TABLE IF NOT EXISTS users (
      id   INT AUTO_INCREMENT,
      name VARCHAR(255) UNIQUE,
      password TEXT,
      PRIMARY KEY(id),
      INDEX key_index (name)
    );
    EOS
  )
  client.query(<<-EOS
    CREATE TABLE IF NOT EXISTS sessions (
      id   INT AUTO_INCREMENT,
      session_id VARCHAR(255) UNIQUE,
      value_json JSON,
      PRIMARY KEY(id),
      INDEX key_index (session_id)
    );
    EOS
  )
  user_push('admin', 'admin')

ユーザの追加・認証処理を定義します。

index.rb
def user_push(name, pass)
  db_client.prepare(
    "INSERT into users (name, password) VALUES (?, ?)"
  ).execute(name, pass)
end

def user_fetch(name, pass)
  result = db_client.prepare("SELECT * FROM users WHERE name = ?").execute(name).first
  return unless result
  result[:password] == pass ? result : nil
end

セッションの追加・取得処理を定義します。

index.rb
def session_save(session_id, obj)
  db_client.prepare(
    "INSERT into sessions (session_id, value_json) VALUES (?, ?)"
  ).execute(session_id, JSON.dump(obj))
end

def session_fetch(session_id)
  return if session_id == ""
  result = db_client.prepare("SELECT * FROM sessions WHERE session_id = ?").execute(session_id).first
  return unless result
  JSON.parse(result&.[](:value_json))
end

cookieを使うためrequire 'sinatra/cookies'を追加します。

index.rb
 require 'sinatra'
 require 'sinatra/reloader' if settings.development?
+require 'sinatra/cookies'
 require 'mysql2'

POST /loginGET /logoutを定義します。

index.rb
post '/login' do
  if user = user_fetch(params['name'], params['pass'])
    cookies[:session_id] = SecureRandom.uuid if cookies[:session_id].nil? || cookies[:session_id] == ""
    session_save(cookies[:session_id], { name: user[:name] })
  end
  redirect back
end

get '/logout' do
  cookies[:session_id] = nil
  redirect back
end

GET /POST /を修正します。

index.rb
 get '/' do
+  name = session_fetch(cookies[:session_id])&.[]("name")
   chats = chats_fetch
   erb :index, locals: {
+    name: name,
     chats: chats.map{ |chat| add_suffix(chat) }
   }
 end

 post '/' do
-  chat_push(params['content'])
+  name = session_fetch(cookies[:session_id])&.[]("name")
+  chat_push(params['content'], name)
   redirect back
 end

Viewのフォーム部分を書き換え、ログインしていない時はログインフォームが、ログイン後には投稿フォームが表示されるようにします。

vieqs/index.erb
<% if name %>
  <p>こんにちは<%= name %>さん</p>
  <a href="/logout">ログアウト</a>
  <form action="/" method="post">
    <input name="content" placeholder="投稿" />
    <button type="submit">送信</button>
  </form>
<% else %>
  <form action="login" method="post">
    <input name="name" placeholder="ユーザ名">
    <input name="pass" placeholder="パスワード">
    <button type="submit">ログイン</button>
  </form>
<% end %>

http://localhost:4567/initialize にアクセスし、adminユーザでログインできたら成功です。

Appの複数台化

docker-compose.ymlを下記のように修正します。
appを2つにして、あらたにWebサーバとなるNginxのコンテナを追加します。
Sinatraの4567ポートを閉じて、Nignx用の8080ポートを開けます。

Nginxは https://hub.docker.com/_/nginx から好きなバージョンを取ってきて使います。

docker-compose.yml
 version: '3'
 services:
-  app:
+  app1: &app
     build: .
     volumes:
       - .:/app
       - /app/vendor/bundle
-    ports:
-      - 127.0.0.1:4567:4567
     environment:
       - MYSQL_HOST=db
       - MYSQL_USER=root
       - MYSQL_PASS=secret
       - MYSQL_DATABASE=nopochat_development
+  app2:
+    <<: *app
+  web:
+    image: nginx:1.19-alpine
+    volumes:
+      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
+    ports:
+      - 127.0.0.1:8080:80
(以下略)

Nginxの設定ファイルを配置します。app1app2を振り分けるようにします。

nginx/default.conf
upstream apps {
  server app1:4567;
  server app2:4567;
}

server {
  listen 80;
  proxy_set_header Host $host:8080;

  location / {
    proxy_pass http://apps;
  }
}

http://localhost:8080/ にアクセスします。もしログイン処理がうまくできていないと、アクセスするたびにセッションが切り替わってログアウトしたりします。

RubyからRustを呼び出す

序盤に挙げた、語尾に「も」を付与する処理

def add_suffix(chat)
  { **chat, content: "#{chat[:content]}も" }
end

これをRustで書いてRubyから呼び出してみます。

https://hub.docker.com/_/rust から好きなバージョンを取ってきます。今回はrust:1.46-slimを使います。
下記コマンドで、rust_libというディレクトリにRustのプロジェクトを作成します。

$ docker run \
  --rm \
  --volume $(pwd):/app \
  --workdir /app \
  --env USER=root \
  rust:1.46-slim cargo new rust_lib --lib
$ cd rust_lib 

お好みでGitHubの.gitignoreから取ってきてセットします。

$ curl https://raw.githubusercontent.com/github/gitignore/master/Rust.gitignore -o .gitignore

Cargoでlibc crateを追加、crate-typeを"dylib"に指定します。

rust_lib/Cargo.toml
[dependencies]
libc = "0.2.77"

[lib]
name = "rust_lib"
crate-type = ["dylib"]

Rustで処理を書いていきます。

rust_lib/src/lib.rs
extern crate libc;
use libc::*;
use std::ffi::{CStr, CString};

#[no_mangle]
pub extern fn add_suffix(s: *const c_char) -> CString {
    let not_c_s = unsafe { CStr::from_ptr(s) }.to_str().unwrap();
    let not_c_message = format!("{}も", not_c_s);
    CString::new(not_c_message).unwrap()
}

ビルドします。

$ docker run \
  --rm \
  --volume $(pwd):/app \
  --workdir /app \
  rust:1.46-slim cargo build

rust_lib/target/release/librust_lib.soがビルドされていれば成功です。

$ nm target/release/librust_lib.so | grep add_suffix
00000000000502c0 T add_suffix

Ruby FFI Gemを使って、RubyからRustを呼び出す処理を書いていきます。

$ docker-compose run --rm app1 bundle add ffi
index.rb
 require 'sinatra'
 require 'sinatra/reloader' if settings.development?
 require 'sinatra/cookies'
 require 'mysql2'
+require 'ffi'

extend FFI::Libraryして、先程のrust_lib/target/release/librust_lib.soを読み込んで使います。FFIのwikiを参考に、引数と返り値の型を指定します。

index.rb
# Rustから呼び出すモジュールの設定
module RustLib
  extend FFI::Library
  ffi_lib('rust_lib/target/release/librust_lib.so')
  attach_function(:add_suffix, [:string], :string)
end

GET /を修正します。

index.rb
 get '/' do
   name = session_fetch(cookies[:session_id])&.[]("name")
   chats = chats_fetch
   erb :index, locals: {
     name: name,
-    chats: chats.map{ |chat| add_suffix(chat) }
+    chats: chats.map{ |chat| { **chat, content: RustLib::add_suffix(chat[:content]).force_encoding("UTF-8") } }
   }
 end

以上です。完成品は下記になります。

https://github.com/s2terminal/sinatra-chat

参考

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

ページ遷移後はJSが読まれず、ロードするとJSが読み込まれるという不具合に直面した件

状況

Railsを利用しフリマアプリ開発中に体験したエラーです。商品出品機能実装中、価格入力でイベント発火し出品する商品の手数料を自動計算し表示する機能を実装しました。

自動表示されることを確認後、トップページから商品出品ページのリンクに飛び商品出品機能が動作しているか確認していると、価格を入力しても手数料が表示されないという不具合を発見しました。

コードの記述等のチェックをしていると、いつの間にか手数料の自動表示機能が復活しているという現象に見舞われパニックを起こしました。

解決の糸口

色々やっていて、いつの間にか機能が復活なんてありえないので、コードを1つずつ書き換えたりボタンを1つ押す毎に挙動がどう変化するのか試したところ、どうやらページ遷移した直後のページではJavaScriptが読み込まれず、リンク先に飛んだ後にもう1度ページ更新をするとJavaScriptが読み込まれているということが発覚しました。

window.addEventListener('load',function(){
この中に手数料表示のコード
});

ページ読み込みが全てのJavaScriptのイベント発火に必要なので、ページを飛んだ時にはloadが読み込まれない仕様なのかと考えました。しかし、調べていくと、loadはページ遷移しただけでも読み込まれるイベントであるとのことで、ますますJSが動かないことに混乱することになりました。

解決

結局JavaScriptのコード自体には何も問題はありませんでした。そこで、そもそもJavaScriptがどのように読み込まれているか確認をすることにしました。railsでJavaScriptを読み込む流れとしては以下のような感じです。

application.html.erbの共通のビューのheadタグの中で読み込んでいます。

<head>
  <title>Furima</title>
  <%= csrf_meta_tags %>
  <%= csp_meta_tag %>
  <script type="text/javascript" src="https://js.pay.jp/v1/"></script>
  <%= stylesheet_link_tag 'application', media: 'all'%>
  <%= javascript_pack_tag 'application' %> ←この記述でapplication.jsを読み込んでいます。
</head>

app/javascript/packs/application.jsでrailsで使うJavaScriptをまとめて読み込んでいます。

中略
require("@rails/ujs").start()
require("turbolinks").start() ←不具合の原因(下記で説明)
require("@rails/activestorage").start()
require("channels")
require("../fee.js")
require("../card.js")
中略

読み込んでいるものを1つずつ調べていくと、ようやく原因が判明しました。
不具合の原因はturbolinksをapplication.jsで読み込んでいることでした。turbolinksは大規模な開発等では読み込むJavaScriptが多くなるので、それを効率よく読み込めるようにする為のものみたいです。

しかし、悪い部分もあってページ遷移した直後にloadのイベント発火を読み込んでくれない現象を起こすことがあるようです。

railsではアプリを作成するとapplication.jsでturbolinksを読み込む記述が自動で記述されてしまいます。turbolinksは大規模サイトでJavaScriptがすぐ読まれるようにする為のもので個人でポートフォリオを作るような場合は不要であるので、こちらをコメントアウトして読まれなくすると、ページ遷移後にJavaScriptが起動しないという不具合を解消することができました。

大規模な開発だとデフォルトで読み込んでいるturbolinks等も別のものに書き換えたりして使うこともあるらしいです。

まだまだ初学者で間違えた知識を書いている可能性もあるので、過ちがあれば教えていただけると嬉しいです。

拙い文章であったかと思いますが、ありがとうございました。

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

簡易LISP処理系の実装例【各言語版まとめ】

この記事は,様々なプログラミング言語でS式入出力および基本リスト処理を実装した上で,John McCarthy氏の原初のLISPインタプリタ記述をPaul Graham氏がCommon Lispで実装した"McCarthy's Original Lisp"jmc.lisp)について,各言語向けに移植・動作確認してみた記述例のリンク集および共通解説をまとめたものです.

なお,複数の種類の括弧を用いた記述の簡易パーサ,基本リスト処理のみを実装した例をまとめた記事もあり,こちらの記述から抜粋・修正して組み込んでいる場合もあります.

この記事の方が新しく,少しずつ整理していますが,各リンク先記事との整合性が合っていなかったり,記述や説明が重複していたりする箇所があるかもしれません.御了解いただけますと幸いです.

実装例の趣旨

LISP系言語については,開発当初より『LISP自身でそのLISP処理系を記述する』という,超循環評価器(meta-circular evaluator)としての実装が行われています.最低限の機能をもったLISP処理系であればそのような実装は可能であり,しかも,その評価器の仕組みはとても簡単です.McCarthy's Original Lispの他,SICP 4.1など,Webでも多くの記述例が公開されています.

これらを参照すれば,LISP系以外の他のプログラミング言語でも,超循環評価器としての性質をもつ同じLISP処理系が容易に実装でき,言語処理系実装の入門用として最適…のはずなのですが,LISP処理系ならば標準で装備している,字句・構文を規定する『S式』の入出力処理,および,S式に基づく基本リスト処理(car,cdr,cons,eq,atom)の実装の方が開発言語ごとの手間が圧倒的にかかり,それが敷居になっているところがあります.

そこで,各プログラミング言語で簡単なS式入出力および基本リスト処理の実装例を別途作成し,"McCarthy's Original Lisp"を可能な限りそのまま移植・動作確認することで,言語処理系実装の最初の敷居を下げてみよう,というのが,今回の各実装例の趣旨です.

処理系の概要

次のサンプルの通り,名前付けや関数定義の記述方法がなく,ひとつのまとまったS式のみの処理を行うものですが,ダイナミックスコープということもあり,lambda式を用いて再帰関数を定義して利用することも可能です(SchemeのletrecやCommon Lispのlabelsなどの代わり).

(car (cdr '(10 20 30)))
=> 20

((lambda (x) (car (cdr x))) '(abc def ghi))
=> def

((lambda (f x y) (f x (f y '()))) 'cons '10 '20)
=> (10 20)

((lambda (f x y) (f x (f y '())))
 '(lambda (x y) (cons x (cons y '())))
 '10 '20)
=> (10 (20 ()))

((lambda (assoc k v) (assoc k v))
 '(lambda (k v)
    (cond ((eq v '()) nil)
          ((eq (car (car v)) k)
           (car v))
          ('t (assoc k (cdr v)))))
 'Orange
 '((Apple . 120) (Orange . 210) (Lemmon . 180)))
=> (Orange . 210)

評価器本体の実装内容は次の通り.

  • "McCarthy's Original Lisp"を基にした,超循環評価器としての性質をもつLISP処理系
  • 数字を含むアトムは全てシンボルとし,変数の値とする場合はquote')を使用
  • 構文としてquoteの他,condlambdaが使用可能
  • 組込関数:cons car cdr eq atom(内部でコンスセルを作成)
  • 真偽値はt(真),および,nil(偽)=空リスト(+内部用記号)
  • 評価器実装専用として,caarassocなどのユーティリティ関数を定義
  • エラーチェックなし,モジュール化なし,ガーベジコレクションなし

また,S式入出力およびリスト処理実装の構成は次の通り.

  • 基本リスト処理:cons car cdr eq atom
  • S式字句解析:1行の文字列から( ) 'を字句として,空白を区切り記号として,文字列配列を生成
  • S式構文解析:( )の括りをconsでリスト化,'(quote ...)を挿入,.はコンスセルを生成
  • S式出力:ドット対簡略その他の表現に基づくリスト構造などを表示,または,文字列として出力
  • REP (no Loop):文字列1行読み込み→S式抽象構文木生成→評価→S式出力をまとめた関数を定義

評価器の解説

s_evalの処理内容を箇条書きにすると,次のようになります.

  • 引数としてS式eと環境変数をとる.
  • eが真偽値を示す文字列ならば所定の真偽値を返す.
  • eがリスト構造ではないならば束縛変数とみなし,対応する値を環境変数から取得して返す.
  • eがリスト構造であり,先頭の要素e1がリスト構造ではないならば,次の処理を行う.
    • e1quoteならば,eの2番目の要素をそのまま値として返す.
    • e1atom eq car cdr consならば,引数要素を評価した後に関数適用を行い,その結果を返す.
    • e1condならば,条件式と処理を組にしたリストをevconに渡し,その結果を返す.
    • それ以外の場合は,e1をlambda式の束縛変数とみなし,対応するlambda式を環境変数から取得してeの先頭要素として置き換え,あらためて評価した結果を返す.
  • e1もリスト構造であり先頭の要素がlambdaならば,lambda式の値適用とみなし,次の処理を行う.
    • 適用する値要素をリストにしたものをevlisに渡し,それぞれの要素が評価された結果を再度リストとして受け取る.
    • lambda式の各引数と評価済の各値要素を対応付けたリストを作り,環境変数に追加する.
    • lambda式の処理本体を,更新後の環境変数を用いて評価した結果を返す.
  • eが上記以外の構成の場合は,エラーとして()を返す.

肝となるのは,lambda式を別のlambda式の引数に束縛した後の,lambda式の値適用,たとえば,

(s_eval '((lambda (f) (f '(a b c))) '(lambda (x) (cdr x))) '()))
=> (s_eval '(f '(a b c)) '((f (lambda (x) (cdr x))) . ()))
=> (s_eval '((lambda (x) (cdr x)) '(a b c)) '((f (lambda (x) (cdr x))) . ()))
=> (s_eval '(cdr x) '((x (a b c)) (f (lambda (x) (cdr x))) . ()))
=> (cdr (s_eval 'x '((x (a b c)) (f (lambda (x) (cdr x))) . ())))
=> (cdr '(a b c))
=> (b c)

のように実行されていく処理でしょうか.環境変数内でlambda式に名前が付くことによって,その名前で自分自身を呼び出す再帰処理が定義可能です.

環境変数は,引数持ち回りとはいえ,ひとつのみです.ですのでダイナミックスコープとなるのですが,今回の評価器は,lambda式のみのS式はエラーとし(というよりも,真偽値およびクォートされた記述以外は値としてそのまま返さない),lambda式の処理本体としてlambda式を記述する,すなわち,lambda式を返すlambda式は処理できません.高階関数機能としては,いわゆる第二級オブジェクト相当となります.

実のところ,真偽値のように,lambda式のみの場合はそのまま返すこともできなくはないのですが,lambda式内にローカル環境変数を保持する,クロージャ機能を実装したレキシカルスコープとしないと,名前衝突(funarg)の問題が起きます.そして,レキシカルスコープでは別のlambda式内のローカル環境変数の値を適用できませんから,再帰処理定義のためには,グローバルな環境変数に変数束縛を直接追加する構文や,Yコンビネータのような不動点コンビネータが必要となってきます.

備考

記事に関する補足

  • 趣旨としては他にも,簡易処理系とはいえ,ホスト言語の様々な機能を活用しないと実装できないことから,その言語を本格的に学ぶための題材としても適切な種類と規模,というのもあります.とりあえず,Haskellのリストモナドの妙なクセは一通りわかった(えっ).

  • 実行サンプルのScheme版,Common Lisp版はそれぞれ次の通り.

sample.scm
(car (cdr '(10 20 30)))
=> 20

((lambda (x) (car (cdr x))) '(abc def ghi))
=> def

((lambda (f x y) (f x (f y '()))) cons '10 '20)    ; 引数として渡された関数名はクォートする必要がない
=> (10 20)

((lambda (f x y) (f x (f y '())))
 (lambda (x y) (cons x (cons y '())))    ; 引数として渡されたlambda式もクォートする必要がない
 '10 '20)
=> (10 (20 ()))

(letrec ((assoc_ (lambda (k v)
                      (cond ((eq? v '()) '())
                            ((eq? (car (car v)) k)
                             (car v))
                            (else (assoc_ k (cdr v)))))))
  (assoc_ 'Orange
          '((Apple . 120) (Orange . 210) (Lemmon . 180))))
=> (Orange . 210)
sample.lsp
(car (cdr '(10 20 30)))
=> 20

((lambda (x) (car (cdr x))) '(abc def ghi))
=> DEF    ; シンボルとしてのアルファベット表示は全て大文字となる

((lambda (f x y) (funcall f x (funcall f y '()))) 'cons '10 '20)    ; 引数として渡された関数はfuncallを用いて実行
=> (10 20)

((lambda (f x y) (funcall f x (funcall f y '())))    ; 引数として渡されたlambda式はfuncallを用いて実行
 (lambda (x y) (cons x (cons y '())))    ; lambda式はクォートする必要がない
 '10 '20)
=> (10 (20 NIL))

(labels ((assoc_ (k v)
           (cond ((eq v '()) '())
                 ((eq (car (car v)) k)
                  (car v))
                 (t (assoc_ k (cdr v))))))
  (assoc_ 'Orange
          '((Apple . 120) (Orange . 210) (Lemmon . 180))))
=> (ORANGE . 210)

更新履歴

2020-09-16:評価器の解説を追加
2020-09-16:初版公開

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

ストロングパラメーターを設定する際のrequireについて

動作環境
Ruby 2.6.5
Rails 6.0.3.2

ストロングパラメーターを設定する際にrequireが必要な場合と必要でない場合の違いについて、理解するのに時間がかかってしまったので投稿してみました。

requireが必要な場合

new.html.erb
<%= form_with model:@hoge, local: true do |f| %>
  <%= f.text_area :fuga %>
<%= f.submit "投稿する" %>
hoges_controller.rb
def create
  @hoge = Hoge.create(hoge_params)
end

private
def hoge_params
  params.require(:hoge).permit(:fuga)
end

上記の場合、ストロングパラメーターにrequire(:hoge)が必要となります。なぜなら、投稿するをクリックした際に送信されるparamsのhogeの中にfugaが含まれているためです。
実際に、binding.pryを使ってcreateアクション内でparamsを確認すると、以下のようになります。fugaには「例」と入力しているとします。

{"authenticity_token"=>"+wXNK4Z3C0wrq4AfslPS5zl/2LSUE6BvV+23hQpkHryrsVzPb0siDIkarIsNYLK2R502fuXlqQ==", "hoge">={"fuga"=>"例"},"commit"=>"投稿する", "controller"=>"hoges", "action"=>"create"}

requireが必要でない場合

new.html.erb
<%= form_with url:hoge_path, local: true do |f| %>
  <%= f.text_area :fuga %>
<%= f.submit "投稿する" %>
hoges_controller.rb
def create
  @hoge = Hoge.create(hoge_params)
end

private
def hoge_params
  params.permit(:fuga)
end

上記の場合、ストロングパラメーターにrequireが必要ないです。なぜなら、投稿するをクリックした際に送信されるparamsの中にhogeが存在せず、直接fugaが含まれているためです。
実際に、binding.pryを使ってcreateアクション内でparamsを確認すると、以下のようになります。fugaには「例」と入力しているとします。

{"authenticity_token"=>"+wXNK4Z3C0wrq4AfslPS5zl/2LSUE6BvV+23hQpkHryrsVzPb0siDIkarIsNYLK2R502fuXlqQ==", "fuga"=>"例","commit"=>"投稿する", "controller"=>"hoges", "action"=>"create"}

上記の通り、先ほどと違ってhogeが存在しないことがわかります。

まとめ

つまり、ストロングパラメーターのrequireとは、paramsの中にある入力したもの(今回の場合はfuga)をある要素(今回の場合はhoge)から引っ張り出すために使われていることがわかりました。

雑にまとめると、form_withでmodelを指定している時はrequireが必要。指定していない時はrequireが必要でないと言えると思います。

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

【Ruby on Rails】検索機能(モデル、方法選択式)

目標

search.gif

開発環境

ruby 2.5.7
Rails 5.2.4.3
OS: macOS Catalina

前提

※ ▶◯◯ を選択すると、説明等が出てきますので、
  よくわからない場合の参考にしていただければと思います。

流れ

1 controllerの作成
2 routingの編集
2 viewの作成

今回はモデルを選択して検索する方法で進めます。

controllerの作成

ターミナル
$ rails g controller searchs
app/controllers/searchs.controller.rb
class SearchsController < ApplicationController
  def search
    @model = params["model"]
    @content = params["content"]
    @method = params["method"]
    @records = search_for(@model, @content, @method)
  end

  private
  def search_for(model, content, method)
    if model == 'user'
      if method == 'perfect'
        User.where(name: content)
      else
        User.where('name LIKE ?', '%'+content+'%')
      end
    elsif model == 'post'
      if method == 'perfect'
        Post.where(title: content)
      else
        Post.where('title LIKE ?', '%'+content+'%')
      end
    end
  end
end

補足を追加したコードは下記になります。

app/controllers/searchs.controller.rb
class SearchsController < ApplicationController
  def search
    # viewのform_tagにて
    # 選択したmodelの値を@modelに代入。
    @model = params["model"]
    # 選択した検索方法の値を@methodに代入。
    @method = params["method"]
    # 検索ワードを@contentに代入。
    @content = params["content"]
    # @model, @content, @methodを代入した、
    # search_forを@recordsに代入。
    @records = search_for(@model, @content, @method)
  end

  private
  def search_for(model, content, method)
    # 選択したモデルがuserだったら
    if model == 'user'
      # 選択した検索方法がが完全一致だったら
      if method == 'perfect'
        User.where(name: content)
      # 選択した検索方法がが部分一致だったら
      else
        User.where('name LIKE ?', '%'+content+'%')
      end
    # 選択したモデルがpostだったら
    elsif model == 'post'
      if method == 'perfect'
        Post.where(title: content)
      else
        Post.where('title LIKE ?', '%'+content+'%')
      end
    end
  end
end


routingの編集

config/routes.rb
get '/search', to: 'searchs#search'

viewの作成

今回は部分テンプレートを活用。
下記部分テンプレートは<%= render 'searchs/form' %>で使用可能。

検索BOXの部分テンプレート

app/views/searchs/_form.html.erb
<% if user_signed_in? %>
    <%= form_tag(search_path, method: :get) do %>
      <%= text_field_tag 'content' %>
      <%= select_tag 'model', options_for_select({ "User" => "user", "Post" => "post" }) %>
      <%= select_tag 'method', options_for_select({ "完全一致" => "perfect", "部分一致" => "partial" }) %>
      <%= submit_tag '検索' %>
    <% end %>
<% end %>

補足【form_tag】
簡単に入力フォームに必要なHTMLを作成することが可能なヘルパーメソッド。

今回の設置場所。

app/views/homes/mypage
...

<%= render 'searchs/form' %>

...

検索結果の表示画面

app/views/searchs/search.html.erb
<% if @model == 'user' %>
  <h3>【Usersモデルの検索結果】検索ワード:<%= @content %></h3>
  <%= render 'users/index', users: @records %>
<% else @model == 'posts' %>
  <h3>【Postsモデルの検索結果】検索ワード:<%= @content %></h3>
    <%= render 'posts/index', posts: @records %>
<% end %>

部分テンプレートの中身

app/views/posts/_index.html.erb
<table>
    <thead>
        <tr>
            <th>投稿者名</th>
            <th>タイトル</th>
            <th>本文</th>
            <th></th>
            <th></th>
            <th></th>
        </tr>
    </thead>
    <tbody id="post">
    <% posts.each do |post| %>
      <tr>
        <td><%= post.user.name %></td>
        <td><%= post.title %></td>
        <td><%= post.body %></td>
        <td><%= link_to "詳細", post_path(post) %></td>
        <% if post.user == current_user %>
          <td><%= link_to "編集", edit_post_path(post) %></td>
          <td><%= link_to "削除", post_path(post), method: :delete, remote: true %></td>
        <% else %>
          <td></td>
          <td></td>
        <% end %>
      </tr>
    <% end %>
    </tbody>
</table>
app/views/books/_users.html.erb
<% users.each do |user| %>
  <%= link_to user_path(user) do %>
    <%= user.name %><br>
  <% end %>
<% end %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsのバージョン指定について

該当箇所

現場で使えるRuby on Rails5 速習実践ガイドChapter3-1-3

$ rails _5.2.1_ new taskleaf -d postgresql

でバージョン指定してアプリを作成したのにサーバを起動するとrails のバージョンが5.2.4.4になってしまいました。

期待する動作

GemfileとGemfile.lockのバージョンを一致させて、サーバを起動するとrails のバージョンが5.2.1になること

取り組んだこと

バージョンはGemfile.lockに記述されているため、見てみるとやはりrails のバージョンが5.2.4.4になっていました。それに対し、Gemfileは

Gemfile
gem 'rails', '~> 5.2.1'

とあり、なんでGemfileとGemfile.lockでバージョンが違うんだろう?と思いました。

調べていくうちに、原因はGemfileの中のgem ‘rails’, ‘~> 5.2.1’の部分だとわかりました。
gem ‘rails’, ‘~> 5.2.1’gem ‘rails’, ‘>= 5.2.1’, < 5.3.0'を表すためGemfile.lockのバージョンが5.2.4.4になってしまっていたのです。
Gemfile.lockも5.2.1にするためにはGemfileでgem ‘rails’, ‘5.2.1’でがっちり指定する必要がありました。

Gemfile
gem 'rails', '5.2.1'

に修正して、

$ bundle update

これで正しくバージョンが指定されました!

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

rails new アプリ名でアプリが作成されない

該当箇所

Railsチュートリアル第3章
$ rails _6.0.3_ new sample_app
を実行すると、エラーが起こりアプリが作成されない。

期待する動作

$ rails _6.0.3_ new sample_appコマンドが正常に動作し、アプリが作成されること。

取り組んだこと

$ rails _6.0.3_ new sample_app

Yarn not installed. Please download and install Yarn from https://yarnpkg.com/lang/en/docs/install/

Yarnがインストールされていないようです。Yarnをインストールしてくださいと言われました。
なので、HomebrewのbrewコマンドでYarnをインストールします。

 $ brew install yarn

 bash: brew: command not found

次は、brewコマンドなんてないよと言われました。
ということはHomebrewがないのだと思い、Homebrewをインストールします。

 $ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

 Warning: /home/linuxbrew/.linuxbrew/bin is not in your PATH.

PATHが通っていませんよ、と言われました。
なので、PATHを通らせます。

$ echo 'export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"' >>  ~/.bashrc
source ~/.bashrc

これでHomebrewがインストールされYarnもインストールされました。

$ rails _6.0.3_ new sample_app

ちゃんとアプリが作成されました!!

結論

$ rails new アプリ名コマンドでアプリを作成するためには、Yarnをインストールする必要がある。
また、Yarnをインストールするためにはnpmでインストールする方法などもありますが、Homebrewでインストールできる。

参考

https://qiita.com/jun3030/items/afcd3287285a57b32ccb

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

現場で使えるRuby on Rails5速習実践ガイド まとめ

現場で使えるRuby on Rails5速習実践ガイド一冊を一通り習い終えたので、なるほど!と思ったことをメモしました。

用語整理

rbenv:

Rubyのバージョン管理を簡単にしてくれるツール。
開発においてプロジェクトに合わせてバージョンを使い分ける必要があるため、バージョン管理は重要。動作の速さやBundlerによるgem管理の使い勝手の良さが評価されている。

rbenvのインストールにはHomebrewが便利で、Homebrewの利用にはXcodeが必要。
つまり、Xcode -> Homebrew -> rbenvの順でインストールを行う。

RDB:

Relational DataBaseの略。
データを表で管理してわかりやすく関連性を示すデータベースのこと。
普段使うMysqlやPostgresqlなども含まれる。

アセットパイプライン:

記述したCSSやJavaScriptをブラウザにとって最適な形にする処理のこと。
production環境では処理速度や通信量を重視する点からアセットの連結・最小化が行われるのに対し、devlopment環境ではデバックのしやすさを追求するためアセットの連結・最小化が行われない。

app/views/layouts/application.html.slim
    = stylesheet_link_tag    'application', media: 'all'
    = javascript_include_tag 'application'

共通ビューでのヘルパーメソッドstylesheet_link_tag, javascript_include_tagの記述によってブラウザがCSSやJavaScriptのアセットを読み込んでいる。

Yarn:

JavaScriptのパッケージマネージャ(RailsでいうGemのパッケージマネージャはBundler)のこと。
RailsがサポートするWebpack,Vue.js,React.jsなどのフロントエンドのモジュールをRailsから管理する役割を担っている。

Yarnはデフォルトで入っていないため、自分でインストールしなければいけない。

Webpacker:
Railsのgemの一つで、Webpackを使ったアセットの管理を簡単にする。
WebpackerはYarnをインストールしていなければ使えない。

〇〇と△△の違い

<renderとredirect_toの違い>
renderはアクション直後にviewを返すのに対し、redirect_toはアクション後に別のURLへとブラウザがリクエストし直す。
アクション内でrenderやredirect_toを記述しなくてもviewが表示されるのはRailsが勝手にアクション名と同じ名前のviewを返してくれるからである。

<saveとsave!の違い>
saveメソッドは検証エラーがあればfalseを返し、そのエラーの中身はerrorsメソッドで見ることができる。
save!メソッドは検証エラーがあればfalseでなく例外を発生させるため、絶対保存できるはず!という時に使うのが妥当。
つまり、if文の分岐処理として使う場合はエラーでなくtrueかfalseを返したいため、!はつけない。

<findとfind_byの違い>
findメソッドはデータがない時にエラーActiveRecord::RecordNotFoundを発生する。
find_byメソッドはデータがない時にnilを返す。

よってログイン処理ではログインしていない状態、すなわちsession[:user_id]がない時にエラーでなくnilを返したいのでfind_byメソッドを使う。

その他のメモ

ルーティングについて

URLとHTTPメソッドの組み合わせからコントローラのアクションを繋げる役目だけでなく、
一つのルートはrails routesと打った時に出てくるPrefix部分に記述されているURLパターン名を用いてURLを簡単に作成するためのヘルパーメソッドを作り出している
link_toメソッドの第2引数の(URLパターン名)_pathはルーティングで生成されたURLヘルパーメソッドである。

マイグレーションについて

マイグレーションファイルはバージョンの上げる記述だけでなく、上げ下げどちらの記述もしたほうがいい。
changeメソッドではバージョンを上げるコードだけの記述から、Railsが勝手にバージョンを下げるロールバック処理も行ってくれるようになっている。
$ rails db:migrate:redoコマンドはバージョンを一回下げてまた戻す処理をしてくれるためロールバックができるか確認することができる。これを常に確認する癖をつけておいたほうがチーム開発では特にトラブルへの対応が楽になる。

Cookieの仕組みについて

まず、ブラウザの初めてのリクエストに対して、サーバはCookie情報を含めたレスポンスを返すことでブラウザはサーバのドメイン情報とそのCookie情報を保存する。
次回以降、サーバはブラウザからのリクエストに含まれるCookie情報から以前の情報を取得して更新する。更新したCookie情報はブラウザに渡され保存される。
この繰り返しでブラウザ側で情報を更新し続けている。

学び終えた全体の感想

とにかくRailsが勝手にやってくれていることが多いな!というのが印象的でした。便利であるからこそその裏側は知っておくべきだと改めて感じました。また、学んでいてRuby on Railsの勉強というよりRuby on Railsによる開発の勉強というような感覚でした。実践的な開発の流れであったり仕組みが細かく記述されていたためとても勉強になりました。基礎学習としておすすめです!

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

チームで共有するための『Rails 6 x MySQL 8』Docker環境構築手順

今回はRails 6とMySQL 8を組み合わせたWebアプリケーションのDocker環境を構築する手順について紹介します。

Rails 6からwebpackerが標準でインストールされるようになったり、MySQL 8からユーザー認証の方式が変わったりと環境構築でつまる部分がいろいろとあったため参考になればと思います。

複数人でもスムーズに開発ができるようにするためリモートリポジトリからcloneしてきたらdocker-compose upするだけでアプリケーションが立ち上がるという環境をゴールにします。

各種バージョンは以下の通りです。

  • Ruby on Rails: 6.0.3.2
  • Ruby: 2.7.1
  • MySQL: 8.0.21

実行環境はDocker Desktop for Mac(バージョン 2.3.0.4)を利用しています。

Railsアプリケーションの準備

ディレクトリの作成・移動をします。
今回作成するRailsアプリケーション名はsample_appとします。

$ mkdir sample_app && cd $_

rubyイメージを利用してローカル環境にGemfileを作成します。
-vはバインドマウント(ホストとコンテナのディレクトリの同期)のオプションです。

ホストのカレントディレクトリとコンテナのワークディレクトリを同期させることで、コンテナ上で作成されるファイルをホストに配置します。

$ docker run --rm -v `pwd`:/sample_app -w /sample_app ruby:2.7.1 bundle init

作成されたGemfileのgem "rails"の部分をアンコメントし、railsのバージョンを指定します。

Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
- # gem "rails"
+ gem "rails", '~> 6.0.3.2'

rails newを実行するDocker環境を構築するためDockerfileを作成します。

Rails 6ではアプリケーションを作成する際にrails webpacker:installも実行されるのでyarnのインストールを忘れずにしましょう。
今回はnpmを利用してyarnをインストールします。

Dockerfile
FROM ruby:2.7.1

RUN apt-get update -qq && \
    apt-get install -y nodejs \
                       npm && \
    npm install -g yarn

# 作業ディレクトリを/sample_appに指定
WORKDIR /sample_app

# ローカルのGemfileをDokcerにコピー
COPY Gemfile /sample_app/Gemfile

# /sample_appディレクトリ上でbundle install
RUN bundle install

Dockerfileをビルドして作成されたイメージを利用してコンテナを起動し、コンテナ上でrails newをします。

$ docker build -t sample_app .

$ docker run --rm -v `pwd`:/sample_app sample_app rails new . –skip-bundle --database=mysql

docker-compose.ymlを作成します。

今回は説明を簡略化するため、rootユーザーでMySQLに接続しています。
一般ユーザーで接続をする場合はMySQLのイメージに対して以下の環境変数を設定する必要があります。

環境変数 内容
MYSQL_USER ユーザー名
MYSQL_PASSWORD ユーザーパスワード

データベースの情報はmysql_dataという名前付きボリュームを作成して永続化します。

docker-compose.yml
version: '3'
services:
  web: # Ruby on Railsが起動するコンテナ
    build: .
    ports:
      - '3000:3000' # localhostの3000ポートでアクセスできるようにする
    volumes:
      - .:/sample_app # アプリケーションファイルの同期
    depends_on:
      - db
    command: ["rails", "server", "-b", "0.0.0.0"]
  db: # MySQLが起動するコンテナ
    image: mysql:8.0.21
    volumes:
      - mysql_data:/var/lib/mysql # データの永続化
    command: --default-authentication-plugin=mysql_native_password # 認証方式を8系以前のものにする。
    environment:
      MYSQL_ROOT_PASSWORD: 'pass'
      MYSQL_DATABASE: 'sample_app_development'
volumes:
  mysql_data: # データボリュームの登録

上記のdocker-compose.ymlの補足説明をします。

MySQLのDockerイメージではMYSQL_DATABASEに設定された名前のデータベースを作成してくれます。
Ruby on Railsのdevelopment環境では[アプリケーション名]_developmentというデータベースを利用するため、sample_app_developmentMYSQL_DATABASEに登録しています。
これでrails db:createを実行しなくてもdevelopment環境のデータベースを用意できます。

MySQL 8からは認証方式がmysql_native_passwordからcaching_sha2_passwordに変更されました。
MySQL 8標準のcaching_sha2_passwordの認証方式だとデータベースへ接続できず、Railsアプリケーション起動時に以下のようなエラーメッセージが表示されてしまいます。

Mysql2::Error::ConnectionError

Plugin caching_sha2_password could not be loaded: /usr/lib/x86_64-linux-gnu/mariadb19/plugin/caching_sha2_password.so: cannot open shared object file: No such file or directory

スクリーンショット 2020-09-15 9.15.45.png

そこで、--default-authentication-plugin=mysql_native_passwordで以前のmysql_native_passwordの認証方式を利用するようにしています。

次にRuby on Railsのデータベース接続設定を行います。

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: pass # (今回はrootなので)MYSQL_ROOT_PASSWORDと一致させる
  host: db # データベースのコンテナ名を設定する

Railsアプリケーションの起動確認

Ruby on Rails 6とMySQL 8を組み合わせたDocker環境ができあがったので起動をしてみます。

$ docker-compose up

localhost:3000にアクセスして以下の画面が表示されればOKです。

スクリーンショット 2020-09-15 9.23.43.png

データベースのデータの永続化についても確認をしてみます。
例としてscaffoldeventに関する機能を作成します。

$ docker-compose exec web rails g scaffold event title:string
$ docker-compose exec web rails db:migrate

localhost:3000/events/newにアクセスし、例として『サンプルイベント』というレコードを登録してみます。

コンテナを削除・起動してもデータが残っていればOKです。

$ docker-compose down
$ docker-compose up

$ docker-compose exec web rails c
> Event.last.title
=> "サンプルイベント"

テスト環境用のデータベースの作成

MYSQL_DATABASEを利用してdevelopment環境のデータベースを作成することでdb:createを実行することなくRailsアプリケーションを起動できるようにしました。

しかし、テストコードで利用するtest環境のデータベース([アプリケーション名]_test)が作られていないためこれだけでは開発環境としては不十分です。

MySQLのDockerイメージでは/docker-entrypoint-initdb.dにスクリプト(.sql.sh.sql.gz)を配置しておくとコンテナ起動時に実行してくれるという機能があります。1
この機能を活用してtest環境用のデータベースを作成します。

スクリプトはアルファベット順で実行されるため、スクリプト間に依存関係がある場合はファイル名も意識してつけるようにしておきましょう。

$ mkdir docker-entrypoint-initdb.d && cd $_
$ vim 00_create.sql
00_create.sql
-- test環境用のデータベースを作成する
CREATE DATABASE sample_app_test;

なお、一般ユーザーのデータベースアクセス権はMYSQL_DATABASEに設定したデータベースのみとなっています。
ですので、一般ユーザーでデータベースに接続する場合はデータベースの作成だけでなく、以下のようなアクセス権の付与も実行する必要があります。

01_grant.sql
-- webappという名前の一般ユーザーを利用する場合
GRANT ALL ON `sample_app_test`.* TO webapp@'%';

作成したスクリプトをコンテナ上で読み取れるようにするためバインドマウントを追加します。

docker-compose.yml
version: '3'
services:
  web:
    build: .
    ports:
      - '3000:3000'
    volumes:
      - .:/sample_app
    depends_on:
      - db
    command: ["rails", "server", "-b", "0.0.0.0"]
  db:
    image: mysql:8.0.21
    volumes:
      - mysql_data:/var/lib/mysql
+     - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_ROOT_PASSWORD: 'pass'
      MYSQL_DATABASE: 'sample_app_development'
volumes:
  mysql_data:

データベースが自動作成されることを確認

dbコンテナを削除・起動し、自動でデータベースが作られるか確認をしてみます。
[アプリケーション名]_development[アプリケーション名]_testのデータベースが存在していればOKです。

# データベースの永続化情報も削除して0からデータベースを作成する
$ docker-compose down --volumes

$ docker-compose up

$ docker-compose exec db mysql -uroot -ppass -D sample_app_development

mysql> show databases;
+------------------------+
| Database               |
+------------------------+
| information_schema     |
| mysql                  |
| performance_schema     |
| sample_app_development |
| sample_app_test        | ← test用のdbが作成されている
| sys                    |
+------------------------+
6 rows in set (0.00 sec)

Railsアプリケーションからも正常に接続できます。

$ docker-compose exec web rails c

> ENV['RAILS_ENV']
=> "development

$ docker-compose exec web rails c -e test

> ENV['RAILS_ENV']
=> "test"

『clone → docker-compose up』だけで環境が立ち上がるようにする

DockerでRailsの開発環境を構築する方法としてよく見かけるのが以下のようなパターンです。

# コンテナを立ち上げる
$ docker-compose up

# データベースの作成
$ docker-compose exec web rails db:create

# テーブルのマイグレーション
$ docker-compose exec web rails db:migrate

Railsアプリケーションとデータベースをdocker-composeで連携させ、アプリケーションで利用するデータベースはコンテナ上で構築するという方法です。

上記の方法でも問題はないのですが、この場合だと初回起動時にデータベースとテーブルの作成をコンテナ上で手動実行する手間がかかります。

複数人でDocker環境を共有する時のことを考えると、リモートリポジトリからアプリケーションファイルをcloneしてきたらdocker-compose upをするだけで環境が立ち上がるのが理想です。

共有メンバーに対して「初回起動時はデータベースの作成をコンテナ上で実施してね」とわざわざ伝えないといけない状況はできれば避けたいです。

ここからはリモートリポジトリからcloneしたらdocker-compose upするだけでRailsアプリケーションが起動できるようにするための設定を行います。

yarn installをコンテナ起動時に実行する

開発環境ではバインドマウト(ホストとコンテナのディレクトリの同期)を利用したソースコードの同期がよく利用されます。

リモートリポジトリからcloneしてきたアプリケーションファイルはpackage.jsonはありますが、node_modulesディレクトリはありません。
そのため、cloneしたあとdocker-compose upをするとnode_modulesがない状態のディレクトリがバインドマウントされます。その結果yarn installの実行を促すエラーが発生し、アプリケーションが起動できません。

========================================
  Your Yarn packages are out of date!
  Please run `yarn install --check-files` to update.
========================================

To disable this check, please change `check_yarn_integrity`
to `false` in your webpacker config file (config/webpacker.yml).

yarn check v1.22.5
info Visit https://yarnpkg.com/en/docs/cli/check for documentation about this command.

これを防ぐにはコンテナ起動時にyarn installを実行する必要があります。
たとえば以下のようにすることでコンテナ起動時にyarn installを実行できます。

docker-compose.yml
version: '3'
services:
  web:
    build: .
+   command: ["./start.sh"]
-   command: ["rails", "server", "-b", "0.0.0.0"]
    ports:
      - '3000:3000'
    volumes:
      - .:/sample_app
    depends_on:
      - db
  db:
    image: mysql:8.0.21
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_ROOT_PASSWORD: 'pass'
      MYSQL_DATABASE: 'sample_app_development'
start.sh
#!/bin/bash -eu
yarn
rails server -b '0.0.0.0'

シェルのパーミッションを変更します。

$ chmod 755 start.sh

コンテナを起動する際のコマンドでシェルを呼び出し、シェルの中でyarn installを実行するようにしています。
yarn install実行後、rails serverでRailsアプリケーションが起動するため先ほどのエラーが解消されます。

マイグレーションを自動実行できるようにする

テーブルのマイグレーションもyarn installと同様、コンテナ起動時に呼び出されるシェルスクリプトで実行するようにします。

start.sh
#!/bin/bash -eu
yarn
+ rails db:migrate
rails server -b '0.0.0.0'

しかし、docker-compose.ymldepends_onを利用することでコンテナの起動順は制御できますが、コンテナの起動を待つということはできないため2、dbコンテナの起動準備が終わる前にdb:migrateが実行されるとマイグレーションが正常に行われません。

マイグレーションの自動化をするにはdbコンテナの起動を待ってからdb:migrateが実行されるようにする必要があります。

データベースの準備を待つ方法にはいくつかありますが、今回はwait-for-itを利用する方法を紹介します。

wait-for-it.shをカレントディレクトリに配置し、docker-compose.ymlを以下のようにするとデータベースの起動を待ってからスクリプトが実行されます。

docker-compose.yml
version: '3'
services:
  web:
    build: .
-   command: [""./start.sh"]
+   command: ["./wait-for-it.sh", "db:3306", "--", "./start.sh"]
    ports:
      - '3000:3000'
    volumes:
      - .:/sample_app
    depends_on:
      - db
  db:
    image: mysql:8.0.21
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_ROOT_PASSWORD: 'pass'
      MYSQL_DATABASE: 'sample_app_development'

まとめ

以上でRails 6 + MySQL 8のDocker環境の構築手順の紹介を終わります。

コンテナ起動時にシェルを実行することで、複数人でDocker環境を利用する場合でもスムーズに開発環境を構築できるようにしました。
なお、今回はコンテナ起動時にシェルを呼び出すことでyarn installdb:migrationを確実に実行するようにしましたが、コンテナ起動時のコマンドの制御はEntrykitと呼ばれるツールでも行えます。

EntrykitについてはRailsのDocker環境にEntrykitを導入し、bundle installを自動実行させる方法で紹介していますので、興味のある方はご覧になってください。

  • 今回のまとめ
    • Rails 6からはyarnのインストールも事前に行う必要がある
    • MySQL 8の認証エラーが発生した場合は標準の認証に戻すことで解決する
    • コンテナ起動時にシェルを実行することでテーブル作成等の自動化が可能になる

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考記事

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

Rails 6で認証認可入り掲示板APIを構築する #11 userモデルのテストとバリデーション追加

Rails 6で認証認可入り掲示板APIを構築する #10 devise_token_auth導入

テストの準備

userにバリデーションとテストを実装します。
テストファーストではなくなってしまっていますが、deviseは特殊なので、ちゃんと動くところまで実装を済ませたかったため、順番が前後しています。

まずはテストに必要なファイルを準備します。
userモデルはdevise_token_authで生成されたため、rspecとfactoryBotのファイルがありません。
以下コマンドで生成します。

$ rails g rspec:model user
      create  spec/models/user_spec.rb
      invoke  factory_bot
      create    spec/factories/users.rb

factoryを先に作ります。

spec/factories/users.rb
# frozen_string_literal: true

FactoryBot.define do
  factory :user do
    provider { "email" }
    sequence(:email) { |n| "test#{n}@example.com" }
    uid { email }
    password { "password" }
    remember_created_at { nil }
    name { "MyString" }
    tokens { nil }
  end
end

userテーブルのカラムは何があったっけ?となったら、db/schema.rbを見ると出力結果が確認できます。

factoryファイルで注目すべきは3点。
sequence(:email) { |n| "test#{n}@example.com" }
uid { email }
password { "password" }

この3つです。

sequence(:email) { |n| "test#{n}@example.com" }
この処理のnには連番が入ってきます。1レコード目はtest1@example.com, 2レコード目はtest2@example.com, …となります。

わざわざこうやっているのは、emailカラムがunique制約がかかっており、同一文字列だと登録できないためです。
例えばここを定数でemail { "test@example.com" }とすると、create_list(:user, 10)で該当メールアドレスが2個以上になってコケちゃうわけですね。

uid { email }
ここはシンプルに、uidにemailの変数を入れているだけです。
deviseの挙動として、providerがemailの場合はuid=emailとなります。

password { "password" }
DBのカラム上はencrypted_passwordですが、そこにはハッシュ化された文字列が保存されます。
パスワード設定時はpasswordに値を入れる必要があるため、schema.rbとカラム名が一致しないのです。

userモデルのテスト

postと似たテストを実行したいので、spec/models/post_spec.rbをコピー。
PostUsersubjectnamebodyemailに置換。

spec/models/user_spec.rb
# frozen_string_literal: true

require "rails_helper"

RSpec.describe User, type: :model do
  describe "name" do
    context "blankの時に" do
      let(:user) do
        build(:user, name: "")
      end
      it "invalidになる" do
        expect(user).not_to be_valid
      end
    end
    context "maxlengthにより" do
      context "30文字の場合に" do
        let(:user) do
          build(:user, name: "あ" * 30)
        end
        it "validになる" do
          expect(user).to be_valid
        end
      end
      context "31文字の場合に" do
        let(:user) do
          build(:user, name: "あ" * 31)
        end
        it "invalidになる" do
          expect(user).not_to be_valid
        end
      end
    end
  end

  describe "email" do
    context "blankの時に" do
      let(:user) do
        build(:user, email: "")
      end
      it "invalidになる" do
        expect(user).not_to be_valid
      end
    end
    context "maxlengthにより" do
      context "100文字の場合に" do
        let(:user) do
          build(:user, email: "@example.com".rjust(100, "a"))
        end
        it "validになる" do
          expect(user).to be_valid
        end
      end
      context "101文字の場合に" do
        let(:user) do
          build(:user, email: "@example.com".rjust(101, "a"))
        end
        it "invalidになる" do
          expect(user).not_to be_valid
        end
      end
    end
    context "email形式により" do
      context "正しい文字列の場合に" do
        let(:user) do
          build(:user, email: "test@example.com")
        end
        it "validになる" do
          expect(user).to be_valid
        end
      end
      context "正しくない文字列の場合に" do
        let(:user) do
          build(:user, email: "test@example")
        end
        it "invalidになる" do
          expect(user).not_to be_valid
        end
      end
    end
  end
end

以下箇所は、postからのコピーではなく独自に置き換えてます。

build(:user, email: "@example.com".rjust(100, "a"))
build(:user, email: "@example.com".rjust(101, "a"))

rjustは文字列が右詰めになるように指定文字で埋めるメソッドです。
rails cで実行してみます。

$ rails c
[1] pry(main)> "@example.com".rjust(100, "a")
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com"
[2] pry(main)> "@example.com".rjust(100, "a").length
=> 100

こうすることによって、100文字ピッタリのダミーデータを生成できます。

userモデルのバリデーション実装

app/models/user.rb
 # frozen_string_literal: true

 #
 # ユーザークラス
 #
 class User < ActiveRecord::Base
   # Include default devise modules. Others available are:
   # :confirmable, :lockable, :timeoutable and :omniauthable
   devise :database_authenticatable, :registerable,
          :rememberable, :validatable
   include DeviseTokenAuth::Concerns::User


+  validates :name, presence: true, length: { maximum: 30 }
+  validates :email, presence: true, length: { maximum: 100 }
end

これでrspecのテストを通過するようになります。

あれ?メールアドレスのフォーマットバリデーション書いてないけどなんでOKなの?と感じたら鋭いです。
実はdeviseの:validatableでemailのフォーマットチェックは行われているので、email形式確認バリデーションはdevise依存モデルの場合特に指定不要です。

authのrequest specを書く

今回のチュートリアルの中では、登録とログインだけの簡易的なテストを書いて終わりにします。

$ rails g rspec:request v1/auth
      create  spec/requests/v1/auths_spec.rb
$ mv spec/requests/v1/auths_spec.rb spec/requests/v1/auth_spec.rb

authsが気持ち悪いのでauthにrenameしておきます。
基本的にはspec/requests/v1/posts_spec.rbを参考にしながら、auth_spec.rbを書いていきます。

spec/requests/v1/auth_spec.rb
# frozen_string_literal: true

require "rails_helper"

RSpec.describe "V1::Auth", type: :request do
  describe "POST /v1/auth#create" do
    let(:user) do
      attributes_for(:user, email: "signup@example.com", name: "signupテスト")
    end
    it "正常レスポンスコードが返ってくる" do
      post v1_user_registration_url, params: user
      expect(response.status).to eq 200
    end
    it "1件増えて返ってくる" do
      expect do
        post v1_user_registration_url, params: user
      end.to change { User.count }.by(1)
    end
    it "nameが正しく返ってくる" do
      post v1_user_registration_url, params: user
      json = JSON.parse(response.body)
      expect(json["data"]["name"]).to eq("signupテスト")
    end
    it "不正パラメータの時にerrorsが返ってくる" do
      post v1_user_registration_url, params: {}
      json = JSON.parse(response.body)
      expect(json.key?("errors")).to be true
    end
  end

  describe "POST /v1/auth/sign_in#create" do
    let(:user) do
      create(:user, email: "signin@example.com", name: "signinテスト")
      { email: "signin@example.com", password: "password" }
    end
    it "正常レスポンスコードが返ってくる" do
      post v1_user_session_url, params: user, as: :json
      expect(response.status).to eq 200
    end
    it "nameが正しく返ってくる" do
      post v1_user_session_url, params: user
      json = JSON.parse(response.body)
      expect(json["data"]["name"]).to eq("signinテスト")
    end
    it "不正パラメータの時にerrorsが返ってくる" do
      post v1_user_session_url, params: {}
      json = JSON.parse(response.body)
      expect(json.key?("errors")).to be true
    end
  end
end

ここまで書いて、rubocopとrspecが正常に通ればcommitします。

続き


連載目次へ

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

railsの中間テーブルでデータの組み合わせを一意にする方法

はじめに

Twitterのように、1人のユーザーは複数のツイートにいいね!ができて、1つのツイートは複数のユーザーにいいね
!されるような多対多の関係をよく実装することはあると思います。
あるユーザーは1つのツイートに対して1回しかいいね!はしないので組み合わせは一意である必要があります。
今回は中間テーブルのデータの組み合わせを一意にする方法を説明します。

開発環境

Rails 6.0.3
Ruby 2.7.1
テスト: Rspec, FactoryBot, shoulda-matchers

1. テーブル

今回使用するテーブルは下記の3つです。

Userテーブル

id name email
1 ユーザー1 a@a.a
2 ユーザー2 b@b.b

Tweetテーブル

id content
1 tweet1
2 tweet2

Likeテーブル(中間テーブル)

id user_id tweet_id
1 1 2
2 1 3

多対多の関係

多対多の説明に関しては今回は省略します。

user.rb
class User < ApplicationRecord
  has_many :likes, dependent: :destroy
end
tweet.rb
class Tweet < ApplicationRecord
  has_many_to :likes
end
like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :tweet
end

2. 組み合わせを一意にする方法

そして今回のメインに実装です。
やることは2つです。

  1. migrationファイルにadd_indexでunique制約をつける (DBに制約をつける)
  2. 中間テーブルのmodelにvalidationを追加する (アプリ側でバリデーションを追加)

2.1 migrationファイルにadd_indexでunique制約をつける

add_index :likes, [:user_id, :tweet_id], unique: trueを中間テーブルのmigrationファイルに追加します。
追加した後はmigrateを忘れないように!

class CreateLikes < ActiveRecord::Migration[6.0]
  def change
    create_table :likes do |t|
      t.references :user, null: false, foreign_key: true
      t.references :tweet, null: false, foreign_key: true

      t.timestamps
    end
    add_index :likes, [:user_id, :tweet_id], unique: true   #ここを追加
  end
end

2.2 中間テーブルのmodelにvalidationを追加する

validates :hotel_id, uniqueness: { scope: :staff_id }を中間テーブルのモデルに追加します。

like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :tweet

  validates :user_id, uniqueness: { scope: :tweet_id }  #ここを追加
end

必要な実装は以上です。
コンソールで同一の組み合わせは一度しか登録できないことを確認してみてください。

3. 一意であることを確認するRspecテスト

参考までにテストもご紹介します。
Rspec, FactoryBot, shoulda-matchersの設定はできているものとします。

like_spec.rb
require 'rails_helper'

RSpec.describe Like, type: :model do
  let(:user) { create(:user) }
  let(:tweet) { create(:tweet) }
  before { create(:like, user: user, tweet: tweet) }

  it { should belong_to(:user) }  # この行と下の行で多対多の関係を確認
  it { should belong_to(:tweet) }
  it { is_expected.to validate_uniqueness_of(:user_id).scoped_to(:tweet_id) } # ここで一意であることを確認
end

おわりに

ちゃんと組み合わせを一意にできましたでしょうか?
今回のような実装はよく使われると思うので参考になれば幸いです。

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

(アンチパターン)Railsの本番環境でローカル環境と同じエラー画面を表示させる

アンチパターン
ソフトウェア開発におけるアンチパターン (英: anti-pattern) とは、必ず否定的な結果に導く、しかも一般的に良く見られる開発方式を記述する文献形式を言う。

アンチパターン - Wikipedia

はじめに

ローカル環境(開発環境)でRuby on Railsアプリケーションの開発をするとき、エラー(例外)が発生すると以下のようなエラー画面が表示されます。

Screen Shot 2020-09-16 at 5.28.23.png

しかし、本番環境にRailsアプリをデプロイすると、ローカル環境のようなエラー画面は表示されず、デフォルトでは"We're sorry, but something went wrong."というようなエラーメッセージが表示されます。

Screen Shot 2020-09-16 at 5.28.53.png

ローカル環境のエラー画面では、エラーが発生した行やスタックトレース等の情報が豊富に表示されるので、デバッグに役立ちます。
一方、本番環境ではそういった情報がまったく表示されないため、どこでどんなエラーが発生したのかわかりません。

では、こんなときはどうしたらよいでしょうか?
この記事では本番環境でエラーが発生した場合の(よくある)間違った方法と、適切な対処方法についてまとめます。

間違った対処方法☠️

"rails 本番環境 エラー画面"のようなキーワードでネットを検索すると、次のように、「config/environments/production.rbconfig.consider_all_requests_localtrue に変更してデプロイすればOK」と書いてある記事が上位に表示されます。しかし、 この対応はNG です。(つまりアンチパターン)

config/environments/production.rb
# falseからtrueに変更する、はNG!!
config.consider_all_requests_local = true

たしかに、こうすると本番環境でローカル環境と同じエラー画面が表示されます。しかし、これだと本来は開発者以外知る必要がないソースコードや実行環境の情報が不特定多数の人々に公開されてしまいます。これはセキュリティ上の深刻なリスクになります。(この点については本記事の後半で詳しく議論します)

ですので、安易に consider_all_requests_localtrue に変更することはやめましょう!

適切な対処方法?

前述の通り、本番環境でローカル環境と同じエラー画面を表示させるのはセキュリティ上のリスクが高いのでいさぎよく諦めましょう。

本番環境で発生したエラーを確認する場合は、以下のような方法が考えられます。

ログを見る

本番環境でエラーを確認する一番の正攻法はログを見ることです。検索するならエラー画面を表示する方法ではなく、ログを確認する方法にしてください。

たとえば、Heroku環境であれば以下のようなコマンドでログを確認できます。

# Heroku上のログを確認する
$ heroku logs

Herokuでログを確認する場合の詳しい情報は公式ドキュメントを参照してください。

Logging | Heroku Dev Center

もしくは、Papertrailのようなアドオンを入れて、ブラウザ上でログを確認することもできます。以下はPapertrailアドオンを使った場合のログの表示例です。

Screen Shot 2020-09-16 at 6.52.41.png

Heroku以外の環境(AWS等)でもログを確認する方法は必ずあるはずです。
一般的な方法としてはsshなどを使って本番サーバーに接続し、tailコマンド等でログを確認する方法があります。

# sshでサーバーに接続し、tailコマンドでログを確認する
$ cd /path/to/your/rails/application
$ tail -f log/production.log

ログを見るとエラーが発生したタイミングでバックトレース(スタックトレース)が出力されているはずです。この情報を元にデバッグを進めてください。

以下はエラー発生時のログの出力例です。

I, [2020-09-15T21:55:25.471329 #4]  INFO -- : [2bc2421a-7441-46ef-8632-a205e72c03e0] Started GET "/" for 124.45.27.199 at 2020-09-15 21:55:25 +0000
I, [2020-09-15T21:55:25.473285 #4]  INFO -- : [2bc2421a-7441-46ef-8632-a205e72c03e0] Processing by WelcomeController#index as HTML
I, [2020-09-15T21:55:25.480425 #4]  INFO -- : [2bc2421a-7441-46ef-8632-a205e72c03e0]   Rendering welcome/index.html.erb within layouts/application
I, [2020-09-15T21:55:25.705386 #4]  INFO -- : [2bc2421a-7441-46ef-8632-a205e72c03e0]   Rendered welcome/index.html.erb within layouts/application (Duration: 224.8ms | Allocations: 115496)
I, [2020-09-15T21:55:25.705619 #4]  INFO -- : [2bc2421a-7441-46ef-8632-a205e72c03e0] Completed 500 Internal Server Error in 232ms (Allocations: 116781)
F, [2020-09-15T21:55:25.706575 #4] FATAL -- : [2bc2421a-7441-46ef-8632-a205e72c03e0]
[2bc2421a-7441-46ef-8632-a205e72c03e0] ActionView::Template::Error (undefined local variable or method `secret_mesage' for #<#<Class:0x0000557c00d75308>:0x0000557c00d6f4f8>
Did you mean?  secret_message):
[2bc2421a-7441-46ef-8632-a205e72c03e0]     1: <h1>Welcome!</h1>
[2bc2421a-7441-46ef-8632-a205e72c03e0]     2: <p><%= build_message("3566 0020 2036 0505") %></p>
[2bc2421a-7441-46ef-8632-a205e72c03e0]
[2bc2421a-7441-46ef-8632-a205e72c03e0] app/helpers/welcome_helper.rb:4:in `build_message'
[2bc2421a-7441-46ef-8632-a205e72c03e0] app/views/welcome/index.html.erb:2

「英語ばっかりでわけがわかんない!」「何から手を付けていいかさっぱりわからん!」という方は、僕が以前書いたこちらの記事を参考にしてみてください。

プログラミング初心者歓迎!「エラーが出ました。どうすればいいですか?」から卒業するための基本と極意(解説動画付き) - Qiita

エラートラッキングツールを導入する

他にもエラートラッキングツールを導入する方法も考えられます。
エラートラッキングツールを導入すると、エラー発生時に自動的にエラーに関する詳細情報がツールの専用サーバーに保存され、さらに開発者にメール等で通知されます。
通知メール等に記載されたリンクを開くと、バックトレースをはじめとした、エラーに関する詳細情報をブラウザ上で確認できます。

以下はBugsnagの画面表示例です。(画像の出典:https://www.bugsnag.com/product)

5ecefa9864d301527a5caef3_dash-stacktrace-prod-typeerror.png

代表的なエラートラッキングツールには以下のようなものがあります。

ただしエラートラッキングツールは有料のものが多いため、個人で利用するのは若干敷居が高いかもしれません。ですので、まずは本番環境でログを確認する方法を習得するのがベストだと思います。

本番環境でローカル環境と同じエラー画面を表示させるリスクについて、もっと詳しく

まだ実際の業務に就いたことがないプログラミング初心者の方は、「ログを見るなんて面倒くさい!本番環境でローカル環境と同じエラー画面を表示させたって別にいいじゃん!」って思っているかもしれません。

しかし、ソースコードはオープンソースソフトウェアではない限り、開発者以外の目に触れさせるべきではありません。
場合によっては次のように見えてはいけない情報が表示されてしまう可能性があります。

Screen Shot 2020-09-16 at 7.14.02.png

また、Railsのエラー画面ではリンクを使ってスタックトレース上のコードを確認していくことができます。
このリンクを使うと、エラーが発生した行以外のコードも見えてしまいます。

以下の例は、Railsのエラー画面からindex.html.erbのリンクをクリックした場合の表示例です。

Screen Shot 2020-09-16 at 7.18.01.png
 ↓ リンクをクリック
Screen Shot 2020-09-16 at 9.32.05.png

さらに、"Framework Trace"や"Full Trace"といったリンクを開くと、gemのコードも含めたバックトレースが表示されます。このバックトレースを見ると、そのRailsアプリが利用しているgemのバージョンがわかります。セキュリティ上の脆弱性を抱えたバージョンを使っていた場合は、悪意のある第三者によってgemの脆弱性を突いた攻撃が行われるかもしれません。

たとえば、以下の表示例ではこのRailsアプリケーションがRails 6.0.3.3を使っていることがわかります。

Screen Shot 2020-09-16 at 7.47.04.png

本記事の執筆時点ではRails 6.0.3.3は最新バージョンですが、将来的にRailsに新たな脆弱性が発見されると、それがセキュリティ上のリスクになり得ます。

実際に動かしてリスクを確認してみる

上で見せた表示例は、筆者がHeroku上で公開しているデモアプリケーションです。
consider_all_requests_localtrue に設定したので、みなさんのブラウザ上でもローカル環境と同じエラー画面が表示されます。

以下のURLを開き、ソースコードやRailsのバージョンが任意の第三者に公開されてしまうリスクを確認してみてください。

https://sunshine-password.herokuapp.com/

0wIrnZ1YXc.gif

想定される反論「誰も見に来ないから大丈夫です」「すぐに戻すから大丈夫です」

しかし、プログラミング初心者の方の中には「なるほど」と思いつつ、「まだどこにも公表してないから誰もアクセスしてこないはず」とか、「設定はすぐに戻すから大丈夫!」と考えている人がいるかもしれません。ですが、筆者はその考え自体がすでに危険だと考えます。

本番環境でローカル環境と同じエラー画面を表示させることは、技術上の問題だけでなく、 「習慣の問題」 という側面も大きいと思います。
すなわち、「いちいち本番サーバーに接続してログを見るなんて面倒くさい」とか、「何か困ったら本番環境でもローカル環境と同じエラー画面を表示させればいいや」と考えてしまう、その習慣に問題があるということです。

そういう習慣が身体に染みついていると、業務に入ったときも同じような方法でデバッグしたくなるかもしれません。
その誘惑に負けてしまったときに限って、「すぐに戻したはずなのに、悪意のあるユーザーがアクセスしてきてコードを盗み見された!」とか、「問題が解決したらホッとしてしまって、設定を戻し忘れた!」という大問題が発生するかもしれません。

だから、常日頃から「本番環境では必ずログを見に行く。安易に production.rb の設定を変えるのはダメ!!」と自分に言い聞かせておくのです。
そういう正しい習慣を身に付けておけば、本番環境のログを見に行くことにも心理的な抵抗がなくなるはずです。

参考:情報処理機構(IPA)の資料にも同じ話が載っています

情報処理機構(IPA)が提供している「安全なウェブサイトの作り方」という資料にも、ほぼ同じ話が載っています。

1-(iii)
エラーメッセージをそのままブラウザに表示しない。

エラーメッセージの内容に、データベースの種類やエラーの原因、実行エラーを起こしたSQL文等の情報が含まれる場合、これらはSQLインジェクション攻撃につながる有用な情報となりえます。また、エラーメッセージは、攻撃の手がかりを与えるだけでなく、実際に攻撃された結果を表示する情報源として悪用される場合があります。データベースに関連するエラーメッセージは、利用者のブラウザ上に表示させないことをお勧めします。

https://www.ipa.go.jp/security/vuln/websecurity-HTML-1_1.html

上記の資料では議論の対象がデータベースに限定されていますが、「エラーメッセージは、攻撃の手がかりを与える」という点はデータベースに限らず、Webアプリケーションの仕組み全般に適用できるはずです。

まとめ

というわけで、本記事ではRailsの本番環境でローカル環境と同じエラー画面を表示させることのリスクと、本番環境でエラーが発生した場合の適切な対処方法について説明しました。

冒頭にも述べたとおり、ネット上には(というかこのQiita上にも!)「 config/environments/production.rbconfig.consider_all_requests_localtrue に変更してデプロイすればOK」と説明している記事はとても多いです。

「セキュリティ上のリスクがありますよ」とか「すぐに戻しましょう」と、ひとこと注意書きが添えてあるならまだしも、何の注意書きもなく「これでOKです。以上!」で終わっている記事もよく見かけます。

もしみなさんの周りでそういった記事を鵜呑みにしている初心者さんがいたら、ぜひ本記事のリンクと一緒に「その設定、危険だよ!」と教えてあげてください。

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

RSpecの実行結果を分かりやすくするGitHub Actionを作った

はじめに

CI/CDのサービスとして最近はGitHub Actionsを利用しているのですが、CircleCIと比較した時にRSpecが失敗した時の実行結果が分かりにくいのが不満でした。
そんなストレスを解消するために社内ハッカソンで作った以下のGitHub Actionを紹介します。

RSpec Report · Actions · GitHub Marketplace · GitHub

何が出来るの?

PRイベントの場合は失敗結果がコメントされます。
またコメントされることで同様の内容がメールでも通知されるので、失敗したテストの内容がGitHubにアクセスしなくても把握できるようになります。

a23798fbb2f0e6454bf75a0e09034eb8.png

PRイベント以外の場合はChecks API経由で通知されます。

0d3c61f6ecc3354a9734eb2b30a82211.png

使い方

test.yml
name: Build
on:
  pull_request:

jobs:
  rspec:
    steps:
      # RSpec実行の為の事前準備は省略しています

      - name: Test
        run: bundle exec rspec -f j -o tmp/rspec_results.json -f p

      - name: RSpec Report
        uses: SonicGarden/rspec-report-action@v1
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          json-path: tmp/rspec_results.json
        if: always()

その他

ポイント

  • JSONフォーマットで出力したRSpecの実行結果を解析してコメントしています
    • rspecコマンドの-f j -o tmp/rspec_results.jsonオプションは必須です(保存先は任意)
  • テストが全て通ればコメントは削除されます
  • 繰り返し同じコメントが投稿されることはありません
  • テストの並列実行にも対応しています
    • その場合は以下のようにタイトルがユニークになるように調整してください
with:
  token: ${{ secrets.GITHUB_TOKEN }}
  json-path: tmp/rspec_results.json
  title: "# :cold_sweat: RSpec failure ${{ matrix.ci_node_index }}"

リポジトリ

GitHub - SonicGarden/rspec-report-action: A GitHub Action that report RSpec failure.

良かったら使ってみてください。

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

アプリのユーザー編集をするとログアウトされる事件(rails)

事件内容

Ruby onrails6 で難解な起こった事件

deviseで作ったユーザーの機能の
パスワードやメアドを編集する機能を作ったが
編集を更新すると、ログアウトされてしまう。

解決方法

rails のガイドブックにちゃんと載っていたみたいです!
デフォルトでパスワードやメアドを編集すると、ログアウトされるみたいです。

これを解決する方法は。。。。

user.controller.rb
 def edit
    @user=User.find(params[:id])
  end

  def update
     @user = User.find(params[:id])
     if @user.update(user_params)
      bypass_sign_in(@user)       #これを記述!!!!!!!!

      else
        render 'edit'
      end

  end

コメントアウトで記したコードを追加することで
この事件を解決することに成功しました。:sunny:

わーいわーい:shamrock:

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