20201121のRailsに関する記事は23件です。

一意性制約のバリデーションをかける方法

一意性制約とは

 同じデータを保存することができないようすること。

マイグレーションファイルに一意性制約

def change
    create_table :schools do |t|
      t.string :name, null: false
      t.timestamps
    end
    add_index :schools, :name, unique: true
end

add_index :schools, :name, unique: true←この部分が一意性制約
       テーブル名, カラム名

モデルに一意性制約

validates :name, presence: true, uniqueness: { case_sensitive: false }

uniqueness: { case_sensitive: false }←この部分が一意性制約
case_sensitive: falseは大文字・小文字を判別するかどうか。

終わりに

マイグレーションとモデル両方に記述する必要がある。
RSpecを使って、一意性制約のテストをしているが、ずっとエラーが出ていて進まない…

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

開発未経験のテストエンジニアがRailsで自動テストをやってみた

私は、テストエンジニアとして働いています。
業務経験としては、業務系システムのテスターが1年3か月、
進捗管理が2か月程です。

私の技術的な強みが、
少しSQLが得意なこと以外に思い浮かばないのに危機感を感じ、
10月からRailsを使った個人開発を自己研鑽として行っています。
(SQLの知識としては、オラクルマスターブロンズ合格程度です。)

そんな私が、Railsでテストを書いてみて、
手動テストとの比較を記事にしてみました。

自動テストの魅力

大きく分けて以下の2つのメリットを感じました。

・テスト実行が楽
・テストを先に書くことで、仕様の整理ができる

テスト実行が楽

何と言っても、テスト実行が楽です。
ひとたびコードを書いてしまえば、
"rails t"というコマンド一行でテストを実行できます。

きちんとテストが書けていればという前提ですが、
本番環境へのデプロイ前に、先述のコマンドを打ち込めば、
正常にアプリケーションが動くかどうかが確認できます。

また、手動テストでExcel方眼紙にスクリーンショットを貼り付けて
エビデンスを整形するのが常識である現場にいる人間にとって、
この効率の良さは驚異的です。

テストを先に書くことで、仕様の整理ができる

これも大きなメリットだと感じます。

テスト設計をするときは、正常系のみならず、
異常系も考慮しなくてはなりません。

個人的にはモデルのテストを書くときが最もわかりやすく効果を感じました。

例えば、パスワードを格納するカラム一つとっても、
文字数に上限や下限を設けるのかや、
半角記号や半角数字を含むことを必須にするのかなど、
検討事項は複数あります。

そして、それらに確固たる正解はありません。

そのような事項を1つ1つテストとして書いて思考を整理するのです。

ウォーターフォール型開発の現場にしかいたことがないせいか、
細かい仕様の検討とテスト設計を同時に進めることは、
新鮮かつ楽しく感じます。
(上流・下流という謎の上下関係のような概念はそこにはないと思う。)

自動テストの課題

私が自動テストを書きながら課題と感じたのは以下のことです。
(私自身の今後の技術的課題でもあります。)

・細かい表示のテストは難しい。
・うまく自動化できない場合がある。

細かい表示のテストは難しい

画面がスクロールしても固定させたい部分や、
画面のスクロールに合わせて一定の範囲で動かしたいといった、
ユーザビリティに関わることをテストするのは難しく感じます。

うまく自動化できない場合がある。

エンジニアの技術不足で、うまくテストを自動化できない場合もあります。

私の技術不足で、今開発しているアプリでうまく自動化できなかったのは、
コントローラーに書いた以下のコードに対応するテストです。

※ブラウザ上では期待結果通り、
destroyアクションが動く前に参照していたページにリダイレクトされました。

app/controllers/mission_controller.rb
 def destroy
    Mission.find(params[:id]).destroy
    flash[:success] = "タスクを削除しました。"
    #以下の1行に関するテストが書けませんでした。
    redirect_back(fallback_location: root_path)
  end

コードの意味としては、
可能ならリファラーにリダイレクトし、
不可能ならroot_pathにリダイレクトするという意味です。

integration testでテストを書くと、
どうしてもroot_pathにリダイレクトしてしまいます。
※以下のように書かないとテストが失敗します。

test/integration/missions_destroy_test.rb
require 'test_helper'

class MissionsDestroyTest < ActionDispatch::IntegrationTest

  def  setup
    @user = users(:michael)
    @mission = Mission.find_by(user_id: @user.id)
  end

  test "destroy mission from users show when cannot get refferer" do
    log_in_as(@user)
    get user_path(@user)
    assert_template 'users/show'
    delete mission_path(@mission)
    assert_redirected_to root_path
  end

  test "destroy mission from missions index when cannot get refferer" do
    log_in_as(@user)
    get missions_path
    assert_template 'missions/index'
    delete mission_path(@mission)
    assert_redirected_to root_path
  end
end

考えられる原因としては、
システムテストのように、
実際のブラウザに対してテストを実行しておらず、
fallback_locationを取得できなかったからだと考えています。
(間違っていたら、ご指摘いただけますと幸いです。)

まとめ

自動テストはとても魅力的だと思います。
これから、システムテストも書けるようになっていきたいです。

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

【Rails】7つのアクション以外のアクションを追加する

オリジナルのメモ投稿アプリの作成中、過去のメモを表示させるViewを追加で作成したいと思ったのですがデフォルトで作成される7つのアクションは既に作成しており、何か方法がないか調べ実装できたことをメモしておきます。

解決策

  1. Controllerに新たなアクション(今回は past というアクション名にしました)を定義
  2. past.html.erbを作成
  3. ルーティングにpastを追加する

ルーティングの記述は下記の通り

routes.rb
resources :schedules do
      get "past", on: :member
      resources :comments, only: :create
    end

rails routesで確認するとURIパターンは下記の通り確認できました。

schedules/:id/past(.:format) 

参考サイト

Railsのルーティング
作って学ぶRuby on Rails Vol.3 Actionを追加しよう!

プログラミングスクールでの学習では7つのアクションを使ったアプリしか作成しなかったので、他にもアクションを追加できるということが今の私にとっては衝撃的でした…!
上記以外にもアクション追加の記述があることを学びましたが時間がないので続きは後日書きます…

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

railsのメソッド"save"がどう定義されているのか読解してみた

rails読解シリーズ第5回目はsaveメソッド。

save
# File activerecord/lib/active_record/base.rb, line 2575
def save
  create_or_update
end

create_or_updateメソッドが呼ばれているだけのようです。

create_or_update
# File activerecord/lib/active_record/base.rb, line 2916
def create_or_update
  raise ReadOnlyRecord if readonly?
  result = new_record? ? create : update
  result != false
end

2行目で、対象のインスタンスが読み取り専用かどうか確認するようです。
メソッドの名前からして、readonly属性はboolean型で定義して、readonly: trueとすることで、利用することができそうです。
プログラマーが自ら定義することもできますが、railsのことだし、メソッドによって定義する方法もありそう。

readonly?
# File activerecord/lib/active_record/base.rb, line 628
      def readonly?
        @readonly
      end

読み取り専用のインスタンスであった場合はReadOnlyRecordエラーが出力されます

ReadOnlyRecordモデル
class ReadOnlyRecord < ActiveRecordError
end
3行目
result = new_record? ? create : update

3項演算子の条件はnew_record?メソッド。
名前の通り、新規レコードか確認するものでしょう。

new_record?
# File activerecord/lib/active_record/base.rb, line 2554
def new_record?
  @new_record || false
end

なんとなく予想できたかもしれませんが、こちらも先程のreadonly?メソッドと同じ仕様のようです。
new_record属性にtrueを設定していた場合はtrueを返し、特に指定がなければfalseとなる模様。
先程は|| falseの記述がなかったので、どう違う動きをするのか気になります。

rubyの場合、値が設定されていない=nilはfalseと判定されます。
わざわざ設定する理由としては2つ考えられます。
1. true、false以外の値が代入された場合にtrue判定してしまうのを防ぐため
2. new_record属性として参照したメモリの位置に以前使用したときのデータが残っている場合の誤動作を防ぐため

2.については組み込みで用いられるC言語など高級でない言語が用いられる分野で考慮されることがあるそうです。
Rubyのような言語でも懸念は必要なのでしょうか。

逆に、readonly?メソッドに同じ処理が施されていないということは、readonly属性として、必ず値が設定される仕組みがあるということでしょうか。

3行目
result = new_record? ? create : update

3項演算子に戻ります。
新しいレコードであればcreateメソッド、そうでなければupdateメソッドが呼び出されます。
ここはシンプルですね。

4行目
result != false

3行目でresultに値が代入された場合はこの等式はtrueとなり、それ以外の場合はfalseとなります。

よくコントローラで

if save
  redirect_to xxx
else
  render :new
end

のような用いられ方をしますが、4行目の等式の結果が返されて、それをif文に使用しているということですね。

saveメソッドの読解は完了です。
ついでに類似メソッドとして、save!メソッドを読んでみましょう。

save!メソッドの読解

save!
# File activerecord/lib/active_record/base.rb, line 2592
      def save!
        create_or_update || raise(RecordNotSaved)
      end

create_or_updateはsaveメソッドと同じ動きですね。

save!メソッドの特徴は保存に失敗したときにエラーを吐くこと。
保存に失敗するとcreate_or_updateがfalseを返します。
A||BのときAがfalseの場合はBが判定されます。

ここでraiseメソッドによりRecordNotSavedエラーが出力されます。

RecordNotSavedモデル
class RecordNotSaved < ActiveRecordError
  attr_reader :record

  def initialize(message = nil, record = nil)
    @record = record
    super(message)
  end
end

このクラスのモデルにより、エラーメッセージが出力されるようです。
エラーの動作の仕組みは勉強にはなりそうですが、今回の記事の趣旨からは外れるので、別の機会に筆は譲りましょう。

まとめ

saveメソッドの動き

  1. 読み取り専用か確認し、必要に応じてエラーを吐く
  2. 新規データかどうかによってcreateまたはupdateメソッドを呼び出す。
  3. 結果に応じてtrueまたはfalseを返す

save!メソッドの動き

saveメソッドの中で上記1~3を実行するメソッドが3にてfalseを返した時、RecordNotSavedエラーを吐く

感想

  • railsの学習を始めた頃のイメージはcreateはnew+saveだよ〜とおぼえていたので、saveの中でcreateメソッドが呼ばれていたのは驚きだった
  • create/updateメソッドとエラー周りの仕組みに関心をもったので読んでみたい。
  • めちゃくちゃ重要な機能をシンプルで短いコードで実現していて美しい!! 特に4行目。

今回もマニアックな記事を読んでいただきありがとうございました。
またお会いしましょう。

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

Railsチュートリアルエラー :FormatError: fixture key is not a hash

エラーの発生!

Ruby on Rails チュートリアル 第6番
"リスト 8.24: 有効な情報を使ってユーザーログインをテストする"
を進めていると。。。。

今まで通っていたテストが全てエラーに!!

21 tests, 0 assertions, 0 failures, 21 errors, 0 skips

しかし、よくよくエラーを確認すると...

ActiveRecord::Fixture::FormatError: fixture key is not a hash: 
/home/ubuntu/environment/sample_app/test/fixtures/users.yml

fixturesフォルダの中のusers.ymlに原因があると判明。

調べてみると、ymlではインデントも意味をもつとのこと。

【参考】
https://teratail.com/questions/263996

/home/ubuntu/environment/sample_app/test/fixtures/users.ymlを
下記のように修正して、

michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>

再度テストを試すと。。。

21 tests, 54 assertions, 0 failures, 0 errors, 0 skips

無事解決!

まずは落ち着いてエラーメッセージを読んでいくことで、
比較的スムーズな解決につながりましたー!

エラーで困っている初学者の方にとって
少しでも助けになれば嬉しいです。

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

ポートフォリオにTwitterAPIを使ってツイートする機能を実装する(アカウント申請した後の流れ)

Ruby on Railsで製作したポートフォリオ(ダイエットアプリ)にTwitterAPIを連携させて、ポートフォリオで投稿すると、Twitterにツイートされるような設定を行いました。

やりたいこと

ポートフォリオのアプリで投稿すると、Twitterで「投稿しました」とツイートされるようにしたい。
Railsで投稿系のアプリを作っている方で、「Twitter APIと連携させたい」と思っている方の参考になれば幸いです。

ちなみに、deviseと連携してTwitterログイン機能を実装されている方もいますが、僕はまだそれはやっていません。

TwitterAPIと連携させる手順

① TwitterAPIに自分のアプリの登録手続きを行う
② Twitter developerアカウントでAPI key(Consumer Key)、API secret key(Cosumer Secret)、Access token、Access token secretの4つを取得する
③ TwitterAPIに書き込み権限を許可する
④ gem 'twitter'を$ bundle installする
⑤ 投稿するコントローラー(PostsController)に必要な記述を追加する

今回の記事では、②〜⑤について説明します。

①TwitterAPIへのアプリ登録手続きのやり方 については、
Twitter API 登録 (アカウント申請方法) から承認されるまでの手順まとめ ※2019年8月時点の情報
にかなり丁寧に書かれていますので、こちらを見ながら進めて頂ければと思います。

TwitterAPIの用途を英語で書く必要がありますが、英語力全く自信ない自分でもできたので、全然大丈夫だと思います。

①の手続きが特に問題なければ、「Account Application Approved」というタイトルのメールが届きます。そのメールに記載されているリンクからdeveloper acountに入ることができます。メールは手続きしてからその日の数時間後に届きました。

② Twitter developerアカウントでAPI key(Consumer Key)、API secret key(Cosumer Secret)、Access token、Access token secretの4つを取得する

「いきなり難しそうな横文字が出てきて何だ?!」と思うかもしれませんが、なんてことないです。
あとで⑤コントローラーに追加の記述をするところで使うための準備をするだけです。

画像の箇所にカーソルを合わせると「keys and tokens」と表示が出てきます。カギのマークをクリックします。
スクリーンショット 2020-11-21 午後7.30.55.png

すでにAPIキーとシークレットキーを登録済みであれば「View keys」を、まだ作っていなければ「Regenerate」をクリックしましょう。
どちらを押しても特に問題が起きる心配はないので大丈夫です。
スクリーンショット 2020-11-21 午後7.34.15.png

必要な4つの情報のうち2つはわかったので、残りあと2つですね。
Access tokens & secretの「Revoke」は取り消すという意味だったので、「Regenerate(作り直す)」がいいと思います。
スクリーンショット 2020-11-21 午後7.38.02.png

③ TwitterAPIに書き込み権限を許可する

settingタブをクリックしましょう。
スクリーンショット 2020-11-21 午後7.55.21.png

ちょっと下の方に行って「App permissions」の「Edit」をクリックしましょう。
スクリーンショット 2020-11-21 午後7.57.37.png

書き込み権限を許可する設定にしましょう。
スクリーンショット 2020-11-21 午後7.58.52.png

④ gem 'twitter'を$ bundle installする

https://github.com/sferik/twitter
公式ページにも書いてあるので、ぜひご確認ください。

⑤ 投稿するコントローラー(PostsController)に必要な記述を追加する

やることはめちゃくちゃ少ないです。
本当にこれだけ?!という感じです。

privateメソッドに下記の記述を書き込む
先ほどdeveloper アカウントで確認した4つの情報をここで使います。

セキュリティ対策として、環境変数での設定に修正しました。

posts_controller.rb
private
  def twitter_client
    @client = Twitter::REST::Client.new do |config|
      config.consumer_key = ENV['CONSUMER_KEY']
      config.consumer_secret = ENV['CONSUMER_SECRET']
      config.access_token = ENV['ACCESS_TOKEN']
      config.access_token_secret = ENV['ACCESS_TOKEN_SECRET']
    end
  end

環境変数で設定するやり方

下記コマンドで環境変数を設定するファイルを開きます。

vim ~/.bash_profile 

そこに下記のように環境変数を設定します。

export CONSUMER_KEY='config.consumer_keyに代入する文字列'
export CONSUMER_SECRET='config.consumer_secretに代入する文字列'
export ACCESS_TOKEN='config.access_tokenに代入する文字列'
export ACCESS_TOKEN_SECRET='config.access_token_secretに代入する文字列'

最後に上記の設定を反映させるため下記のコマンドを実行します。

source ~/.bash_profile

☆☆☆ 注意 ☆☆☆
Twitter APIの権限を「読み込みのみ」から「読み込みと書き込み」に変更した場合は、③で確認した4つのキーやトークンを作り直す必要があります(regenerateをクリックするだけです)のでご注意ください。

上記の設定をcreateメソッドに反映させるために、上の方に下記の記述を追加します。

posts_controller.rb
before_action :twitter_client, only: [:create]

そして投稿が保存された場合の挙動に下記の記述を追加します。
引数にTwitterにツイートしたい文章を入れてください。

posts_controller.rb
def create
  @post = Post.new(post_params)
    if @post.save
      @client.update("例)投稿しました!(ここにTwitterにツイートされる内容を書き込む)")
      redirect_back(fallback_location: root_path)
    else
     #省略
    end
end

どうでしたか?思ったことやること少なくてビックリですよね?
それだけ使いやすく作られているということですね!

僕は書き込み権限を許可する設定をせずに投稿しようとしてちょっと詰まりました...。
皆さんはそんなことしないようにお気をつけください。

完成イメージ動画

8b7f434d469683ebbbdca1146c56feb4.gif
Gyazo GIFは7秒しか録画できないので、非常にわかりづらいかもしれませんが、雰囲気だけでも伝わってくれたら嬉しいです。

ダイエットアプリなので、食べたものを投稿できるようになっているのですが、食べたものを投稿すると、Twitterで「TwitterAPIと連携しました!!」と自動でツイートされる設定をしました。よーく見ると見えるかもしれません。

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

railsのgem deviseのログインページを開くコントローラを読解してみた

rails読解シリーズ、第4回はdeviseのコントローラを読解してみました。
いつも深入りしすぎて、全体像が見えづらくなっているので今回はあっさりめに読んでみました。

ログインページを開くための、session#newアクションを見ていきます

session#new
def new
  self.resource = resource_class.new(sign_in_params)
  clean_up_passwords(resource)
  yield resource if block_given?
  respond_with(resource, serialize_options(resource))
end
2行目
self.resource = resource_class.new(sign_in_params)

resource_classによってdeviseを適用しているモデル、例えばUserモデルが呼ばれます。
sign_in_paramsによりストロングパラメータを通した値が、Userモデルに格納されます。

resource_classメソッドの中の動作を見たものの、いまいちなぜ、Userモデルが呼び出されるのかは理解しきれませんでした?
resource_classメソッドを定義している理由はおそらく、deviseの適用モデルを柔軟に変えることができるようにするためかと推測しています。

3行目
clean_up_passwords(resource)

clean_up_passwordsメソッドにより、resourceに格納されたUserモデルのpassword、password_confirmation属性を空にする(nilを代入する)

4行目
yield resource if block_given?

コントローラにブロックが渡された場合はyield resourceが実行される。
これはdeviseのデフォルトコントローラを再利用しつつ、追加で機能を追加する場合に機能する。
deviseコントローラをカスタマイズするため、のgenerateコマンドをターミナルで実行すると生成されるコントローラは、

カスタマイズ用に生成されるコントローラ
def new
  super
end

のように生成される。
このときsuperにブロックを引数と渡すことで、ブロック内の動作をdeviseのコントローラ動作に追加することができる。

deviseのデフォルトの動作を残しつつ、カスタマイズする方法
def new
  super { |resource| ... }
end

逆にdeviseのデフォルトコントローラ側ではsuperメソッドによってブロックが渡されていないか確認し、渡されている場合はyield resourceによってブロックのコードを実行する。

5行目
  respond_with(resource, serialize_options(resource))

respond_withメソッドは、こちらによると、httpレスポンスを生成するようです。

クライアント側が指定したmimetypeに応じて、レスポンスは生成できます。
mimetypeとはデータの形式のことで、HTML形式やjson形式などが含まれます。
中のコードは見ていませんが、第2引数のserialize_options(resource)でmimetypeの指定ができるようになっているのかと思われます。

ソースコード
# File actionpack/lib/action_controller/metal/mime_responds.rb, line 323
    def respond_with(*resources, &block)
      raise "In order to use respond_with, first you need to declare the formats your "              "controller responds to in the class level" if self.class.mimes_for_respond_to.empty?

      if collector = retrieve_collector_from_mimes(&block)
        options = resources.size == 1 ? {} : resources.extract_options!
        options[:default_response] = collector.response
        (options.delete(:responder) || self.class.responder).call(self, resources, options)
      end
    end

中身の動きに関してはrespond_toに関する記事が参考になりそう。

まとめ

収穫はdeviseのデフォルトコントローラに上書き、ではなく追加する方法がわかったことですかね。
かなりシンプルな動きです。
どちらかというとcurrent_userとかヘルパーメソッドの定義のほうが気になります。

短いですが今回は以上です。

おまけ

resource_classメソッドについて、わかるところまで。
ほぼメモ書きです。

class DeviseController < Devise.parent_controller.constantize

def resource_class
  devise_mapping.to
end

def devise_mapping
  @devise_mapping ||= request.env["devise.mapping"]
end

@devise_mappingにはMappingモデルのインスタンスが代入されている。
Mappingモデルにはtoメソッドが定義されている。
toメソッドはDeviseMappingモデルのklass属性の値を呼び出す。
klass属性の中にはUserモデルが格納されている。

Mappingモデル
class Mapping #:nodoc:
  attr_reader :singular, :scoped_path, :path, :controllers, :path_names,
              :class_name, :sign_out_via, :format, :used_routes, :used_helpers,
              :failure_app, :router_name

def to
  @klass.get
end

このとき@devise_mappingにどのようにして、Mappingモデルの値が渡されているのか、さらにUserモデルの情報が渡されているのか追いきれませんでした。

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

railsで"Model.new"が呼ばれるとどうなるか読解してみた

railsメソッドがどのように定義されているのか学んでいく企画第3弾はモデルからオブジェクトを生成するnewメソッド(Rails APIリンク)です。

実際にはnewメソッドはRubyで定義されています。
ので、正確にはタイトルの通り、railsのActiveModelによって定義されるモデルのインスタンスをrubyで定義されているnewメソッドが呼び出して生成したときに何が起こるか、について見ていきます。

new
class Person
  include ActiveModel::Model
  attr_accessor :name, :age
end

person = Person.new(name: 'bob', age: '18')
person.name # => "bob"
person.age  # => "18"

newメソッドが呼ばれたら?

newメソッドによりインスタンスが生成されると、ActiveModel::Modelのinitializeメソッドが動作します。

newメソッドの定義
def initialize(attributes = {})
  assign_attributes(attributes) if attributes

  super()
end

非常にシンプルですね。

1行目
def initialize(attributes = {})

attributesにはハッシュが引き渡されます。
何も渡されない場合は空のハッシュ{}が渡されます。

2行目
assign_attributes(attributes) if attributes

冒頭のPerson.new(name: 'bob', age: '18')のようにattributesに値が渡されている場合はassign_attributesメソッドが呼び出されます。

assign_attributesメソッド

assign_attributesメソッド
# File activemodel/lib/active_model/attribute_assignment.rb, line 26
def assign_attributes(new_attributes)
  if !new_attributes.respond_to?(:stringify_keys)
    raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
  end
  return if new_attributes.nil? || new_attributes.empty?

  attributes = new_attributes.stringify_keys
  _assign_attributes(sanitize_for_mass_assignment(attributes))
end

プロなりたての私からすると、相変わらずインパクトある見た目してます^^;
目をそらさずに読んでいきましょう。

1行目は引数が渡されているだけなので飛ばします。

2行目
if !new_attributes.respond_to?(:stringify_keys)

条件式を見てみましょう。
new_attributesにはハッシュが渡されています。
respond_to?メソッドにはシンボル:stringify_keysが渡されています。

さっそくrespond_to?メソッドについても読んでいきましょう。

respond_to?メソッド

respond_to?メソッド
# File activemodel/lib/active_model/attribute_methods.rb, line 448
def respond_to?(method, include_private_methods = false)
  if super
    true
  elsif !include_private_methods && super(method, true)
    # If we're here then we haven't found among non-private methods
    # but found among all methods. Which means that the given method is private.
    false
  else
    !matched_attribute_method(method.to_s).nil?
  end
end

newメソッドは表面的には4行しかありませんが、裏に何行ものコードが隠れていますね。

1行目
def respond_to?(method, include_private_methods = false)

methodには先程述べた通り、:stringify_keysというシンボルが渡されています。
第2引数には何も渡されていないので、定義通りfalseが代入されます。

2行目
if super

if文の中にsuperメソッドが書かれています。superメソッドについて見ていきましょう。

かなりややこしいですが、親クラスのメソッドの中から同名のメソッドを呼び出す特殊なメソッドのようです。

superの使用例
class Car
  def accele
    print("アクセルを踏みました¥n")
  end
end

class Soarer < Car
  def accele
    super
    print("加速しました¥n")
  end
end

(引用)

つまりrepond_to?メソッドが定義されているクラスの親クラスにある同名のメソッドの中身を見ることが、このコードの意味を理解する役に立ちそうです。

respond_to?メソッドが定義されているのはAttributeMethodsモジュール。

モジュールに関してもクラスと同様に継承関係が生まれるようです。

class Example
  include ActiveModel::AttributeMethods
end

この場合は

Example < ActiveModel::AttributeMethods

の関係となります。
include(含む)という意味から生まれる感覚からすると逆が直感的と思いますが、違うのですね。

class Example
  prepend ActiveModel::AttributeMethods
end

この場合は

ActiveModel::AttributeMethods < Example

となります。

したがってsuperメソッドにより親モデルのメソッドを呼び出す、と考えるとどこかに後者のprependによる宣言が書かれていることが推測されます。

しかしながらprependにより明示的に、モジュールを呼び出している箇所がRailsライブラリ内に含まれていませんでした。

そこでRailsアプリ上で私が定義したモデルに対して、継承関係のうち祖先を返すメソッドであるancestorsメソッドを用いました。

ancestorsメソッドによる継承関係の出力
=> [OriginalModel(id: integer, created_at: datetime, updated_at: datetime),
 OriginalModel::GeneratedAttributeMethods,
 # 省略
 ActiveRecord::AttributeMethods,
 ActiveModel::AttributeMethods,
 ActiveModel::Validations::Callbacks,
 # 省略
 Object,
 # 省略
 Kernel,
 BasicObject]

2つ目のブロックの2段めに目的のActiveModel::AttributeMethodsがありますね。
しかしその親にあたるActiveModel::Validations::Callbacksにはモジュールの使用は宣言されていません。

この場合はancestorsメソッドで示された順に、そのクラス/モジュール内にメソッドが定義されていないか確認していきます。

親をたどっていくとようやく見つけました。3つ目のブロックObjectモデルはrespond_to?メソッドを定義しています。

ここで定義されているrespond_to?メソッドはオブジェクトがrespond_to?("・・・")のように渡された引数"・・・"の名前を持つメソッドを持っているか?ということを判定するメソッドです。

ちなみにObject.respond_to?(:respond_to?)はtrueを返します。

superメソッドに引数が明記されていない場合は、呼び出し元のメソッドの引数がそのまま渡されます。

したがってここでは、respond_to?(:stringify_keys)が動いていることになります。

stringify_keysメソッドはハッシュに対して定義されるため、正しく定義したモデルであれば、必ずtrueになりそうです。

ぶっちゃけrespond_to?メソッドでググれば、わかった内容ではありますが、moduleと継承のことを学べたので良しとしましょう。

さて、もとのassign_attributesメソッドに戻りましょう。

assign_attributesメソッド
# File activemodel/lib/active_model/attribute_assignment.rb, line 26
def assign_attributes(new_attributes)
  if !new_attributes.respond_to?(:stringify_keys)
    raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
  end
  return if new_attributes.nil? || new_attributes.empty?

  attributes = new_attributes.stringify_keys
  _assign_attributes(sanitize_for_mass_assignment(attributes))
end

二行目のif条件のうち、new_attributes.respond_to?(~)はnew_attributesがハッシュであればtrue、ハッシュお外であればfalseとなります。

!が先頭についているので全体としては、ハッシュでない場合はifの内容に進み、AugumentErrorを発する、ということになります。

メッセージもそのままですね。
「ハッシュいれてね。」

5行目
return if new_attributes.nil? || new_attributes.empty?

次に代入された属性に対してnil?とempty?のチェックがなされます。
該当する場合は、assign_attributesメソッドからは何も返されません。

ちなみにnil?メソッドはレシーバ(この場合はnew_attributes
)がnilのときにtrueを返し、empty?メソッドはレシーバの長さが0のときにtrueを返します。

6行目
attributes = new_attributes.stringify_keys

ここではstringify_keysによってハッシュnew_attributesのキーがシンボル形式から文字列形式に変換されます。

7行目
_assign_attributes(sanitize_for_mass_assignment(attributes))

_assign_attributesメソッドという、微妙に名前が違うメソッドが呼び出されます。
※ちなみに先頭のアンダースコアは、Rubyの慣習でプライベートメソッドの名前の先頭につけるものです。

またその引数にはsanitize_for_mass_assignmentにattributesが代入された状態で呼び出されています。

ますはsanitize_for_mass_assignmentについて見てみましょう。

sanitize_for_mass_assignmentメソッド
# File activemodel/lib/active_model/forbidden_attributes_protection.rb, line 21
      def sanitize_for_mass_assignment(attributes)
        if attributes.respond_to?(:permitted?)
          raise ActiveModel::ForbiddenAttributesError if !attributes.permitted?
          attributes.to_h
        else
          attributes
        end
      end

2行目のif条件を見てみましょう。
ここではrespond_to?メソッドが呼び出されます。

このメソッドはattributesがpermitted?メソッドが定義されたメソッドであるかを確認しています。

permitted?メソッドはActiveControllerのストロングパラメータモジュールに定義されたメソッドで、ストロングパラメータが通されてpermit: trueとなっているかを判定するメソッドです。

したがってparamsなどコントローラ上で生成された値がattributesに代入されている場合は1つ目のif条件が満たされます。

3行目
raise ActiveModel::ForbiddenAttributesError if !attributes.permitted?

ここでストロングパラメータを通しているか?permitted?メソッドで判定します。
許可されていない場合はエラー発生。

4行目
attributes.to_h

この行によって、ストロングパラメータに許可された値に限り、ActiveControllerクラスから"ただの"ハッシュが返されます。

assign_attributesメソッドに戻ります。

(再掲)

assign_attributesメソッド7行目
_assign_attributes(sanitize_for_mass_assignment(attributes))

sanitize_for_mass_assignment(attributes)によりハッシュが_assign_attributesメソッドに代入されることがわかりました。

_assign_attributesメソッド
# File activerecord/lib/active_record/attribute_assignment.rb, line 12
      def _assign_attributes(attributes)
        multi_parameter_attributes  = {}
        nested_parameter_attributes = {}

        attributes.each do |k, v|
          if k.include?("(")
            multi_parameter_attributes[k] = attributes.delete(k)
          elsif v.is_a?(Hash)
            nested_parameter_attributes[k] = attributes.delete(k)
          end
        end
        super(attributes)

        assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
        assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
      end

4行目にeach文があります。

このattributesは必ずハッシュであるため、キー+値ごとにeach文の内容が実行されます。
kはハッシュのキー、vはバリュー=値を意味します。

1つ目のif条件については、用途不明です。
multi_parameter属性というものがあるらしい?

2つ目のif条件についてはハッシュの値にさらにハッシュが格納されていた場合にtrueとなります。
(is_a?(~)はレシーバが~であるかどうか判定するメソッドですね。今回はハッシュであればtrueを返します)

これはOwnerとCarのように、親子関係のアソシエーションにある2つのモデルを同時に扱う際に、親の属性を格納したハッシュの中に、子の属性を格納したハッシュをネスト(入れ子に)するケースを扱うためのものと考えられます。

このとき、nested_parameter_attributes[k]にネストされたハッシュが格納されます。

(これまでの読解企画にも同上しているように右辺のdeleteはカッコ内のキーに対応する値を返すために用いられていると推測されます)

これでmulti_parameterとnested_parameterに値が格納されました。

11行目
super(attributes)

親クラスの同名メソッド(_assign_attributesメソッド)を参照します。

継承関係
 ActiveRecord::AttributeAssignment,
 ActiveModel::AttributeAssignment,
 ## 略
 Object,
 ## 略

assign_attributesメソッドが定義されているactiverecord/attribute_assignment.rbにおいて

module ActiveRecord
  module AttributeAssignment
    include ActiveModel::AttributeAssignment

    private
      def _assign_attributes(attributes)
      ## 略
      end

のようにActiveRecord::AttributeAssignmentはActiveModel::AttributesAssignmentをincludeしているので、superが参照するのはActiveModelに定義された_assign_attributesメソッドです。

def _assign_attributes(attributes)
  attributes.each do |k, v|
    _assign_attribute(k, v)
  end
end

def _assign_attribute(k, v)
  setter = :"#{k}="
  if respond_to?(setter)
    public_send(setter, v)
  else
    raise UnknownAttributeError.new(self, k.to_s)
  end
end

_assign_attributesメソッド内ではハッシュの各属性について_assign_attributeメソッドが呼び出されます。
(複数形と単数形の違いがありますよ)

7行目でsetterと名の通り、セッター名称を代入します。
セッター、ゲッターについてはこちらを初心者向けのとしてリンク貼っておきます。
xxx=という名称はRubyにおいてセッターを定義するメソッドの命名パターンですね。
※xxxにはモデルに定義した属性の名称が入ります。

8行目のif条件ではrespond_to?メソッドでsetterに代入された名称のセッターが定義されているか確認します。
存在する場合はpublic_sendメソッドを呼び出し。

public_sendメソッドはRubyリファレンスによるとObjectクラスに定義されたメソッドで、第一引数に渡された名称のメソッドに第2引数に渡された値を引き渡して実行します。

セッターメソッドはモデルの当該属性に値を代入するメソッド。
_assign_attributes(複数形の方ですよ)の引数は、最初にnewメソッドを呼び出したときに代入した値をハッシュに変換したもの。

例えばUserというモデルとその属性として、name、ageを定義していたならUser.new(name: John, age: 20)のように名前と年齢を代入しているかと思います

つまり各属性に指定した値を代入するという処理が行われています。

本来であればUser.name = Johnのように指定しないといけなかったところを、代わりにやってくれているのですね。

ちなみにif文でのrespond_to?メソッドによる確認の結果、定義されていない属性の場合は、

11行目
  raise UnknownAttributeError.new(self, k.to_s)

そんな属性知らないよエラーが返ります。
ここ、newメソッドが呼び出されているの面白いですね。

UnknownAttributeErrorモデル
class UnknownAttributeError < NoMethodError
  attr_reader :record, :attribute

  def initialize(record, attribute)
    @record = record
    @attribute = attribute
    super("unknown attribute '#{attribute}' for #{@record.class}.")
  end
end

のように定義されており、属性は2つrecordとattributeで、なおかつselfとk.to_sの2つ渡されているので、
11行目でnewメソッドが呼び出された場合、同じ11行目の分岐に入ることはないので、無限ループに陥ることはなさそうです。

なおエラーメッセージは

unknown attribute '#{キー名称}' for #{モデル名称}.

のように返すと読み取れます。

結局の所、ActiveModelに定義された_assign_attributesメソッドによってセッターが代入された属性の数分呼び出されるのがnewメソッドの肝でしたね^^;

これ以上は蛇足感が否めませんがせっかくなので、ActiveRecordに定義された_assign_attributesモデルの読解に戻ってみましょう。

_assign_attributesメソッド
# File activerecord/lib/active_record/attribute_assignment.rb, line 12
      def _assign_attributes(attributes)
        multi_parameter_attributes  = {}
        nested_parameter_attributes = {}

        attributes.each do |k, v|
          if k.include?("(")
            multi_parameter_attributes[k] = attributes.delete(k)
          elsif v.is_a?(Hash)
            nested_parameter_attributes[k] = attributes.delete(k)
          end
        end
        super(attributes)

        assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
        assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
      end

11行目のsuperメソッドのところをここまでで読みました。
現状は謎のmulti_parameter_attributesとモデルをネストしたときのためのnested_parameter_attributesに値が代入された上で、newによって生成したいインスタンスの各属性に対して
値が代入された状態です。

12行目のメソッドについても見てみましょう

assign_nested_parameter_attributes
def assign_nested_parameter_attributes(pairs)
  pairs.each { |k, v| _assign_attribute(k, v) }
end

何のことはありません。
最初にnewメソッドが呼び出された際に{name: John, age: 20, car_attributes: {color: red, type: sportscar } }
のように渡されていた部分から抜き出された{color: red, type: sportscar}がこのメソッドの引数として渡されています。
これらの属性に定義されたセッターメソッドを繰り返し呼び出しているだけです。

assign_multiparameter_attributesメソッドの定義についても読解すれば、multiparameterとやらが何を実現するためのものかわかりそうですが、聞いたことがない以上使われていないものと思われるので、今回は省略します。

assign_attributesメソッド
# File activemodel/lib/active_model/attribute_assignment.rb, line 26
def assign_attributes(new_attributes)
  if !new_attributes.respond_to?(:stringify_keys)
    raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
  end
  return if new_attributes.nil? || new_attributes.empty?

  attributes = new_attributes.stringify_keys
  _assign_attributes(sanitize_for_mass_assignment(attributes))
end

これで最終行の_assign_attributesメソッドの動作がわかりました。

newメソッドの定義
def initialize(attributes = {})
  assign_attributes(attributes) if attributes

  super()
end

ようやく、もとのnewメソッドに戻ってきました。

残りはsuperのみですね。
superメソッドは()なしで呼ぶと、引数をそのまま、superにより呼び出されるメソッドに渡します。
ここでいうとattributesです。

super()の場合は引数なしで呼び出されます。
親モデルのinitializeが呼び出されることで、親モデルの属性がインスタンスの属性として定義されるのかと思います。
引数が渡されていないのでどういう動きになるのか少々疑問ですが、省略します。

以上です。

まとめ

newメソッドにより、モデルが生成されると、ActiveModelに定義されたinitializeにより、assign_attributesメソッドが呼ばれ、newメソッドに渡されたハッシュを分解して、属性ごとにセッターメソッドを呼び出す。

生Rubyだといちいち値を代入しなければならないところ、railsでは勝手にやってくれるのですね。
便利だな〜。

今回新たに生まれた疑問としては、じゃあ生Rubyのnewメソッド自体はどう定義されているのか、インスタンスを生成した際にinitializeメソッドが呼び出される仕組み、今度はsaveメソッドでレコードが保存される仕組み、が気になりますね。

今後の課題としましょう。

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

Rubyでロックファイルによる簡易的排他制御

冪等性がないやアクセス制限などの理由で、同時実行不可という要望はしばしば出てきます。普段ではデータベースに実行フラグを置いたり、RedisやQueueで制御したりするのが一般的ですが、どれも実装コストが高くて、小規模プロジェクトにはコスパがやや高いと思います。
ロックファイルで制御を行えば、データベースの変更などが不要で、手軽いに排他制御ができます。

TD;LR

require 'fileutils'
DIR_NAME = 'tmp/locks'

# 排他制御用ラッパー
def synchronized(key = :default_lock)
  # フォルダーがない時エラーが生じるので、あらかじめ生成しておく
  FileUtils.mkdir_p(DIR_NAME)
  lock_file_path = "#{DIR_NAME}/#{key}"
  File.open(lock_file_path, 'w') do |lock_file|
    # 排他ロック
    if lock_file.flock(File::LOCK_EX|File::LOCK_NB)
      yield
    else
      raise "[Error] #{key} in use"
    end
  end
end

# ロックの取得
def locked?(key = :default_lock)
  lock_file_path = "#{DIR_NAME}/#{key}"
  return false if !File.exist?(lock_file_path)
  File.open(lock_file_path, 'r') do |lock_file|
    # 共有ロックの取得を試みる、失敗した時はファイルの最終更新日時を返す
    lock_file.flock(File::LOCK_SH|File::LOCK_NB) ? false : lock_file.mtime
  end
end

# 使い方
def run_synchronized
  locked_at = locked?(:run_synchronized)
  if locked_at
    puts "Locked at #{locked_at.strftime("%Y-%m-%d %H:%M:%S")}"
    return
  end

  synchronized(:run_synchronized) do
    p 'Start....'
    sleep(10)
    p 'End...'
  end
end

実行結果(例)
2つのセッションでrun_synchronizedを同時実行してみます。

# 1つ目のセッション
irb(main):002:0> run_synchronized
"Started at 2020-11-21 17:45:07"
"Ended at 2020-11-21 17:45:17"
=> "Ended at 2020-11-21 17:45:17"

# 2つ目のセッション
irb(main):002:0> run_synchronized
Locked at 2020-11-21 17:45:07
=> nil

本文

synchronizedメソッド

解説

このメソッドは今回の仕組みのコアです。OSのファイルシステムを活用して、ファイルの排他ロックをRubyのロジックのロックに転用しています。
そしてロックファイルの名前の違いで、異なるロックを同時に存在できます。

def synchronized(key = :default_lock)
  # フォルダーがない時エラーが生じるので、あらかじめ生成しておく
  FileUtils.mkdir_p(DIR_NAME)
  lock_file_path = "#{DIR_NAME}/#{key}"
  File.open(lock_file_path, 'w') do |lock_file|
    # 排他ロック
    if lock_file.flock(File::LOCK_EX|File::LOCK_NB)
      yield
    else
      raise "[Error] #{key} in use"
    end
  end
end

使い方

synchronized do
  # Do something in this block
end

もし排他ロックの取得が成功した場合、ブロック内のロジックが実行されます。ブロックの実行が完了した時(ブロックが例外発生した時も)、ロックが自動解除されます。
もし排他ロックの取得が失敗した場合、すでにロックされたとみなし、ブロックが実行されず、例外がraiseされます。

#<RuntimeError: [Error] run_synchronized in use>
=> #<RuntimeError: [Error] run_synchronized in use>

locked? メソッド

解説

このメソッドはおまけみたいな感じで、ロックを触れずにロックされるかを確認できます。共有ロックの取得を試みます。もし取得できない(ロックされている)場合、該当ファイルが最後に更新された時刻を返すように作っています。
ただし、ここにBugがあります。synchronizedでロックの取得が失敗した時も、ファイルの更新時刻も変化するので、返された時刻は必ずしもロックされた時刻ではありません(実装により回避はできますが)。

# ロックの取得
def locked?(key = :default_lock)
  lock_file_path = "#{DIR_NAME}/#{key}"
  return false if !File.exist?(lock_file_path)
  File.open(lock_file_path, 'r') do |lock_file|
    # 共有ロックの取得を試みる、失敗した時はファイルの最終更新日時を返す
    lock_file.flock(File::LOCK_SH|File::LOCK_NB) ? false : lock_file.mtime
  end
end
# ロックされた場合
irb(main):007:0> locked?(:run_synchronized)
=> 2020-11-21 18:18:45 +0900

# ロックされてない場合
irb(main):008:0> locked?(:run_synchronized)
=> false

参考

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

「もっと見る」で非同期(Ajax)ページネーションを実装する

はじめに

やりたいこと

「もっと見る」を押すことで次の記事が出るようにしたい

やってみて

JavaScriptが苦手な僕でも、簡単に実装することができました。
今回はjQueryで実装しています。

実装画面

画面収録 2020-11-21 15.56.42.mov.gif

実装例

Output App

参考

kaminari徹底入門
t-taira blog > ailsで「もっと見る」の実装

※情報のソースが古く、かつ私のGemの理解及びjQueryの理解が乏しいため、適切ではない表現が含まれているかもしれません。

手順

Gem kaminari インストール

Gemfile
gem 'kaminari'

Controllerにメソッド追加

controllers/items/controller.rb
class ItemsController < ApplicationController

  def index
    @items = Item.all.order('created_at DESC').page(params[:page])
  end

end

kaminariをインストールすると
.page(params[:page])
こちらの記述でページネーションを組むことができます。

1ページ毎の取得件数を指定

Controllerに指定する場合

controllers/items/controller.rb
class ItemsController < ApplicationController

  def index
    @items = Item.order('created_at DESC').page(params[:page]).per(10)
  end

end

Modelに指定する場合

models/item.rb
class Item < ApplicationRecord
  paginates_per 10
end

View

each展開箇所を部分テンプレートに切り出す

views/items/index.html.erb
# @items リスト展開

<%= render "shared/item-list" %>

# // @items リスト展開
views/shared/_item-list.html.erb
<% @items.each do |item| %>

# 中略

<% end %>

jQueryで読み込むためにidをつける

views/items/index.html.erb
# @items リスト展開

<div id='item-pagenate'>
  <%= render "shared/item-list" %>
</div>

# // @items リスト展開

ページネーションリンクを作る

views/items/index.html.erb
# @items リスト展開

<div id='item-pagenate'>
  <%= render "shared/item-list" %>
</div>

<%= link_to_next_page @items, 'もっと見る', remote: true, class: 'more-link', id: 'more-link' %>

# // @items リスト展開

kaminariをインストールすると
link_to_next_page
こちらの記述でページネーションのリンクが作れます。

また、remote: trueを付与することでサーバーに送られるデータがjson形式となり非同期通信が可能となります。

jQuery側の処理を記述

json形式のデータを受け取り、返す処理の内容を記述します。

views/items/inde.js.erb
$('#item-pagenate').append("<%= escape_javascript(render 'shared/item-list', object: @items) %>")
$("#more-link").replaceWith("<%= escape_javascript(
  link_to_next_page(@items, 'もっと見る', remote: true, class: 'more-link', id: 'more-link')
) %>");

一行目で id : 'item-pagenate'の部分にappend(引数を追加するメソッド)を用いて次ページ分の@itemsを渡した部分テンプレートを挿入しています。
二行目以降では、さらに次のページ分のリンクを表示させるための記述をしています(replaceWithで元々存在しているリンクを置き換えています)

以上で、「もっと見る」スタイルのページネーションは完成です!

おわりに

jQueryが勉強不足ゆえ、メソッドの意味を一つ一つ調べながら解説したつもりですが、間違っていたり情報が古かったらごめんなさい。

ただ、基本的にはこちらの記述で実装はできるはずなので、うまく表示されないのであれば

  • jQueryの設定がそもそも出来ていない
  • 部分テンプレート(renderメソッド)の記述に誤りがある

のどちらかが疑わしいと思います。

私はファイル名の誤記というシンプルなミスで無駄な時間を消費しました。

お気をつけください。

✔︎

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

「もっと見る」でページネーションを非同期(Ajax)で実装する

はじめに

やりたいこと

「もっと見る」を押すことで次の記事が出るようにしたい

やってみて

JavaScriptが苦手な僕でも、簡単に実装することができました。
今回はjQueryで実装しています。

実装画面

画面収録 2020-11-21 15.56.42.mov.gif

実装例

Output App

参考

kaminari徹底入門
t-taira blog > ailsで「もっと見る」の実装

※情報のソースが古く、かつ私のGemの理解及びjQueryの理解が乏しいため、適切ではない表現が含まれているかもしれません。

手順

Gem kaminari インストール

Gemfile
gem 'kaminari'

Controllerにメソッド追加

controllers/items/controller.rb
class ItemsController < ApplicationController

  def index
    @items = Item.all.order('created_at DESC').page(params[:page])
  end

end

kaminariをインストールすると
.page(params[:page])
こちらの記述でページネーションを組むことができます。

1ページ毎の取得件数を指定

Controllerに指定する場合

controllers/items/controller.rb
class ItemsController < ApplicationController

  def index
    @items = Item.order('created_at DESC').page(params[:page]).per(10)
  end

end

Modelに指定する場合

models/item.rb
class Item < ApplicationRecord
  paginates_per 10
end

View

each展開箇所を部分テンプレートに切り出す

views/items/index.html.erb
# @items リスト展開

<%= render "shared/item-list" %>

# // @items リスト展開
views/shared/_item-list.html.erb
<% @items.each do |item| %>

# 中略

<% end %>

jQueryで読み込むためにidをつける

views/items/index.html.erb
# @items リスト展開

<div id='item-pagenate'>
  <%= render "shared/item-list" %>
</div>

# // @items リスト展開

ページネーションリンクを作る

views/items/index.html.erb
# @items リスト展開

<div id='item-pagenate'>
  <%= render "shared/item-list" %>
</div>

<%= link_to_next_page @items, 'もっと見る', remote: true, class: 'more-link', id: 'more-link' %>

# // @items リスト展開

kaminariをインストールすると
link_to_next_page
こちらの記述でページネーションのリンクが作れます。

また、remote: trueを付与することでサーバーに送られるデータがjson形式となり非同期通信が可能となります。

jQuery側の処理を記述

json形式のデータを受け取り、返す処理の内容を記述します。

views/items/inde.js.erb
$('#item-pagenate').append("<%= escape_javascript(render 'shared/item-list', object: @items) %>")
$("#more-link").replaceWith("<%= escape_javascript(
  link_to_next_page(@items, 'もっと見る', remote: true, class: 'more-link', id: 'more-link')
) %>");

一行目で id : 'item-pagenate'の部分にappend(引数を追加するメソッド)を用いて次ページ分の@itemsを渡した部分テンプレートを挿入しています。
二行目以降では、さらに次のページ分のリンクを表示させるための記述をしています(replaceWithで元々存在しているリンクを置き換えています)

以上で、「もっと見る」スタイルのページネーションは完成です!

おわりに

jQueryが勉強不足ゆえ、メソッドの意味を一つ一つ調べながら解説したつもりですが、間違っていたり情報が古かったらごめんなさい。

ただ、基本的にはこちらの記述で実装はできるはずなので、うまく表示されないのであれば

  • jQueryの設定がそもそも出来ていない
  • 部分テンプレート(renderメソッド)の記述に誤りがある

のどちらかが疑わしいと思います。

私はファイル名の誤記というシンプルなミスで無駄な時間を消費しました。

お気をつけください。

✔︎

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

「もっと見る」で非同期(Ajax)ページネーションする方法

はじめに

やりたいこと

「もっと見る」を押すことで次の記事が出るようにしたい

やってみて

JavaScriptが苦手な僕でも、簡単に実装することができました。
今回はjQueryで実装しています。

実装画面

画面収録 2020-11-21 15.56.42.mov.gif

実装例

Output App

参考

kaminari徹底入門
t-taira blog > ailsで「もっと見る」の実装

※情報のソースが古く、かつ私のGemの理解及びjQueryの理解が乏しいため、適切ではない表現が含まれているかもしれません。

手順

Gem kaminari インストール

Gemfile
gem 'kaminari'

Controllerにメソッド追加

controllers/items/controller.rb
class ItemsController < ApplicationController

  def index
    @items = Item.all.order('created_at DESC').page(params[:page])
  end

end

kaminariをインストールすると
.page(params[:page])
こちらの記述でページネーションを組むことができます。

1ページ毎の取得件数を指定

Controllerに指定する場合

controllers/items/controller.rb
class ItemsController < ApplicationController

  def index
    @items = Item.order('created_at DESC').page(params[:page]).per(10)
  end

end

Modelに指定する場合

models/item.rb
class Item < ApplicationRecord
  paginates_per 10
end

View

each展開箇所を部分テンプレートに切り出す

views/items/index.html.erb
# @items リスト展開

<%= render "shared/item-list" %>

# // @items リスト展開
views/shared/_item-list.html.erb
<% @items.each do |item| %>

# 中略

<% end %>

jQueryで読み込むためにidをつける

views/items/index.html.erb
# @items リスト展開

<div id='item-pagenate'>
  <%= render "shared/item-list" %>
</div>

# // @items リスト展開

ページネーションリンクを作る

views/items/index.html.erb
# @items リスト展開

<div id='item-pagenate'>
  <%= render "shared/item-list" %>
</div>

<%= link_to_next_page @items, 'もっと見る', remote: true, class: 'more-link', id: 'more-link' %>

# // @items リスト展開

kaminariをインストールすると
link_to_next_page
こちらの記述でページネーションのリンクが作れます。

また、remote: trueを付与することでサーバーに送られるデータがjson形式となり非同期通信が可能となります。

jQuery側の処理を記述

json形式のデータを受け取り、返す処理の内容を記述します。

views/items/inde.js.erb
$('#item-pagenate').append("<%= escape_javascript(render 'shared/item-list', object: @items) %>")
$("#more-link").replaceWith("<%= escape_javascript(
  link_to_next_page(@items, 'もっと見る', remote: true, class: 'more-link', id: 'more-link')
) %>");

一行目で id : 'item-pagenate'の部分にappend(引数を追加するメソッド)を用いて次ページ分の@itemsを渡した部分テンプレートを挿入しています。
二行目以降では、さらに次のページ分のリンクを表示させるための記述をしています(replaceWithで元々存在しているリンクを置き換えています)

以上で、「もっと見る」スタイルのページネーションは完成です!

おわりに

jQueryが勉強不足ゆえ、メソッドの意味を一つ一つ調べながら解説したつもりですが、間違っていたり情報が古かったらごめんなさい。

ただ、基本的にはこちらの記述で実装はできるはずなので、うまく表示されないのであれば

  • jQueryの設定がそもそも出来ていない
  • 部分テンプレート(renderメソッド)の記述に誤りがある

のどちらかが疑わしいと思います。

私はファイル名の誤記というシンプルなミスで無駄な時間を消費しました。

お気をつけください。

✔︎

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

Railsを使ったToDoリストの作成(8.ログイン機能の実装)

概要

本記事は、初学者がRailsを使ってToDoリストを作成する過程を記したものです。
私と同じく初学者の方で、Railsのアウトプット段階でつまづいている方に向けて基礎の基礎を押さえた解説をしております。
抜け漏れや説明不足など多々あるとは思いますが、読んでくださった方にとって少しでも役に立つ記事であれば幸いです。

環境

  • Homebrew: 2.5.10 -> MacOSのパッケージ管理ツール
  • ruby: 2.6.5p114 -> Ruby
  • Rails: 6.0.3.4 -> Rails
  • node: 14.3.0 -> Node.js
  • yarn: 1.22.10 -> JSのパッケージ管理ツール
  • Bundler: 2.1.4 -> gemのバージョン管理ツール
iTerm
$ brew -v => Homebrew 2.5.10
$ ruby -v => ruby 2.6.5p114
$ rails -v => Rails 6.0.3.4
$ npm version => node: '14.3.0'
$ yarn -v => 1.22.10
$ Bundler -v => Bundler version 2.1.4

第8章 Deviseを使ったユーザ認証機能の実装

第8章では、Railsのライブラリであるdeviseを使ってユーザ認証機能を実装していきます。

1 deviseの設定を行う

まず、Gemfileにてdeviseを読み込みます。

Gemfile
gem 'devise'

?‍♂️diviseというライブラリを読み込んでください
->Gemfileに追記した時は必ずiTermにて$bundle installを行う

$bundle installができたらiTermにて以下のコマンドを入力しジェネレーターを実行します。

iTerm
$ rails generate devise:install
=>create  config/initializers/devise.rb
  create  config/locales/devise.en.yml

?‍♂️ジェネレーターをインストールしてください
?configのinitializersにdevise.rbというファイルを作成しました
?configのlocalesにdevise.en.ymlというファイルを作成しました

また、コンソールには上記2つのファイルを作成した旨に加え、以下4つの設定が必要である旨も表示されるため、指示に従い設定していきます。

config/environments/development.rbに以下のようなメール設定に関する情報を入力します。

development.rb
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

config/routes.rbにroot_urlを設定します。

routes.rb
Rails.application.routes.draw do
  root to: "home#index"
end

app/views/layouts/application.html.erbにフラッシュメッセージが表示されるように設定します。

application.html.erb
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>

④ログイン画面のHTMLテンプレートファイルを作成します。

iTerm
$ rails g devise:views

上記の設定が完了したら、最後にUserを管理するモデルを作成します。

iTerm
$ rails generate devise User
=>create db/migrate/20201114092712_devise_create_users.rb
  create app/models/user.rb

?‍♂️Userを管理できるモデルを作成してください
?migrationファイル(データを作るためのテーブルを作成する場所)を作成しました
?userモデルを作成しました
->migrationファイルを作成した場合は必ず$rails db:migrateでmigrationファイルを読み込む

※エラーが起こってしまう場合は定期的に$rails sでサーバを立ち上げ直しましょう。

ここまできたら以下のようなページ等が作成できているはずです。スクリーンショット 2020-11-14 18.47.28.png

2 ビュー

deviseの設定が終わったらビューの設定をしていきます。
deviseは初期設定がerbなので、まずはhamlに書き換えます。

既存のerbファイルをhamlに置き換えるためにはターミナルにて以下のコマンドを実行します。

iterm
$ bundle exec rake haml:replace_erbs

また、デフォルトではエラーメッセージなどが英語のままなので日本語化します。
日本語化するためにはconfig/localsdevise.ja.ymlファイルを作り、ルールを記述します。

3 ログイン状態によって表示を変更する

まずは、ログアウト機能を実装しましょう。

app/views/layouts/application.html.haml
= link_to 'Log out', destroy_user_session_path, data: { method: 'delete' }

rails infoで確認すると、userのログイン状況を削除するためにはdestroy_user_session_pathを指定することがわかるので指定します。
link_toはデフォルトはGETリクエストのため、data: { method: 'delete' }を指定します。

では、次にログイン状態によって表示を変更できるよう条件分けしていきます。

app/views/layouts/application.html.haml
- if user_signed_in?
  .dropdown
    = image_tag 'default-avatar.png', class: 'header_avatar dropbtn'
    .dropdown-content
      %a{:href => "#"} Profile
      = link_to 'Log out', destroy_user_session_path, data: { method: 'delete' }
- else
  = link_to new_user_session_path, class: 'header_loginBtn' do
    = image_tag 'log-in.png'

ポイントは2つあります。

  • if user_signed_in?でログイン時の状態を表示します。user_signed_in?メソッドがdeviseでは用意されており、ユーザがログインしているか否かを判断してくれます。
  • ログインしていないときはnew_user_session_pathでログインページに遷移するようにします。

4 一部機能をログイン時にしか操作できないようにする

投稿機能や編集機能や削除機能をログイン時のみ使えるようにしていきます。
コントローラに以下のように記述します。

app/controllers/boards_controller.rb
before_action :authenticate_user!, only: [:new, :create, :edit, :update, :destroy]

authenticate_user!はdeviseが用意してくれているメソッドでログインしているユーザのみ操作を許可し、ログインしていない場合はログイン画面に遷移するようにしてくれます。
beeforeアクションを使うことで各アクションの前にログイン状況を判断できるようにします。
しかし、Read機能はログインしていなくてもできるようにしたいので、onlyオプションで対象を絞っています。

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

hidden_field と hidden_field_tag 備忘録

<%= form_with(model: @comment, local: true) do |f| %>
  <%= f.hidden_field :post_id, value: @post.id %>
  <%= f.text_field :content %>
<% end %>
controller
# 受け取り方
class CommentsController < ApplicationController
  def create
    @comment = Comment.new(comment_params)
    @comment.user_id = current_user.id
    if @comment.save
      redirect_back(fallback_location: root_path)
    else
      @post = Post.find(params[:post_id])
      @comments = @post.comments
      render 'posts/show'
    end
  end

  private
  def comment_params
    params.require(:comment).permit(:post_id, :content)
  end
end
<%= form_with(model: @comment, local: true) do |f| %>
  <%= hidden_field_tag :post_id, @post.id %>
  <%= f.text_field :content %>
<% end %>
controller
# 受け取り方
params[:follow_id]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ActiveSupport::Concernでクラスメソッドを定義して、さらにクラスメソッドを呼び出せるのか

こちらの記事を参照して、

https://qiita.com/h-shima/items/d772b4cbe7368ddb8255

modelにインクルードすることによりConcernでクラスメソッドを定義できることがわかったのですが、そのクラスメソッド内でさらにモデルに定義したクラスメソッドが呼び出せるか検証しました。

concern内でクラスメソッドを呼び出す

app/models/concerns/hogehoge.rb
module Hogehoge
  extend ActiveSupport::Concern

  module ClassMethods
    def my_class_method
      puts("実行結果")
      self.model_class_method("model_class_method")
      puts("my_class_method")
    end
  end

end 

モデルを定義

app/models/concerns/model_class_hoge.rb
class ModelClassHoge < ApplicationRecord

  include hogehoge

  def self.model_class_method(str)
    puts("call " + str)
  end
end

実行した結果

ModelClassHoge.my_class_method

実行結果
call model_class_method
my_class_method

別のクラスを用意してみる

app/models/concerns/model_class_hogehoge.rb
class ModelClassHogehoge < ApplicationRecord

  include hogehoge

  def self.model_class_method(str)
    puts("ModelClassHogehoge Call " + str)
  end
end

実行した結果

ModelClassHogehoge.my_class_method

実行結果
ModelClassHogehoge Call model_class_method
my_class_method

問題なく呼び出せてる感じはする。

ちなみにモデル側にmodel_class_methodを定義していない場合、
undefined method
となることは確認しました。

このあたりのメタプログラミング的な部分はよく分かっていないので、追記していきます。

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

letとlet!の使い分け(Ruby/Rspec)

letとlet!の使い方ではまったので備忘録として残します。

letとは


letはインスタンス変数やローカル変数をletという機能で置き換えることができます。
ex)
@user = user.create
let(:user) { create(:user) }

letを使うと何が嬉しいかというと、テストコードがすっきりするというのと
letはその変数が必要になるまでは呼び出されないという遅延評価されるという特徴があるため、効率のよいテストコードを作成できることです。
beforeを使うとブロックの宣言時に全ての変数が評価されるため、
使用していない変数があったとしても評価されてしまい
あまり効率のよくないテストコードになってしまうことがあります。

let!は基本的にはbeforeと同じ意味になります。
つまりテストを実行する上で前提条件にあたる部分はletではなくlet!で記載してあげる必要があります。

ex)
before do
@user = user.create
end
let!(:user) { create(:user) }

ただしメソッドを事前に実行したい場合は、letでは記載できないため、beforeを使う必要があります。

before do
  sign_in(current_user)
end

はまったポイント


ぼくがはまったのは記事アプリを作成する際に
ログアウトの機能のテストコードを作成していたときです。

ぼくが最初に作成したテストコードは以下になります。
この場合、次のように処理されるためletで定義していても問題ありませんでした。
subjectが実行される
subjectの中でheaders使用されているため
let(:headers) { user.create_new_auth_token }が実行される
headersの中でuserが使用されているため
let(:user) { create(:user) }が実行される

describe "DELETE /api/v1/auth/sign_out" do
 subject { delete(destroy_api_v1_user_session_path, headers: headers) }
 context "ユーザーがログインしているとき" do
  let(:user) { create(:user) }
  let(:headers) { user.create_new_auth_token }
  it "ログアウトできる" do
   subject
   expect(response).to have_http_status(:ok)
  end
 end
end

次にヘッダーのトークン情報の変化を確認するために
以下のコードに修正しました。

describe "DELETE /api/v1/auth/sign_out" do
 subject { delete(destroy_api_v1_user_session_path, headers: headers) }
 context "ユーザーがログインしているとき" do
 let(:user) { create(:user) }
 let(:headers) { user.create_new_auth_token }
  it "ログアウトできる" do
   expect { subject }.to change { user.reload.tokens }.from(be_present).to(be_blank)
   expect(response).to have_http_status(:ok)
  end
 end
end
---エラー内容
Failure/Error:expect{subject}.to change{user.reload.tokens}.from(be_present).to(be_blank)
expected user.reload.tokens to have initially been be present, but was {}

expect { A }.to change { B }.from(X).to(Y)
とすると、ぼくは最初Aを実行後Bの前後でXからYに変化しているかのテストをしていると考えていました。
その場合、順番に処理していけばletで定義していても問題ないのでは?と考えていました。

メンターの方からの指摘で、正しくは
expect { A }.to change { B }.from(X).to(Y)
とすると、Aが発生した時にBがXからYに変化しているかのテストがやりたかったことになります。
つまりリロード(B)前後じゃなくsubject(A)の前後でuser.reload.tokensの値が変化しているかどうかをチェックする。

修正したコードです。
headersをlet→let!で定義するように修正しました。

describe "DELETE /api/v1/auth/sign_out" do
 subject { delete(destroy_api_v1_user_session_path, headers: headers) }
 context "ユーザーがログインしているとき" do
 let(:user) { create(:user) }
 let!(:headers) { user.create_new_auth_token }
  it "ログアウトできる" do
   expect { subject }.to change { user.reload.tokens }.from(be_present).to(be_blank)
   expect(response).to have_http_status(:ok)
  end
 end
end

今回はまったのはテストコードの処理を正しく理解していなかったため、
前提条件を正しく定義できていなかったことが原因でした。

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

[Rails]ゲストログイン機能

はじめに

ポートフォリオにゲストログイン機能があった方が良いとのことだったので実装してみました。

目次

  • 1. ルーティング
  • 2. コントローラー
  • 3. モデル
  • 4. ビュー

1. ルーティング

routes.rbにゲストログイン用のアクションを設定します。
deviseのsessionsコントローラーに新しくメソッドを追加しています。

config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  devise_scope :user do
    post 'users/guest_sign_in', to: 'users/sessions#new_guest'
  end
  ~~
end

2. コントローラー

controllersの中にusersフォルダを作成しsessions_controller.rbファイルを作成。
new_guestメソッドをコントローラーに作成します。
guestメソッドはモデルに作成します。

app/controllers/users/sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
  def new_guest
    user = User.guest
    sign_in user   # ユーザーをログインさせる
    redirect_to root_path, notice: 'ゲストユーザーとしてログインしました。'
  end
end

3. モデル

find_or_create_by!でゲストユーザーが無ければ作成、あれば取り出します。
あとはゲストユーザーがない時に作成するユーザー情報を記述しています。

app/models/user.rb
def self.guest
  find_or_create_by!(email: 'aaa@aaa.com') do |user|
    user.password = SecureRandom.urlsafe_base64
    user.password_confirmation = user.password
    user.nickname = 'サンプル'
    user.birthday = '2000-01-01'
  end
  ~~
end

4. ビュー

今回はヘッダーにゲストログイン用のボタンを作成しました。

app/views/shared/_header.html.erb
<ul class='lists-right'>
  <% if user_signed_in? %>
    <li><%= link_to current_user.nickname, user_path(current_user.id), class: "user-nickname" %></li>
    <li><%= link_to 'ログアウト', destroy_user_session_path, method: :delete, class: "logout" %></li>
  <% else %>
    <li><%= link_to 'ゲストログイン(閲覧用)', users_guest_sign_in_path, method: :post, class: "login" %></li>
    <li><%= link_to 'ログイン', new_user_session_path, class: "login" %></li>
    <li><%= link_to '新規登録', new_user_registration_path, class: "sign-up" %></li>
  <% end %>
</ul>

参考リンク

https://qiita.com/take18k_tech/items/35f9b5883f5be4c6e104

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

ActionController::UrlGenerationError in CreditCard#new ~~pathをprefixで書いた時のエラー~~

はじめに

マイページへのリンク先をprefixで書いた時にエラーでつまずいたので記録として残します。

実装したこと

hamlの = link_toでurlを"/users/#{current_user.id}"
記載してマイページに遷移するようにしていました。
"/users/#{current_user.id}"をprefixの記載に変更するために
rails routesで探しにいきました。
user:id routes.png
userとなっているのでuser_pathとして記載しました。
すると下記のエラーが発生。
UrlGenerationError.png
No route matches {:action=>"show", :controller=>"users"}, missing required keys: [:id]
のエラーコードを見てmissing required keys: [:id]の部分に着目しました。
「idが必要だけどないよー」と言われてます。
なのでuser_pathの後に(current_user.id)を足して記述しました
結果、無事に解決しました。

修正前

= link_to "マイページ", user_path

修正後

= link_to "マイページ", user_path(current_user.id)

おわりに

これからは最初からprefixで記載する習慣をつけて行こうと思います。
今回はエラー文を読み取って割と早期に解決することができましたので
今後も冷静にエラー文を分析して解決する能力を高めていこうと思います。
何か補足事項とうございましたら、是非コメントをお願いいたします。

しょうま

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

rails new でDBをPostgresqlに指定、pg 1.2.3 が足りずbundle install を実行するも Can't find the 'libpq-fe.h headerが発生した時の解決法

使用環境

Windows10
Ubuntu18.0

やりたかったこと

以下のコマンドでDBをpostgresqlに指定しRailsアプリを作成。

$ rails new app_name -d postgresql

エラー状況

1)上記rails newの実行途中でpg 1.2.3が足りないようで、passwordを入力しbundle installを実行。

Fetching pg 1.2.3

Your user account isn't allowed to install to the system RubyGems.
  You can cancel this installation and run:

      bundle install --path vendor/bundle

  to install the gems into ./vendor/bundle/, or you can enter your password
  and install the bundled gems to RubyGems using sudo.

  Password:
Installing pg 1.2.3 with native extensions
/usr/lib/ruby/vendor_ruby/rubygems/defaults/operating_system.rb:10: warning: constant Gem::ConfigMap is deprecated
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

2)bundle install 実行中に” libpq-fe.h header が見つかりません。”と表示。

Using config values from /usr/bin/pg_config
checking for libpq-fe.h... no
Can't find the 'libpq-fe.h header
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of
necessary
libraries and/or headers.  Check the mkmf.log file for more details.You may
need configuration options.

3)さらに読み進めると"pg -v '1.2.3'がインストールされているか確認してください。"と表示。

An error occurred while installing pg (1.2.3), and Bundler
cannot continue.
Make sure that `gem install pg -v '1.2.3' --source
'https://rubygems.org/'` succeeds before bundling.

解決法

手順

Step.1 libpq-fe.h headerを追加するためlibpq -dev packageをインストール。
Step.2 gem pg -v 1.2.3をインストール。
Step.3 bundle installを実行。
Step.4 rails new app_name -d postgresqlを再実行。

Step.1 libpq-fe.h headerを追加するためlibpq -dev packageをインストール。

libpq-fe.h headerはlibpq -dev packageの一部のようです。

#Step1. libpq -dev をインストール
$ sudo apt-get install libpq-dev
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following packages were automatically installed and are no longer required:
  gyp libc-ares2 libhttp-parser2.7.1 libjs-async libjs-inherits
  libjs-node-uuid libjs-underscore libssl1.0-dev libuv1-dev
  nodejs-doc
Use 'sudo apt autoremove' to remove them.
The following additional packages will be installed:
  libpq5
Suggested packages:
  postgresql-doc-13
The following NEW packages will be installed:
  libpq-dev
The following packages will be upgraded:
  libpq5
1 upgraded, 1 newly installed, 0 to remove and 13 not upgraded.

Step.2 gem pg -v 1.2.3をインストール。

#Step.2 gem pg -v 1.2.3をインストール。
$ sudo gem install pg -v '1.2.3'

Step.3 bundle installを実行。

#Step.3 bundle installを実行。
$ bundle install

Step.4 rails new app_name -d postgresqlを再実行。

#Step.4 rails new app_name -d postgresqlを再実行。
$ rails new app_name -d postgresql

最初に実行した際に作成されたファイルがあるので、上書確認をされれば”y”で上書き実行し、アプリ作成完了。

Overwrite /home/XXXXXX/.gitignore? (enter "h" for h lp) [Ynaqdhm] y

おわりに

rails学習を開始したばかりの初学者です。
学習に詰まった箇所への対応策のアウトプットです。
間違い等ありましたら、ご指摘頂けますと幸いです。

参考

Step.1で参考にさせて頂いた記事。
stackoverflow: Can't find the 'libpq-fe.h header when trying to install pg gem
ubuntu packages: パッケージ: libpq-dev (10.15-0ubuntu0.18.04.1 など)

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

Railsを使ったToDoリストの作成(7.CRUDのDelete機能)

概要

本記事は、初学者がRailsを使ってToDoリストを作成する過程を記したものです。
私と同じく初学者の方で、Railsのアウトプット段階でつまづいている方に向けて基礎の基礎を押さえた解説をしております。
抜け漏れや説明不足など多々あるとは思いますが、読んでくださった方にとって少しでも役に立つ記事であれば幸いです。

環境

  • Homebrew: 2.5.10 -> MacOSのパッケージ管理ツール
  • ruby: 2.6.5p114 -> Ruby
  • Rails: 6.0.3.4 -> Rails
  • node: 14.3.0 -> Node.js
  • yarn: 1.22.10 -> JSのパッケージ管理ツール
  • Bundler: 2.1.4 -> gemのバージョン管理ツール
iTerm
$ brew -v => Homebrew 2.5.10
$ ruby -v => ruby 2.6.5p114
$ rails -v => Rails 6.0.3.4
$ npm version => node: '14.3.0'
$ yarn -v => 1.22.10
$ Bundler -v => Bundler version 2.1.4

第7章 CRUDのDelete(destroy)

第7章では、以下の機能を実装していきます。

  • 削除機能(destroyアクション)

では、詳しく見ていきましょう。

destroyアクション

今回は、destroyアクションを使って削除機能を実装していきます。
一覧表示画面から削除ボタンを押したら、本当に削除していいのかの確認が表示され、OKを押すと投稿が削除され、一覧表示画面に遷移するようにしていきたいと思います。

1 コントローラ

コントローラには以下のように記述します。

app/controllers/boards_controller.rb
def destroy
    board = Board.find(params[:id])
    board.destroy!
    redirect_to root_path, notice: 'Delete successful'
end

まず、削除する対象のboardを取得します。引数にparams[:id]を指定することで、パラメータ経由で対象のboardのidを探し、オブジェクトを取得します。
その上で、ActiveRecordのdestroyメソッドを使って、対象のレコードをデータベースから削除し、削除した旨を伝えるFlashメッセージとともに一覧表示画面に遷移させます。

ポイントは2つあります。

  • 今回はビューに渡さないため、インスタンス変数には代入していません。
  • destroyメソッドに!を付けているのは削除されなかった場合に例外を発生させるためです。例外が発生したらそこで処理が止まりますが、destroyアクションでは確実にデータを削除して欲しいので、意図的に例外が発生する状況を作っています。

2 ビュー

次に、一覧表示画面に削除するためのリンクを貼って、削除ボタンを押したら、本当に削除していいのかの確認が表示され、OKを押すと投稿が削除され流ようにしていきたいと思います。

app/views/index.html.haml
= link_to 'Delete', board_path(board), data: { method: 'delete', confirm: '本当に削除してもいいですか?' }

ここでのポイントは第3引数にdataを指定していることです。

記述 意味
method: 'delete' board_pathはデフォルトでGETリクエストが指定されているため、DELETEリクエストにする場合はこのように指定してあげる必要があります
confirm: '本当に削除してもいいですか?' confirmでは確認表示を実装することができます。valueに指定した値が画面上に表示されます。

以上で、削除機能の実装は完了です。

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

Railsを使ったToDoリストの作成(6.CRUDのUpdate機能)

概要

本記事は、初学者がRailsを使ってToDoリストを作成する過程を記したものです。
私と同じく初学者の方で、Railsのアウトプット段階でつまづいている方に向けて基礎の基礎を押さえた解説をしております。
抜け漏れや説明不足など多々あるとは思いますが、読んでくださった方にとって少しでも役に立つ記事であれば幸いです。

環境

  • Homebrew: 2.5.10 -> MacOSのパッケージ管理ツール
  • ruby: 2.6.5p114 -> Ruby
  • Rails: 6.0.3.4 -> Rails
  • node: 14.3.0 -> Node.js
  • yarn: 1.22.10 -> JSのパッケージ管理ツール
  • Bundler: 2.1.4 -> gemのバージョン管理ツール
iTerm
$ brew -v => Homebrew 2.5.10
$ ruby -v => ruby 2.6.5p114
$ rails -v => Rails 6.0.3.4
$ npm version => node: '14.3.0'
$ yarn -v => 1.22.10
$ Bundler -v => Bundler version 2.1.4

第6章 CRUDのUpdate(edit/update)

第6章では、以下の機能を実装していきます。

  • 編集画面(editアクション)
  • 編集画面で入力された情報をデータベースに保存する機能(updateアクション)

では、詳しく見ていきましょう。

1 editアクション

まずは、editアクションを使って編集画面を実装していきます。
一覧表示画面から編集ボタンを押したら、編集画面に遷移できるようにしたいと思います。

1 コントローラ

コントローラには以下のように記述します。

app/controllers/boards_controller.rb
def edit
    @board = Board.find(params[:id])
end

editアクションでは、登録済みのフォーム画面を予め表示できるように、URLに含まれているタスクのidをパラメータから受け取り、それを使ってデータベースから対象のオブジェクトを取得します。

2 ビュー

ビューでは、まず編集フォームを作成します。
フォームはnew.html.hamlで作成したものとほぼ同じ内容です。

app/views/edit.html.haml
.container
  %h2.form_title Edit Board
  = form_with(model: @board, local: 'true') do |f|
    %div
      = f.label :title, 'Name'
    %div
      = f.text_field :name, class: 'text'
    - if @board.errors.include?(:name)
      %p.error=@board.errors.full_messages_for(:name).first
    %div
      = f.label :description, 'Description'
    %div
      = f.text_area :description, class: 'text'
    - if @board.errors.include?(:description)
      %p.error=@board.errors.full_messages_for(:description).first
    %div
      = f.submit :Submit, class: 'btn-primary'

次に、一覧表示画面に編集画面へ遷移することができるようリンクを貼ります。

app/views/index.html.haml
= link_to 'Edit', edit_board_path(board)

以上で、編集画面の実装は終了です。

2 updateアクション

編集画面から入力されたデータを使ってデータベースを更新するためにupdateアクションを実装していきます。

コントローラ

コントローラに以下のように記述します。

app/controllers/boards_controller.rb
def update
    @board = Board.find(params[:id])
    if @board.update(board_params)
        redirect_to board_path(@board), notice: 'Update successful'
    else
        flash.now[:error] = 'Could not update'
        render :edit
    end
end

Board.findで更新したいboardを取得します。
ActiveRecordのupdateメソッドを実行し、編集したインスタンスをデータベース保存します。

editアクションとupdateアクションの内容や関係性は、新規登録機能のnewアクションとcreateアクションに似ているため、Createの記事を参考にしてください。

以上で、編集機能の実装は完了です。

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

[Rails]hidden_fieldとhidden_field_tagの違いについて![初心者]

どういう時に使用するの?

form_withform_forを利用して、ユーザーに何かを打ち込んでもらい、送信してもらいたい時に便利なメソッドです。

例えば、AmazonのようなECサイトで、ショッピングカートの商品を購入するとき。
ユーザーからすると、「確定ボタン」だけ押したいのに、再度usernameaddressを打たなければならないのは面倒ですよね。
また、パラメーターを経由したいけれども、ユーザー側にその情報を隠しておきたい時などにも使えます。

使い方

hidden_field
hidden_field :値の取得時に使用する名前(シンボル), :value => 実際に渡す値
              #第一引数→name属性                        #第二引数→value属性

アクションでhidden_fieldで渡されたパラメータを受け取ることが出来ます。
ここで、controllerに記述する際に、フォームフィールドに紐づく値となっている為、記述方法は
params[:モデル名][:渡したname属性]
という形になるので、注意が必要です。

hidden_field_tag
hidden_field_tag :渡したいパラメータの値, 実際に渡す値

それぞれの使用タイミング

hidden_field
form_withやform_forで渡すインスタンスがある場合(もしくはそれらのヘルパーを使っている場合)。

hidden_field_tag
一個だけパラメータを他のアクションへ単体で渡したい時に、独立して使用。

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

ヘッダーが日本語の巨大CSVを取り込んでみる

CSVファイルの取り込みが必要とする場面はかなり頻繁に出ていると思います。しかし数百メガ以上ファイルサイズだとサーバーの処理に影響出かねなません。そして取り込まれるファイルのヘッダーが日本語だと、実装のハードルが格段上がると思います。

TL;DR

Demo用のRuby fileは下記のURLで見れます。
https://gist.github.com/jerrywdlee/55ba403f02651afc67dbda8185329780

# まずは日本語ヘッダーを英語に転換するlambdaを作成
headers_jp = %w[ID 名前 フリガナ 年齢 血液型 都道府県 携帯キャリア]
headers_en = %w[id name kana age blood state carrier]
headers_dict = headers_jp.zip(headers_en).to_h
converter = lambda { |h| headers_dict[h] }

# CSV.foreacで1行ずつ取り込む
CSV.foreach(path, headers: true, header_converters: converter) do |row|
  p row.headers if row['id'] == 1
  p row if row['id'] == 1
  # Do sth. with `row`
end

解説

サンプルファイルの作成

下記のロジックでサンプルCSVを作成しています。

def generate(cnt = 1_000_000)
  headers = %w[ID 名前 フリガナ 年齢 血液型 都道府県 携帯キャリア]
  exec_benchmark do
    CSV.open('dummy_data.csv', 'w', write_headers: true, headers: headers) do |csv|
      cnt.times do |i|
        age = rand(100)
        blood = %w[A B O AB][rand(4)]
        carrier = %w[ドコモ au ソフトバンク][rand(3)]
        csv << [i, '打見 花子', 'ダミ ハナコ', age, blood, '東京都', carrier]
      end
    end
    file_size = `ls -lah dummy_data.csv | awk '{print $5}'`
    puts "File size: #{file_size}"
  end
end

100万行で65Mのファイルとなります。

irb(main):002:0> LargeUnicodeCsv.generate
File size: 65M
Time: 10.3s
Memory: 2.11MB

CSV.table

まずおなじみのCSV.tableメソッドを試してみます。

def csv_table(path = 'dummy_data.csv')
  exec_benchmark do
    table = CSV.table(path)
    p table.headers
    p table[0]
  end
end

結果見ると、まず、日本語のヘッダーは全部空白になっていました。
そしてin memory処理したせいか、1G程度のメモリを消耗しました。
処理時間も1分間超えていました。

irb(main):003:0> LargeUnicodeCsv.csv_table
[:id, :"", :"", :"", :"", :"", :""]
#<CSV::Row id:0 :"打見 花子" :"ダミ ハナコ" :27 :"B" :"東京都" :"ソフトバンク">
Time: 74.47s
Memory: 1021.91MB
=> nil

CSV.each

こちらの記事が紹介した、ヘッダー行が日本語のCSVの対処法です。

def csv_each(path = 'dummy_data.csv')
  exec_benchmark do
    headers_jp = %w[ID 名前 フリガナ 年齢 血液型 都道府県 携帯キャリア]
    headers_en = %w[id name kana age blood state carrier]
    headers_dict = headers_jp.zip(headers_en).to_h
    header_converter = lambda { |h| headers_dict[h] }

    csv = CSV.read(path, headers: :first_row, header_converters: header_converter)

    p csv.headers
    p csv[0]
  end
end

結果見ると日本語のヘッダーはちゃんと処理されました。
そして処理時間も劇的によくなりました。
しかしメモリ使用はまだ1G程度のままでした。

irb(main):002:0> LargeUnicodeCsv.csv_each
["id", "name", "kana", "age", "blood", "state", "carrier"]
#<CSV::Row "id":"0" "name":"打見 花子" "kana":"ダミ ハナコ" "age":"27" "blood":"B" "state":"東京都" "carrier":"ソフトバンク">
Time: 9.61s
Memory: 1000.56MB

CSV.csv_foreach

今回紹介したいCSV.csv_foreachメソッドです。文章によると、File.openのラッパーのようです。
converters: :integerencoding: 'Shift_JIS:UTF-8'などパラメーターも付けられるので、汎用性はとても高いです。

def csv_foreach(path = 'dummy_data.csv')
  exec_benchmark do
    headers_jp = %w[ID 名前 フリガナ 年齢 血液型 都道府県 携帯キャリア]
    headers_en = %w[id name kana age blood state carrier]
    headers_dict = headers_jp.zip(headers_en).to_h
    converter = lambda { |h| headers_dict[h] }

    CSV.foreach(path, headers: true, header_converters: converter) do |row|
      p row.headers if row['id'] == '1'
      p row if row['id'] == '1'
    end
  end
end

結果見ると日本語のヘッダーはちゃんと処理されました。
そして処理時間も低く抑えられていました。
肝心なメモリ使用量も5メガ以下に抑えられていました。

irb(main):002:0> LargeUnicodeCsv.csv_foreach
["id", "name", "kana", "age", "blood", "state", "carrier"]
#<CSV::Row "id":"1" "name":"打見 花子" "kana":"ダミ ハナコ" "age":"52" "blood":"A" "state":"東京都" "carrier":"ソフトバンク">
Time: 6.34s
Memory: 4.6MB

参考

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