nilはクラスであり、メソッドの使用ができる
sum関数でエラーがでた
下記のメソッドを使いたいとします。
def calc_score [@first_shot.point, @second_shot.point, @third_shot.point].sum end
インスタンスはこうやって作られます。
def initialize(first_shot, second_shot = nil, third_shot = nil) @first_shot = Shot.new(first_shot) @second_shot = Shot.new(second_shot) @third_shot = Shot.new(third_shot) end
インスタンスのShotクラスの実装はこうです。
class Shot attr_reader :point def initialize(pinfall) @point = pinfall end end
この実装だと、
in `+': nil can't be coerced into Integer (TypeError)
と怒られてしまいます。
nilにもメソッドがある
まず結論から。
上記のエラーを無くす方法は、
class Shot attr_reader :point def initialize(pinfall) @point = pinfall.to_i end end
to_iのメソッドを使用します。
今回sumメソッドのエラーの原因は、
(TypeError)です。
そのままの意味で、nilとの型の不一致です。
Rubyはすべてがインスタンス扱いです。ゆえに
nilクラスがあり、メソッドもあります。
to_iを行い0を返すことで、エラー解消となります。
ちなみに、
def initialize(first_shot, second_shot = nil, third_shot = nil)
ここで初期値設定していますが、もとはArgumentError: wrong number of arguments (given 2, expected 3)対策でした。
ですがその後の実装によって、
・そもそも設定せずともエラーがでない
・例えば、0にしても結局nilになって返ってくる
などあり、こちらについてはもう少し調査したいと思います。
以上です。
テストでタイムゾーンを考慮した
テストのプラクティスにて
Date型への変換メソッドのテストを行いました。
テストの観点
対象のメソッドは下記です。
app/models/report.rb
class Report < ApplicationRecord belongs_to :user has_many :comments, as: :commentable, dependent: :destroy validates :title, presence: true validates :content, presence: true def created_on created_at.to_date end
Reportモデルのメソッドで、型変換を行っています。
irb(main):014:0> Report.columns_hash['created_at'].type => :datetime # これがclass Date に変換されていたらOK
注意点!
まず自分は型チェックのみの実装を行いました。
#型チェック assert_not_equal Date, report.created_at.class assert_equal Date, report.created_on.class
テストの方針にもよるでしょうが、
reportの期待値が一致するか?
つまり、インスタンス生成時のcreated_atもチェックしてみるべきなのです。
そして初期の実装がこちら。
test '#created_on' do me = User.create(email: 'me@example.com', password: 'password') report = Report.create(user_id: me.id, title: 'タイトル', content: '内容') #型チェック assert_not_equal Date, report.created_at.class assert_equal Date, report.created_on.class #期待値のチェック assert_not_equal Date.today, report.created_at assert_equal Date.today, report.created_on
実はこちらバグになりうる問題点があります。
何か分かりますか?
Date.todayしたときの注意点
Date.todayを行うと普通に今日が取得できると思いますよね?
でもこれってどこを参照してどうやってできるのか?まったく把握していませんでした。
指摘をもらったのがこのタイムゾーンについてです。
フィヨルドでも大変お世話になっている、伊藤さんのめちゃくちゃ分かりやすい記事↓
抜粋:
# todayは環境変数のタイムゾーンを使う。(ただし、タイムゾーン情報は保持されない) l Date.today => 2015/01/01, Date # currentはapplication.rbのタイムゾーンを使う。 l Date.current => 2014/12/31, Date # Time.zone.todayはDate.current と同じ l Time.zone.today => 2014/12/31, Date
環境変数のタイムゾーンを使うか、またはapplication.rbのタイムゾーンを使うのか。
ここを知っておかなければ、テストは通ったのに本番環境でバグが起こるなんてことも??
まとめ
特に理由がなければ Time(RailsならTimeWithZone)を使う < 完全な受け売り
ただ今回のテスト対象のメソッドって、Dateに型変換するメソッドなので、私は分かりやすい様に、
Date.currentにて実装しました。
★Date.todayは使わない!
以上です。
destoroyとnullfityについて(ポリモーフィックあり)
ユーザーと日報とコメントと・・・
Aというユーザー(User)がいます。
日報(Report)は、N個書けます。
そしてその日報には、N個コメント(Comment)が書けます。
というシステムを作る時に、
じゃあユーザー削除したときや日報削除したときの関連したデータの扱いをどうしましょう?といった状況が起きます。
ひとつの例
では、ユーザーを削除したとき。
①そのユーザーが持っている日報は削除します。
②その削除された日報に、書かれていたコメントも削除します。
③ただし、削除されたユーザーが他のユーザーの日報にコメントしていたものは残します。
という運用にしたいと思います。
コメントについて
コメントはポリモーフィックにて実装。
また③の場合、ビュー側ではユーザー名が取得できないときは、「不明なユーザー」など表示できる記述にする必要があります。
User、Report、Commentの各関連性を書きます。
正解の書き方。
ネタバレ対策のためだいぶ端折って書くので、恐縮ですが、、
class Report < ApplicationRecord belongs_to :user has_many :comments, as: :commentable, dependent: :destroy end
class Comment < ApplicationRecord belongs_to :commentable, polymorphic: true belongs_to :user end
class User < ApplicationRecord has_many :reports, dependent: :destroy has_many :comments, dependent: :nullify
ここでの注目は、destroyとnullifyです。
ユーザーでnullifyを使用しているのは、ユーザー削除時にもコメントを残す仕様のためです。
DB側の設定、schemaは
"reports" t.integer "user_id", null: false "comments" t.integer "user_id" t.string "commentable_type", null: false t.integer "commentable_id", null: false add_foreign_key "comments", "users" add_foreign_key "reports", "users"
null: falseの設定も確認します。※このあとのアンチパターンにて重要になります。
コメントは、commentable_typeとcommentable_idがnull: false
※ポリモーフィックになっていますが、詳細は割愛。
(元ソースのコメントが、日報と本とにコメントするという仕様なための実装です。)
流れ:
User削除→ has_many :reports, dependent: :destroy なので親が削除で子も削除。
上で日報が削除されると has_many :comments, as: :commentable, dependent: :destroy なので親が削除で子も削除。
よって、ユーザーを削除すると書いた日報とそこにあるコメントも削除される。
ミスったこと
これまでを踏まえて、
class Report < ApplicationRecord has_many :comments, as: :commentable, dependent: :nullify
この変更を行うとどうなるでしょうか?
アカウント作成(userA) -> userAで日報作成 -> その日報にコメント -> そのままこのuserAをアカウント削除
この操作を行うとエラーになります。
流れ
irb(main):002:0> User.find(1) (0.7ms) SELECT sqlite_version(*) User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] => #<User id: 1, email: "sample-0@example.com", created_at: "2022-04-21 22:15:28.102283000 +0900", updated_at: "2022-04-21 22:15:28.102283000 +0900", name: "新井 太一", postal_code: "123-0000", address: "Apt. 956 12893 愛, 佐野村, 41 304-0049", self_introduction: "こんにちは、新井 太一です。"> User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] Report Load (0.1ms) SELECT "reports".* FROM "reports" WHERE "reports"."user_id" = ? [["user_id", 1]] => [#<Report:0x00007f94838e4bc0 id: 56, title: "user_id: 1 の日報", content: "", created_at: Thu, 21 Apr 2022 22:18:02.649311000 JST +09:00, updated_at: Thu, 21 Apr 2022 22:18:02.649311000 JST +09:00, user_id: 1>] irb(main):005:0> User.find(1).reports.first.comments User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] Report Load (0.2ms) SELECT "reports".* FROM "reports" WHERE "reports"."user_id" = ? ORDER BY "reports"."id" ASC LIMIT ? [["user_id", 1], ["LIMIT", 1]] Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."commentable_id" = ? AND "comments"."commentable_type" = ? [["commentable_id", 56], ["commentable_type", "Report"]] => [#<Comment:0x00007f9483d244b0 id: 1, body: "この日報が消えてもコメントは残る想定", commentable_type: "Report", commentable_id: 56, user_id: 2, created_at: Thu, 21 Apr 2022 22:18:37.814100000 JST +09:00, updated_at: Thu, 21 Apr 2022 22:18:37.814100000 JST +09:00>] # ユーザid: 1 の日報にコメントがついていることを確認 irb(main):006:0> User.find(1).destroy! User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] TRANSACTION (0.1ms) begin transaction # ... # 実際に更新対象となる comment が存在するので、更新が走ってエラーになる Report Load (0.1ms) SELECT "reports".* FROM "reports" WHERE "reports"."user_id" = ? [["user_id", 1]] Comment Update All (1.0ms) UPDATE "comments" SET "commentable_id" = ?, "commentable_type" = ? WHERE "comments"."commentable_id" = ? AND "comments"."commentable_type" = ? [["commentable_id", nil], ["commentable_type", nil], ["commentable_id", 56], ["commentable_type", "Report"]] TRANSACTION (0.6ms) rollback transaction /Users/k.fukai/.anyenv/envs/rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/sqlite3-1.4.2/lib/sqlite3/statement.rb:108:in `step': SQLite3::ConstraintException: NOT NULL constraint failed: comments.commentable_type (ActiveRecord::NotNullViolation) /Users/k.fukai/.anyenv/envs/rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/sqlite3-1.4.2/lib/sqlite3/statement.rb:108:in `step': NOT NULL constraint failed: comments.commentable_type (SQLite3::ConstraintException)
アカウント削除→日報削除される→付随したコメントを削除しようとする。
その commentable_type や commentable_id を NULL に更新しようとする。
DB(schema)は、NULL許容していない。
"comments" t.integer "user_id" t.string "commentable_type", null: false t.integer "commentable_id", null: false
なのでエラーとなります。
以下、頂いた指摘。
①現状のポリモーフィック関連の実装では commentable_type, commentable_id は NULL 禁止にしていますよね。これは安全で、基本的に良い実装だと思います。
②Rails の関連付けでは、あるレコードが削除されたときの関連オブジェクトの処理を dependent オプションを用いて制御できますよね。
例えば :destroy を指定すると、親のレコードが削除されたら子のレコードも削除されます。
一方、:nullify にすると、親が削除されると子のレコードは ***_type, ***_id を NULL にして親との関連を切りつつ、レコード自体は残ったままになります。
現状の実装は後者なわけですが、これが「commentable_type, commentable_id は NULL 禁止」という DB スキーマとマッチしていないのが事の原因です。commentable_type, commentable_id を NULL 禁止にしていると、今回のように「レコードは残しつつ、親との関連を示す情報は NULL にする」というのはできません。
言い換えると、それでも「親が消えても子のレコード自体は残す」という仕様にしたいなら NULL 許容にしないと成り立たないというわけです。(※)
まとめ
実装前には、しっかりとモデルの関連性とメソッドが行う動作を確認する必要があるという話でした。
DBのNULL許容、各モデルの関係などを理解しないとポリモーフィックで余計に混乱します。
ここでの実装の指針は、DBでは基本的にNULL禁止で行っているので生じたエラーでした。
以上です。
ユーザーの情報をログインユーザーにする書き方
状況:
ログインしているユーザーにて、日報(report)を作成しようとするとき。
日報を新規作成時のControllerの記述について、書き方のメモ。
User と Report は1 : Nの関係性。
コード:
app/controllers/reports_controller.rb
①これと、
def create @report = Report.new(report_params) @report.user = current_user
②これは
def create @report = current_user.reports.new(report_params)
同じことを行っている。
②のreportsとは、
app/models/user.rb
class User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable has_many :reports, dependent: :destroy
ちなみにhas_manyなので、reportsと複数形で書くこと。
current_userからそのまま、reportsメソッドが使用できて、newすることができちゃう。
Report.new(report_params) → reports.new(report_params)※current_userに紐付けられる。
NULL禁止制約を後付けする際の注意点
データメンテについて
すでにあるカラムをNULL禁止にしようとする際の注意点を教えて頂いたので、以下にまとめます。
参考コード1
まず最初に記述したコード
class ChangeColumnToAllowNull < ActiveRecord::Migration[6.1] def up change_column :comments, :user_id, :integer, null: true end def down change_column :comments, :user_id, :integer, null: false end end
null: falseに変更しようとしています。
以下指摘
down のときのようにあとから NULL 禁止制約をつける場合、対象カラムが NULL であるレコードがすでに存在しているとマイグレーション適用時にエラーになってしまいます。
そして、本番運用しているサービスだと大抵の場合はそういうデータが存在しているものなので、制約をつける前に、何かしらのデータを入れる必要があります(これをデータメンテと呼んだりします)。今回の場合はこんな感じになります。def down # id: 1 のユーザーに無理やり関連付ける execute "UPDATE comments SET user_id = 1 WHERE user_id IS NULL" change_column :comments, :user_id, :integer, null: false end例として id: 1 のユーザーに無理やり関連付けていますが、これはあくまで一例です。
すでに運用しているサービスの場合には、チームで話し合ってもうちょっと現実的な手段を取ります。
ご指摘の通り本番サービスを意識すると、こういったそもそものデータ基盤自体の変更って気軽にできませんので、慎重に検討すべき事由です。
参考コード2
またカラムの変更方法は、下記の様にも書けます。
変更したコードが下記。
だいぶスッキリ!
class ChangeColumnToAllowNull < ActiveRecord::Migration[6.1] def change change_column_null(:comments, :user_id, true) end end
そして下記が、さらに本番を意識した記述方法。
class ChangeColumnToAllowNull < ActiveRecord::Migration[6.1] def change # 実装当時のチーム協議の結果、ロールバック時は、退会済みユーザのコメントはダミーのユーザアカウントを用意してそれに関連づけてしまう # 協議ログ: https://hogehoge-log.com/xxxxxx change_column_null(:comments, :user_id, true, 2) end end
詳細
change_column_null は、NULL 許容 -> 禁止 に変更する場合、NULL になっているカラムを何の値で埋めるかをオプションで受け付けてくれます。
なのでそれを使う感じですね〜
あと、後にこのマイグレーションファイルを見た人が「なぜ2で埋めるのか」がわかるように詳しくコメントを書いています(これはプラクティスなので良いですが、実際運用されているアプリケーションでは、時間が経ってコンテキストが失われても読めばわかる状態にすることが本当に重要です)。
この書き方だと、楽に書けるのでいいですね。
そしてちゃんと起こりうるエラーケースへの対策も想定した書き方もできます。
うーんやっぱりRailsは知るほど楽しい。
NULL禁止制約を後付けする際の注意点
データメンテについて
すでにあるカラムをNULL禁止にしようとする際の注意点を教えて頂いたので、以下にまとめます。
参考コード1
まず最初に記述したコード
class ChangeColumnToAllowNull < ActiveRecord::Migration[6.1] def up change_column :comments, :user_id, :integer, null: true end def down change_column :comments, :user_id, :integer, null: false end end
null: falseに変更しようとしています。
以下指摘
down のときのようにあとから NULL 禁止制約をつける場合、対象カラムが NULL であるレコードがすでに存在しているとマイグレーション適用時にエラーになってしまいます。
そして、本番運用しているサービスだと大抵の場合はそういうデータが存在しているものなので、制約をつける前に、何かしらのデータを入れる必要があります(これをデータメンテと呼んだりします)。今回の場合はこんな感じになります。def down
# id: 1 のユーザーに無理やり関連付ける
execute "UPDATE comments SET user_id = 1 WHERE user_id IS NULL"
change_column :comments, :user_id, :integer, null: false
end
例として id: 1 のユーザーに無理やり関連付けていますが、これはあくまで一例です。
すでに運用しているサービスの場合には、チームで話し合ってもうちょっと現実的な手段を取ります。
ご指摘の通り本番サービスを意識すると、こういったそもそものデータ基盤自体の変更って気軽にできませんので、慎重に検討すべき事由です。
参考コード2
またカラムの変更方法は、下記の様にも書けます。
変更したコードが下記。
だいぶスッキリ!
class ChangeColumnToAllowNull < ActiveRecord::Migration[6.1] def change change_column_null(:comments, :user_id, true) end end
class ChangeColumnToAllowNull < ActiveRecord::Migration[6.1] def change # 実装当時のチーム協議の結果、ロールバック時は、退会済みユーザのコメントはダミーのユーザアカウントを用意してそれに関連づけてしまう # 協議ログ: https://hogehoge-log.com/xxxxxx change_column_null(:comments, :user_id, true, 2) end end
フィヨルドブートキャンプの取り組み方とアウトプットについて振り返る
はじめに
この記事は、 フィヨルドブートキャンプ Part 2 Advent Calendar 2021 - Adventar 5日目の記事です。
フィヨルドブートキャンプの学習
現在フィヨルドブートキャンプにて学習中です。 ※以下、FBC(フィヨルドブートキャンプ)と表記。 学習はプラクティスごとに大まかに別れており、課題を提出してメンターの方に添削してもらいます。 基本的な流れは、プラクティスごとに全く知識のない状態から、必要な知識をインプットして課題を提出して合格を目指していきます。
振り返って
3月頃から開始しまして、途中一定期間休会を挟んだものの、半年以上を終えてざっと振り返りと反省の記事です。
プラクティス
現在Railsのプラクティスに取り組んでいます。 正直まだまだRails何もわからない・・・状態ですが、気を引き締め直して取り組んでいきます。 進捗に関しては遅いほうではあるのですが、後述するLTでも触れているように、焦って取り組んでしまわないように、 楽しんで!苦しんで!取り組んでいきたいなという心持ちです。
タスク分解
タスク分解の重要性を認識しています。 ただ最初の何も分からない・・・状態で、それを行うのがかなり脳みそへの負荷が高いので、 QAと日報のありがたみを感じでいます。 というか無かったら、絶対合格できなかっただろうなと。かなり苦手です。 仮に知っている知識で事足りると、タスク分解を行わず、わーっとやりたがりがちです。 今は規模も規模なので、それで事足りますが、 実務に入った際には、一度こうやりますね?を上司に通したほうが良いのは、自明の事実なので(特に初めのうちは)、 タスク分解の負荷に慣れたいなというところです。
模範となる人
合格後も、他の方のコーディングを参考にできたり、Rubyの第一人者の伊藤さんのコーディング様子を見れたりできるので、個人的にこれが一番最高です! Gitのコミットメッセージなんか、Qiitaにわざわざ書かれてないので、 実際の様子をみて、へーそう書くのかー。このタイミングかー。と勉強になります。 コーディングに関しては、やはりレベル高いので、自分の中で消化するまで結構時間かかります。 というか同じのは書けないなあ。
アウトプットについて
知識の定着のためには、アウトプットも行ったほうがいいですよね。 ですが、私はとても苦手です。 また、FBCには優秀なメンター・生徒さんが多く、 例えばQiitaなんかで絶対お世話になる伊藤さんなど、良質なアウトプットを量産される方が多い印象です。 ちょっと何か書くにしても億劫になってしまいます。 ですが試行錯誤しながら、 アウトプット0、できない、苦手から、 アウトプット大好き、誰かのためになる記事を作れる を目指して改善を行っていきたいです。
行ったアウトプットの方法
・ブログ ・日報(FBCの機能) ・LT
ブログ
ぼちぼち投稿しています。 ただ現在タイトルどおり、メモ書きの意味合いが強いです。 だいたいの流れとして、 エラー出る。ググる。試す。エラー継続なら、再度ググる。試す。成功。 この過程をしっかりメモしながら行い、エラー対処からのエラーが出た際にも対応できるようにしつつ、成功までの過程をアウトプットできるよう努めています。 特に誰かに質問する際に、メモがないとエラーの再現ができないなど、回答者も困るため徹底していきたいです。 けっこうこれだけでも、ネットに載ってるやつの脳死コピペを防げる気もします。 (これが起きたから、これを実行しよう、と認識できる。)
もっと気軽にブログにアウトプットに取り組みたい
ちなみにこう書くと、下記の伊藤さんの記事が突き刺さってきます。
悩ましいところです。。 ひとまずは現在の様に、プラクティス中の ・エラーが出て、解消に至るまでの過程と思考 ・バージョン違いによるエラーなどのアップデート版としての記事 ・過去の自分に教えてあげるような記事 を意識していきたいと思います。
日報
日報に関しては、とても勉強になります。 参考サイトや書籍にて、自分でインプットした知識と他の人との解釈の違いを照らし合わせたりできるからです。 また日報のコメント欄は、メンターの方からの ・より深堀りした情報 ・併せて知っておくとためになる情報 などが多く、これも面白く拝見させてもらっています。
FBCはそれだけでなく、メンターも知らない情報を知っている・すぐに調べて教えてくれる生徒さんが多く、これは他のコミュニティにはない驚くような事かと思います。 人のアウトプット(おおよそ正しい情報)にこれだけ触れられるのは、学習者として大変ありがたいです。
LTに登壇した
※アウトプットかと言われるとなんともですが、自分の考え・気持ちをアウトプットしたと解釈してください。
絶対に自分は、挑戦しないだろうと思っていたLTに登壇しました。 これもFBCの生徒さんの熱気やコミュニティの雰囲気にいい意味で当てられたのかもしれません。 終わったその日に振り返りの記事を書くべきでしたが、、、 これも今後のアウトプット改善に繋げたいです。 近いうち記事書きます!
処女作:休む、
LTに関しては、暫くないかなあーと。思っています。(陰キャなので) それよりは、新しい分野に挑戦したいです。
最後に
まだまだ卒業まで長いですし、もっと早く卒業されている方も多くいるのは事実です。 ただ信頼できるプロからレビューをもらって、(動画でも頂いたときは、めちゃ緊張したけど最高!)、 あーでもないこーでもないと悩むの楽しいです。 卒業制作用のネタとして、作成挑戦してみたいサービスも頭にポコポコ浮かんでます。 (みなさん凄まじいサービスを作り上げてるので恐縮するのですが。)
ちょうど1日前担当の、ima1zumiさんの記事も面白くリンクを載せたいと思います。
こういった卒業の先はまだ考えるのは早いのですが、 自分が行動を起こした源泉も知っておくと、 今みたいに寒さのせいにして、モチベが下がっている(事から目を背けている)自分へのカンフル剤となり得るかもしれませんね。
余談 内容が無いようという記事ですが、言い訳をすると熱が出て体調崩したり、ほんとになんやかんやあって急遽書き上げました。 ですがこういったときにも、アウトプット慣れしているとすらすらと書けるはずなんですよね。 精進します。 以上、誕生日記念のアウトプット記事でした。