- 投稿日:2020-11-21T22:33:35+09:00
railsのメソッド"save"がどう定義されているのか読解してみた
rails読解シリーズ第5回目はsaveメソッド。
save# File activerecord/lib/active_record/base.rb, line 2575 def save create_or_update endcreate_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 end2行目で、対象のインスタンスが読み取り専用かどうか確認するようです。
メソッドの名前からして、readonly属性はboolean型で定義して、readonly: trueとすることで、利用することができそうです。
プログラマーが自ら定義することもできますが、railsのことだし、メソッドによって定義する方法もありそう。readonly?# File activerecord/lib/active_record/base.rb, line 628 def readonly? @readonly end読み取り専用のインスタンスであった場合はReadOnlyRecordエラーが出力されます
ReadOnlyRecordモデルclass ReadOnlyRecord < ActiveRecordError end3行目result = new_record? ? create : update3項演算子の条件は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 : update3項演算子に戻ります。
新しいレコードであればcreateメソッド、そうでなければupdateメソッドが呼び出されます。
ここはシンプルですね。4行目result != false3行目で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) endcreate_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メソッドの動き
- 読み取り専用か確認し、必要に応じてエラーを吐く
- 新規データかどうかによってcreateまたはupdateメソッドを呼び出す。
- 結果に応じてtrueまたはfalseを返す
save!メソッドの動き
saveメソッドの中で上記1~3を実行するメソッドが3にてfalseを返した時、RecordNotSavedエラーを吐く
感想
- railsの学習を始めた頃のイメージはcreateはnew+saveだよ〜とおぼえていたので、saveの中でcreateメソッドが呼ばれていたのは驚きだった
- create/updateメソッドとエラー周りの仕組みに関心をもったので読んでみたい。
- めちゃくちゃ重要な機能をシンプルで短いコードで実現していて美しい!! 特に4行目。
今回もマニアックな記事を読んでいただきありがとうございました。
またお会いしましょう。
- 投稿日:2020-11-21T22:07:00+09:00
【基礎】RubyのREPLとは【Cloud9】
RubyのREPLとは
REPL = Read eval print loop
を略したもので、対話型のRuby実行環境。Rubyのメソッドを試したりできる。
ターミナルで[$ irb]を実行すると開始。
puts "hello"
hello
=>nil[exit]で終了できる。
- 投稿日:2020-11-21T21:10:03+09:00
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.ymlfixturesフォルダの中の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無事解決!
まずは落ち着いてエラーメッセージを読んでいくことで、
比較的スムーズな解決につながりましたー!エラーで困っている初学者の方にとって
少しでも助けになれば嬉しいです。
- 投稿日:2020-11-21T19:31:26+09:00
railsのgem deviseのログインページを開くコントローラを読解してみた
rails読解シリーズ、第4回はdeviseのコントローラを読解してみました。
いつも深入りしすぎて、全体像が見えづらくなっているので今回はあっさりめに読んでみました。ログインページを開くための、session#newアクションを見ていきます
session#newdef new self.resource = resource_class.new(sign_in_params) clean_up_passwords(resource) yield resource if block_given? respond_with(resource, serialize_options(resource)) end2行目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モデルの情報が渡されているのか追いきれませんでした。
- 投稿日:2020-11-21T19:20:25+09:00
railsで"Model.new"が呼ばれるとどうなるか読解してみた
railsメソッドがどのように定義されているのか学んでいく企画第3弾はモデルからオブジェクトを生成するnewメソッド(Rails APIリンク)です。
実際にはnewメソッドはRubyで定義されています。
ので、正確にはタイトルの通り、railsのActiveModelによって定義されるモデルのインスタンスをrubyで定義されているnewメソッドが呼び出して生成したときに何が起こるか、について見ていきます。newclass 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 endnewメソッドは表面的には4行しかありませんが、裏に何行ものコードが隠れていますね。
1行目def respond_to?(method, include_private_methods = false)methodには先程述べた通り、:stringify_keysというシンボルが渡されています。
第2引数には何も渡されていないので、定義通りfalseが代入されます。2行目if superif文の中に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 end2行目の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? end4行目に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? end11行目のsuperメソッドのところをここまでで読みました。
現状は謎のmulti_parameter_attributesとモデルをネストしたときのためのnested_parameter_attributesに値が代入された上で、newによって生成したいインスタンスの各属性に対して
値が代入された状態です。12行目のメソッドについても見てみましょう
assign_nested_parameter_attributesdef 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メソッドでレコードが保存される仕組み、が気になりますね。
今後の課題としましょう。
- 投稿日:2020-11-21T18:47:03+09:00
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参考
- 投稿日:2020-11-21T17:52:26+09:00
【Rails・devise】複数モデルを管理する際の<<before_action>>
はじめに
deviseを用いて一般の方とグループ(企業や団体等)で分けてログインを実装する上で、
ログインした人のみが閲覧できるbefore_actionの実装に手間取ってしまったので記録します!結果から
applicationコントローラーで
・グループの方がログインした場合 → authenticate_any!が有効
・それ以外の人がログインした場合 → authenticate_user!が有効application_controller.rbclass 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.rbbefore_action :authenticate_any!無事、ログインについてのRspecも通りました!
ハマったこと
userまたはgroupがログインしている場合のみページにアクセスができるように実装したかったので、
左から順に評価し、最初に真になったものを返してくれる||を使用してみました。posts_controller.rbbefore_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を使用していましたが、勉強になりました!(かなり初歩的なのかとは思いますが笑)
同じようなことに困っている方のお役に立てたら嬉しいです。
- 投稿日:2020-11-21T17:04:46+09:00
「もっと見る」で非同期(Ajax)ページネーションを実装する
はじめに
やりたいこと
「もっと見る」を押すことで次の記事が出るようにしたい
やってみて
JavaScriptが苦手な僕でも、簡単に実装することができました。
今回はjQueryで実装しています。実装画面
実装例
参考
kaminari徹底入門
t-taira blog > ailsで「もっと見る」の実装※情報のソースが古く、かつ私のGemの理解及びjQueryの理解が乏しいため、適切ではない表現が含まれているかもしれません。
手順
Gem kaminari インストール
Gemfilegem 'kaminari'Controllerにメソッド追加
controllers/items/controller.rbclass ItemsController < ApplicationController def index @items = Item.all.order('created_at DESC').page(params[:page]) end endkaminariをインストールすると
.page(params[:page])
こちらの記述でページネーションを組むことができます。1ページ毎の取得件数を指定
Controllerに指定する場合
controllers/items/controller.rbclass ItemsController < ApplicationController def index @items = Item.order('created_at DESC').page(params[:page]).per(10) end endModelに指定する場合
models/item.rbclass Item < ApplicationRecord paginates_per 10 endView
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メソッド)の記述に誤りがある
のどちらかが疑わしいと思います。
私はファイル名の誤記というシンプルなミスで無駄な時間を消費しました。
お気をつけください。
✔︎
- 投稿日:2020-11-21T17:04:46+09:00
「もっと見る」でページネーションを非同期(Ajax)で実装する
はじめに
やりたいこと
「もっと見る」を押すことで次の記事が出るようにしたい
やってみて
JavaScriptが苦手な僕でも、簡単に実装することができました。
今回はjQueryで実装しています。実装画面
実装例
参考
kaminari徹底入門
t-taira blog > ailsで「もっと見る」の実装※情報のソースが古く、かつ私のGemの理解及びjQueryの理解が乏しいため、適切ではない表現が含まれているかもしれません。
手順
Gem kaminari インストール
Gemfilegem 'kaminari'Controllerにメソッド追加
controllers/items/controller.rbclass ItemsController < ApplicationController def index @items = Item.all.order('created_at DESC').page(params[:page]) end endkaminariをインストールすると
.page(params[:page])
こちらの記述でページネーションを組むことができます。1ページ毎の取得件数を指定
Controllerに指定する場合
controllers/items/controller.rbclass ItemsController < ApplicationController def index @items = Item.order('created_at DESC').page(params[:page]).per(10) end endModelに指定する場合
models/item.rbclass Item < ApplicationRecord paginates_per 10 endView
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メソッド)の記述に誤りがある
のどちらかが疑わしいと思います。
私はファイル名の誤記というシンプルなミスで無駄な時間を消費しました。
お気をつけください。
✔︎
- 投稿日:2020-11-21T17:04:46+09:00
「もっと見る」で非同期(Ajax)ページネーションする方法
はじめに
やりたいこと
「もっと見る」を押すことで次の記事が出るようにしたい
やってみて
JavaScriptが苦手な僕でも、簡単に実装することができました。
今回はjQueryで実装しています。実装画面
実装例
参考
kaminari徹底入門
t-taira blog > ailsで「もっと見る」の実装※情報のソースが古く、かつ私のGemの理解及びjQueryの理解が乏しいため、適切ではない表現が含まれているかもしれません。
手順
Gem kaminari インストール
Gemfilegem 'kaminari'Controllerにメソッド追加
controllers/items/controller.rbclass ItemsController < ApplicationController def index @items = Item.all.order('created_at DESC').page(params[:page]) end endkaminariをインストールすると
.page(params[:page])
こちらの記述でページネーションを組むことができます。1ページ毎の取得件数を指定
Controllerに指定する場合
controllers/items/controller.rbclass ItemsController < ApplicationController def index @items = Item.order('created_at DESC').page(params[:page]).per(10) end endModelに指定する場合
models/item.rbclass Item < ApplicationRecord paginates_per 10 endView
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メソッド)の記述に誤りがある
のどちらかが疑わしいと思います。
私はファイル名の誤記というシンプルなミスで無駄な時間を消費しました。
お気をつけください。
✔︎
- 投稿日:2020-11-21T15:35:46+09:00
Railsを使ったToDoリストの作成(8.ログイン機能の実装)
概要
本記事は、初学者がRailsを使ってToDoリストを作成する過程を記したものです。
私と同じく初学者の方で、Railsのアウトプット段階でつまづいている方に向けて基礎の基礎を押さえた解説をしております。
抜け漏れや説明不足など多々あるとは思いますが、読んでくださった方にとって少しでも役に立つ記事であれば幸いです。環境
Homebrew: 2.5.10
-> MacOSのパッケージ管理ツールruby: 2.6.5p114
-> RubyRails: 6.0.3.4
-> Railsnode: 14.3.0
-> Node.jsyarn: 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を読み込みます。Gemfilegem '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.rbconfig.action_mailer.default_url_options = { host: 'localhost', port: 3000 }②
config/routes.rb
にroot_urlを設定します。routes.rbRails.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
でサーバを立ち上げ直しましょう。ここまできたら以下のようなページ等が作成できているはずです。
2 ビュー
deviseの設定が終わったらビューの設定をしていきます。
deviseは初期設定がerbなので、まずはhamlに書き換えます。既存のerbファイルをhamlに置き換えるためにはターミナルにて以下のコマンドを実行します。
iterm$ bundle exec rake haml:replace_erbsまた、デフォルトではエラーメッセージなどが英語のままなので日本語化します。
日本語化するためにはconfig/locals
にdevise.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.rbbefore_action :authenticate_user!, only: [:new, :create, :edit, :update, :destroy]
authenticate_user!
はdeviseが用意してくれているメソッドでログインしているユーザのみ操作を許可し、ログインしていない場合はログイン画面に遷移するようにしてくれます。
beeforeアクションを使うことで各アクションの前にログイン状況を判断できるようにします。
しかし、Read機能はログインしていなくてもできるようにしたいので、onlyオプションで対象を絞っています。
- 投稿日:2020-11-21T14:22:10+09:00
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]
- 投稿日:2020-11-21T14:19:12+09:00
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
- 投稿日:2020-11-21T13:52:20+09:00
【自分メモ】フレームワークについて
制作したいものに対して、これを使えば作りやすくなりますよ、的なものがフレームワーク。
Webアプリケーションを制作する時は、RubyonRailsというフレームワークを使います。
RubyonRailsにはWebアプリケーションを制作するための様々な機能がモリモリ入っているので、RubyonRails(の書き方)に従ってコードを書けば、自分で一から作成するよりも遥か簡単にWebアプリケーションを制作することができます。※自分の考えをメモとして残していますので、間違っていたり修正した方が良い点がございましたら、ドシドシつっこみいただけると嬉しいです!
- 投稿日:2020-11-21T13:48:57+09:00
ActiveSupport::Concernでクラスメソッドを定義して、さらにクラスメソッドを呼び出せるのか
こちらの記事を参照して、
https://qiita.com/h-shima/items/d772b4cbe7368ddb8255
modelにインクルードすることによりConcernでクラスメソッドを定義できることがわかったのですが、そのクラスメソッド内でさらにモデルに定義したクラスメソッドが呼び出せるか検証しました。
concern内でクラスメソッドを呼び出す
app/models/concerns/hogehoge.rbmodule 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.rbclass 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.rbclass 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
となることは確認しました。このあたりのメタプログラミング的な部分はよく分かっていないので、追記していきます。
- 投稿日:2020-11-21T13:44:56+09:00
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今回はまったのはテストコードの処理を正しく理解していなかったため、
前提条件を正しく定義できていなかったことが原因でした。
- 投稿日:2020-11-21T13:18:41+09:00
[Rails]ゲストログイン機能
はじめに
ポートフォリオにゲストログイン機能があった方が良いとのことだったので実装してみました。
目次
- 1. ルーティング
- 2. コントローラー
- 3. モデル
- 4. ビュー
1. ルーティング
routes.rbにゲストログイン用のアクションを設定します。
deviseのsessionsコントローラーに新しくメソッドを追加しています。config/routes.rbRails.application.routes.draw do devise_for :users devise_scope :user do post 'users/guest_sign_in', to: 'users/sessions#new_guest' end ~略~ end2. コントローラー
controllersの中にusersフォルダを作成しsessions_controller.rbファイルを作成。
new_guestメソッドをコントローラーに作成します。
guestメソッドはモデルに作成します。app/controllers/users/sessions_controller.rbclass Users::SessionsController < Devise::SessionsController def new_guest user = User.guest sign_in user # ユーザーをログインさせる redirect_to root_path, notice: 'ゲストユーザーとしてログインしました。' end end3. モデル
find_or_create_by!でゲストユーザーが無ければ作成、あれば取り出します。
あとはゲストユーザーがない時に作成するユーザー情報を記述しています。app/models/user.rbdef 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 ~略~ end4. ビュー
今回はヘッダーにゲストログイン用のボタンを作成しました。
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>参考リンク
- 投稿日:2020-11-21T13:18:41+09:00
ActionController::UrlGenerationError in CreditCard#new ~~pathをprefixで書いた時のエラー~~
はじめに
マイページへのリンク先をprefixで書いた時にエラーでつまずいたので記録として残します。
実装したこと
hamlの
= link_to
でurlを"/users/#{current_user.id}"
で
記載してマイページに遷移するようにしていました。
"/users/#{current_user.id}"
をprefixの記載に変更するために
rails routesで探しにいきました。
userとなっているのでuser_path
として記載しました。
すると下記のエラーが発生。
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で記載する習慣をつけて行こうと思います。
今回はエラー文を読み取って割と早期に解決することができましたので
今後も冷静にエラー文を分析して解決する能力を高めていこうと思います。
何か補足事項とうございましたら、是非コメントをお願いいたします。しょうま
- 投稿日:2020-11-21T12:37:35+09:00
【Rails】一意性制約のバリデーションメッセージを設定する
どんな問題?
例えばユーザー登録機能を実装する場合、emailは一意性制約(unique)をつけるのがふつうだよね
当然Userモデルのバリデーションにuniquenessを設定することになるけど、ひとつ困ったことが起こるんだこのままだと一意性制約に引っかかった場合、エラーメッセージは
「メールアドレスはすでに存在します」
と表示されちゃうよね(目暮警部) 「しかしそれがどうしたと言うのかね」
ふつうのユーザーなら特段問題にはならないと思うよ
でも、この機能を使えばあることが可能になるんじゃないかなそう、この機能によって
「すでに登録済みの有効なメールアドレスがあるか」
確かめらちゃうよね悪意あるユーザーにメールアドレスがバレてしまうのは良くないってことなんだ
どのように解決する?
解決方法はいたってシンプル
バリデーションエラーのメッセージを個別に設定するだけだ!user.rbvalidates :email, uniqueness: { message: 'そのアドレスは使用できません' }, confirmation: trueちなみに
confirmation: true
はメールアドレス変更時に確認用の入力をさせることを想定してつけてるオプションだから、この記事の本質とは直接関係ないよ
- 投稿日:2020-11-21T11:46:08+09:00
Railsを使ったToDoリストの作成(7.CRUDのDelete機能)
概要
本記事は、初学者がRailsを使ってToDoリストを作成する過程を記したものです。
私と同じく初学者の方で、Railsのアウトプット段階でつまづいている方に向けて基礎の基礎を押さえた解説をしております。
抜け漏れや説明不足など多々あるとは思いますが、読んでくださった方にとって少しでも役に立つ記事であれば幸いです。環境
Homebrew: 2.5.10
-> MacOSのパッケージ管理ツールruby: 2.6.5p114
-> RubyRails: 6.0.3.4
-> Railsnode: 14.3.0
-> Node.jsyarn: 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.rbdef 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に指定した値が画面上に表示されます。 以上で、削除機能の実装は完了です。
- 投稿日:2020-11-21T11:00:56+09:00
Railsを使ったToDoリストの作成(6.CRUDのUpdate機能)
概要
本記事は、初学者がRailsを使ってToDoリストを作成する過程を記したものです。
私と同じく初学者の方で、Railsのアウトプット段階でつまづいている方に向けて基礎の基礎を押さえた解説をしております。
抜け漏れや説明不足など多々あるとは思いますが、読んでくださった方にとって少しでも役に立つ記事であれば幸いです。環境
Homebrew: 2.5.10
-> MacOSのパッケージ管理ツールruby: 2.6.5p114
-> RubyRails: 6.0.3.4
-> Railsnode: 14.3.0
-> Node.jsyarn: 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.rbdef edit @board = Board.find(params[:id]) endeditアクションでは、登録済みのフォーム画面を予め表示できるように、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.rbdef 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 endBoard.findで更新したいboardを取得します。
ActiveRecordのupdateメソッドを実行し、編集したインスタンスをデータベース保存します。editアクションとupdateアクションの内容や関係性は、新規登録機能のnewアクションとcreateアクションに似ているため、Createの記事を参考にしてください。
以上で、編集機能の実装は完了です。
- 投稿日:2020-11-21T08:45:09+09:00
【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 endestate = { 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 といったメソッド名をつけるケースが多いようです。
これを知った私は、確かに業務上でこれらのメソッド名を見かける機会多く、実は普段から触れていたアプリケーションがコマンドパターンに倣っているケースが多かったのだと気づきました。
みなさんの携わっているアプリケーションでも、これらのメソッド名が使用されていた場合、もしかしたらコマンドパターンが適用されているかもしれません。
まとめ
コマンドパターンは特定の動作だけを担うオブジェクトを構築するパターンでした。
複雑なロジックのリファクタリングに有用なため、使いこなせるようになりたいです。
- 投稿日:2020-11-21T08:35:56+09:00
[Rails]hidden_fieldとhidden_field_tagの違いについて![初心者]
どういう時に使用するの?
form_with
やform_for
を利用して、ユーザーに何かを打ち込んでもらい、送信してもらいたい時に便利なメソッドです。例えば、AmazonのようなECサイトで、ショッピングカートの商品を購入するとき。
ユーザーからすると、「確定ボタン」だけ押したいのに、再度username
やaddress
を打たなければならないのは面倒ですよね。
また、パラメーターを経由したいけれども、ユーザー側にその情報を隠しておきたい時などにも使えます。使い方
hidden_fieldhidden_field :値の取得時に使用する名前(シンボル), :value => 実際に渡す値 #第一引数→name属性 #第二引数→value属性アクションで
hidden_field
で渡されたパラメータを受け取ることが出来ます。
ここで、controllerに記述する際に、フォームフィールドに紐づく値となっている為、記述方法は
params[:モデル名][:渡したname属性]
という形になるので、注意が必要です。hidden_field_taghidden_field_tag :渡したいパラメータの値, 実際に渡す値それぞれの使用タイミング
hidden_field
form_withやform_forで渡すインスタンスがある場合(もしくはそれらのヘルパーを使っている場合)。
hidden_field_tag
一個だけパラメータを他のアクションへ単体で渡したい時に、独立して使用。
- 投稿日:2020-11-21T04:48:02+09:00
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.rbrequire '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 falsetrueが赤色に,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.rbrequire '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.rbrequire '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.rbrequire '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.rbrequire '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) endcodeが書いてあるファイル名(FILE)が,動いているファイル名($PROGRAM_NAME)と一致したら中身を実行とある.
どういう意味なんだ...(分かる方,コメントお願いします.)
Rubular
正規表現
文字情報を取り出す便利ツールな正規表現(regular expression)
Rubularとやらを使って勉強する.
Regex quick referenceが載ってるので,自分でいろいろ試してみよう.
気が向いたら,講義資料読んで解説するね.
締め
今回は関数と正規表現について学んだ.
授業外の内容も書いてるから書く量が多いわ...
次回,執筆時には知らない.
- source ~/school/multi/my_ruby/grad_members_20f/members/evendemiaire/post/assert.org
- 投稿日:2020-11-21T00:06:24+09:00
ヘッダーが日本語の巨大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 end100万行で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: :integer
やencoding: '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参考