20201121のRubyに関する記事は25件です。

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で続きを読む

【基礎】RubyのREPLとは【Cloud9】

RubyのREPLとは

REPL = Read eval print loop
を略したもので、対話型のRuby実行環境。

Rubyのメソッドを試したりできる。

ターミナルで[$ irb]を実行すると開始。

puts "hello"
hello
=>nil

[exit]で終了できる。

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

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で続きを読む

【Rails・devise】複数モデルを管理する際の<<before_action>>

はじめに

deviseを用いて一般の方とグループ(企業や団体等)で分けてログインを実装する上で、
ログインした人のみが閲覧できるbefore_actionの実装に手間取ってしまったので記録します!

結果から

applicationコントローラーで
・グループの方がログインした場合 → authenticate_any!が有効
・それ以外の人がログインした場合 → authenticate_user!が有効

application_controller.rb
class ApplicationController < ActionController::Base
  def authenticate_any!
    if group_signed_in?
        true
    else
        authenticate_user!
    end
  end
end

各コントローラーで必要なところでbefore_action :authenticate_any!を記載する
application_controller.rbに記載して全て制限したいのであれば
before_action :authenticate_any!の下でauthenticate_any!を定義する

posts_controller.rb
before_action :authenticate_any!

無事、ログインについてのRspecも通りました!

ハマったこと

userまたはgroupがログインしている場合のみページにアクセスができるように実装したかったので、
左から順に評価し、最初に真になったものを返してくれる||を使用してみました。

posts_controller.rb
before_action :authenticate_user! || :authenticate_group!

こうするとauthenticate_user!は反応するもののgroupが反応せず、
groupを作成できるもののroot_pathでCompleted 401 Unauthorizedとエラーが発生して、
弾き出されてuserのsign_inページにリダイレクトされてしまっていました。

Completed 401 UnauthorizedでググるとCSRF トークンの不整合がでてくることが多かったので、
application_controller.rbにprotect_from_forgery with: :null_session記載をして
対応したりもしましたが、ダメでした。。。
(これはCSDF 対策として弱くなるという意見もあるみたいなので、そもそも使わないほうがいいのかも・・・)

学んだこと

before_actionで場合分けしたい場合は、メソッドを作成してその中で条件分けをする!

今までなんとなくbefore_actionを使用していましたが、勉強になりました!(かなり初歩的なのかとは思いますが笑)

同じようなことに困っている方のお役に立てたら嬉しいです。

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

Ruby基礎 ①出力・変数・変数展開・if文(条件分岐)

Rubyとは

RubyはWEBアプリケーションの「システム」をつくるためのプログラミング言語

出力方法

puts "Hallo World!"  
# Hallo World!

文字列にはクォーテーション( ' もしくは " )で囲むこと

puts 10
# 10

puts 10+20
# 30

puts "10" + "20"
# 1020

数値をクォーテーションで囲むと連結してしまう

+  # 足し算
-  # 引き算
/  # 割り算
%  # 割り算の余り

変数

変数とは変数とは、値を入れておく箱のようなもの

name = "Mike"
puts name
# Mike

変数を使うのは、必ず変数を定義(変数名 = 値)した後でなければ使えない
puts "name"にした場合、nameと出力されるので注意

name = "Mike"
puts name + "です"
# Mikeです

・変数の更新

name = "Mike"
puts name
# Mike
name = "John"
puts name
#John

後で代入された値で変数の中身が更新される

・変数の数値計算

number = 2
number = number + 3
puts number
# 5

省略化できる

number = 2
number += 3
puts number
# 5

変数展開

name = "John"
puts "こんにちは #{name}さん"
# こんにちは Johnさん

ダブルクォーテーションで全体を囲むこと
シングルクォーテーションで囲むとそのまま文字列で出力されてしまう
基礎文法 "~#{変数名}~"

.変数展開のメリット
数値と文字列を足し算で連結することはできないため連結したいときに変数展開を使う

age = 18
puts age + "歳です"
# ERROR

age = 18
puts "#{age}歳です"
# 18歳です

if文(条件分岐)

#基礎文法

if 条件式
 処理
end

条件が成立した時(true)に処理が実行される
endを忘れないこと

score = 95
if score > 90
 puts "よくできました!"
end
# よくできました!

score = 80
if score > 90
 puts "よくできました!"
end
# (何も表示されない)

比較演算子

 a > b  # aがbより大きい → true
a >= b  # aがbより大きい または 同じ → true
a <= b  # aよりbが大きい または 同じ → true
 a < b  # aよりbが大きい → true
a == b  # aとbが等しい時 → true
a != b  # aとbが等しくない時 → true
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【自分メモ】フレームワークについて

制作したいものに対して、これを使えば作りやすくなりますよ、的なものがフレームワーク。

Webアプリケーションを制作する時は、RubyonRailsというフレームワークを使います。
RubyonRailsにはWebアプリケーションを制作するための様々な機能がモリモリ入っているので、RubyonRails(の書き方)に従ってコードを書けば、自分で一から作成するよりも遥か簡単にWebアプリケーションを制作することができます。

※自分の考えをメモとして残していますので、間違っていたり修正した方が良い点がございましたら、ドシドシつっこみいただけると嬉しいです!

  • このエントリーをはてなブックマークに追加
  • 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】一意性制約のバリデーションメッセージを設定する

ダウンロード.jpeg
解説はコナン君の声に脳内変換してください

どんな問題?

例えばユーザー登録機能を実装する場合、emailは一意性制約(unique)をつけるのがふつうだよね
当然Userモデルのバリデーションにuniquenessを設定することになるけど、ひとつ困ったことが起こるんだ

このままだと一意性制約に引っかかった場合、エラーメッセージは「メールアドレスはすでに存在します」と表示されちゃうよね

(目暮警部) 「しかしそれがどうしたと言うのかね」

ふつうのユーザーなら特段問題にはならないと思うよ
でも、この機能を使えばあることが可能になるんじゃないかな

そう、この機能によって「すでに登録済みの有効なメールアドレスがあるか」確かめらちゃうよね

悪意あるユーザーにメールアドレスがバレてしまうのは良くないってことなんだ

どのように解決する?

解決方法はいたってシンプル
バリデーションエラーのメッセージを個別に設定するだけだ!

user.rb
validates :email, uniqueness: { message: 'そのアドレスは使用できません' },
                  confirmation: true

ちなみにconfirmation: trueはメールアドレス変更時に確認用の入力をさせることを想定してつけてるオプションだから、この記事の本質とは直接関係ないよ

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

【Rubyによるデザインパターン】コマンドパターンのメモ

【Rubyによるデザインパターン】コマンドパターンのメモ

プログラムの設計力向上のため『Rubyによるデザインパターン』を読んでおり、気になるデザインパターンを、1つずつまとめています。

今回は、コマンドパターンについてまとめました。

【Rubyによるデザインパターン】テンプレートメソッドパターンのメモ - Qiita
【Rubyによるデザインパターン】ファクトリーメソッドパターンのメモ - Qiita
【Rubyによるデザインパターン】ストラテジーパターンのメモ - Qiita
【Rubyによるデザインパターン】コマンドパターンのメモ - Qiita <- 本記事

コマンドパターンについて

特定の動作を実行するオブジェクト(コマンドオブジェクト)を定義し、そのオブジェクトを経由して動作を実行するパターンです。

複雑なロジックを分離したいときに使えるリファクタリングのテクニックとしても有用です。
分離後にはロジックをさらに分割する、変数名を修正するなどのさらなる改善が容易になり、コードの見通しが良くできることが特徴です。

サンプルコード

私はジム通いしているのですが、その趣味にちなんで、ジムの月あたりの維持費用を計算するプログラムを考えます。

簡易的に、維持費用は家賃と設備コストだけと仮定しています。
また、設備コストは設備のグレードから算出しています。

まずはコマンドパターン適用前からです。

class Gym
  def initialize(estate, equipment)
    @estate = estate
    @equipment = equipment
  end

  def calculate_maintenance_cost
    equipment_cost =
      case @equipment[:grade]
      when 'small'
        10
      when 'middle'
        50
      when 'high'
        100
      end

    @estate[:rent] + equipment_cost
  end
end

実行時はこのとおりです。
不動産(estate)や設備(equipment)は本来はクラス化したほうが良いのでしょうが、簡易的にハッシュで定義します。

estate = { name: 'サンプル不動産ビルディング', rent: 30 }
equipment = { name: 'ベンチプレスマシーン', grade: 'middle'}
gym = Gym.new(estate, equipment)
puts gym.calculate_maintenance_cost
# 80

現状では Gym に 1つのメソッドしか定義されていないので、見通しが悪いわけではありませんが、通常モデルにはビジネスロジックが集約され複雑化することが往々にしてあるかと思います。

そこで、 calculate_maintenance_cost 内のロジックにコマンドパターンを適用しオブジェクト化します。

class Gym
  def initialize(estate, equipment)
    @estate = estate
    @equipment = equipment
  end

  def calculate_maintenance_cost
    MaintenanceCostCalculater.new(@estate[:rent], @equipment[:grade]).execute
  end
end

class MaintenanceCostCalculater
  def initialize(rent, equipment_grade)
    @rent = rent
    @equipment_grade = equipment_grade
  end

  def execute
    equipment_cost =
    case @equipment_grade
    when 'small'
      10
    when 'middle'
      50
    when 'high'
      100
    end

    @rent + equipment_cost
  end
end
estate = { name: 'サンプル不動産ビルディング', rent: 30 }
equipment = { name: 'ベンチプレスマシーン', grade: 'middle'}
gym = Gym.new(estate, equipment)
puts gym.calculate_maintenance_cost
# 80

calculate_maintenance_cost の内容を MaintenanceCostCalculater のオブジェクトを通して実行するように修正しました。

MaintenanceCostCalculater は維持費用計算に必要な値のみを属性として持つため、シンプルなクラスです。

コマンドオブジェクトのメソッド名

『リファクタリング 第2版』によると、コマンドオブジェクトは、 execute や call, run といったメソッド名をつけるケースが多いようです。

これを知った私は、確かに業務上でこれらのメソッド名を見かける機会多く、実は普段から触れていたアプリケーションがコマンドパターンに倣っているケースが多かったのだと気づきました。

みなさんの携わっているアプリケーションでも、これらのメソッド名が使用されていた場合、もしかしたらコマンドパターンが適用されているかもしれません。

まとめ

コマンドパターンは特定の動作だけを担うオブジェクトを構築するパターンでした。
複雑なロジックのリファクタリングに有用なため、使いこなせるようになりたいです。

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

Assert&Rubular

Document_Links

テスト駆動開発

assert_equal

とりあえず,equalかどうかを調べる関数を書いてみる.

def assert_equal(expected, result)
  return expected == result
end

p assert_equal(1,1)
p assert_equal(1,2)

これで,

$ ruby assert_equal.rb
true
false

となる.

Colorize

trueとfalseは返すようになったが,パッと見で分かるようにしたい.

というわけで,

assert_equal.rb
require 'colorize'

def assert_equal(expected, result)
  if expected == result
    puts 'true'.green
  else
    puts 'false'.red
  end
end

assert_equal(1,1)
assert_equal(1,2)

で,

$ ruby assert_equal.rb
true
false

trueが赤色に,falseが緑色に出力される.(example内で色付けるのどうするんだ?)

ちなみに,

$ irb
irb(main):001:0> require 'colorize'
=> true
irb(main):002:0> String.colors
=> [:black, :light_black, :red, :light_red, :green, :light_green, :yellow, :light_yellow, :blue, :light_blue, :magenta, :light_magenta, :cyan, :light_cyan, :white, :light_white, :default]

で,何色にcolorizeできるかがわかる.

出力をrichにする

今のままでは,ファイルの中の引数を書き換えねばならないので,修正する.

  • 引数でとってきた,expectedとresultの値を表示する
  • 結果の記述
    • true: print "succeeded in assert_equal.\n".green
    • false: print "failed in assert_equal.\n".red
assert_equal_ro.rb
require 'colorize'

def assert_equal(expected, result)
  if expected == result
    puts "expected :: #{expected}"
    puts "result   :: #{result}"
    print "succeeded in #{__method__}.\n".green
  else
    puts "expected :: #{expected}"
    puts "result   :: #{result}"
    print "failed in #{__method__}.\n".red
  end
end

assert_equal(1,1)
assert_equal(1,2)

これで,

$ ruby assert_equal_ro.rb
expected :: 1
result   :: 1
succeeded in assert_equal.
expected :: 1
result   :: 2
failed in assert_equal.

出来上がり.

assert_not_equal

必要とは思えないが,not_equalでtrueな関数を書く.

assert_not_equal_ro.rb
require 'colorize'

def assert_not_equal(expected, result)
  if expected != result
    puts "expected :: #{expected}"
    puts "result   :: #{result}"
    print "succeeded in #{__method__}.\n".green
  else
    puts "expected :: #{expected}"
    puts "result   :: #{result}"
    print "failed in #{__method__}.\n".red
  end
end

assert_not_equal(1,2)
assert_not_equal(1,1)

これで,

$ ruby assert_not_equal_ro.rb
expected :: 1
result   :: 2
succeeded in assert_not_equal.
expected :: 1
result   :: 1
failed in assert_not_equal.

関数の整理

今のままでは重複した処理がある.

  • puts "expected: #{expected}"
  • puts "result: #{result}"

これを別の関数にして,整理する.

aero.rb
require 'colorize'

def puts_value(expected, result)
  puts "expected :: #{expected}"
  puts "result   :: #{result}"
end

def assert_equal(expected, result)
  puts_value(expected, result)
  if expected == result
    print "succeeded in #{__method__}.\n".green
  else
    print "failed in #{__method__}.\n".red
  end
end

def assert_not_equal(expected, result)
  puts_value(expected, result)
  if expected != result
    print "succeeded in #{__method__}.\n".green
  else
    print "failed in #{__method__}.\n".red
  end
end


assert_equal(1,1)
assert_equal(1,2)
assert_not_equal(1,2)
assert_not_equal(1,1)

これで,

$ ruby aero.rb
expected :: 1
result   :: 1
succeeded in assert_equal.
expected :: 1
result   :: 2
failed in assert_equal.
expected :: 1
result   :: 2
succeeded in assert_not_equal.
expected :: 1
result   :: 1
failed in assert_not_equal.

case

caseの存在を忘れてた.

aeroc.rb
require 'colorize'

def puts_vals(expected, result)
  puts "expected :: #{expected}"
  puts "result   :: #{result}"
end

def assert_equal(expected, result)
  puts_vals(expected, result)
  print  case expected == result
     when true; "succeeded in #{__method__}.\n".green
     when false; "failed in #{__method__}.\n".red
     end
end

def assert_not_equal(expected, result)
  puts_vals(expected, result)
  print case expected != result
     when true; "succeeded in #{__method__}.\n".green
     when false; "failed in #{__method__}.\n".red
    end
end

assert_equal(1, 1)
assert_equal(1, 2)
assert_not_equal(1, 2)
assert_not_equal(1, 1)

これで完成ですわ.

と思って講義資料を見てみると,caseの部分,

print expected != result ?
  "succeeded in #{__method__}.\n".green :
    "failed in #{__method__}.\n".red

っていう書き方もあるみたい.

さらに,重複実行の予防のidiomとして,

if $PROGRAM_NAME == __FILE__
  assert_equal(1, 1)
  assert_equal(1, 2)
  assert_not_equal(1, 2)
  assert_not_equal(1, 1)
end

codeが書いてあるファイル名(FILE)が,動いているファイル名($PROGRAM_NAME)と一致したら中身を実行とある.

どういう意味なんだ...(分かる方,コメントお願いします.)

Rubular

正規表現

文字情報を取り出す便利ツールな正規表現(regular expression)

Rubularとやらを使って勉強する.

Regex quick referenceが載ってるので,自分でいろいろ試してみよう.

気が向いたら,講義資料読んで解説するね.

締め

今回は関数と正規表現について学んだ.

授業外の内容も書いてるから書く量が多いわ...

次回,執筆時には知らない.


  • source ~/school/multi/my_ruby/grad_members_20f/members/evendemiaire/post/assert.org
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む