- 投稿日:2021-12-24T23:24:07+09:00
Dockerで実装しているテーブルの内容をターミナルで確認する方法(rails)
備忘録として Dockerを使ってrailsのアプリケーション実装をしている時に、 テーブルの更新内容を確認したくなったので調べた内容を記録しています。 方法 % docker-compose run web rails db 上記のコマンドを入力すると ターミナルに下記内容が表示されます psql (13.5 (Debian 13.5-0+deb11u1), server 14.1 (Debian 14.1-1.pgdg110+1)) WARNING: psql major version 13, server major version 14. Some psql features might not work. Type "help" for help. ○○_development=# #の後に \d テーブル名; と打てばテーブル詳細が表示されます。 ex: \d users; と入力すれば Table "public.users" Column | Type | Collation | Nullable | Default ------------------------+--------------------------------+-----------+----------+----------------------------------- id | bigint | | not null | nextval('users_id_seq'::regclass) first_name | character varying | | not null | ''::character varying last_name | character varying | | not null | ''::character varying first_name_kana | character varying | | not null | ''::character varying last_name_kana | character varying | | not null | ''::character varying email | character varying | | not null | ''::character varying encrypted_password | character varying | | not null | ''::character varying Indexes: "users_pkey" PRIMARY KEY, btree (id) "index_users_on_email" UNIQUE, btree (email) "index_users_on_reset_password_token" UNIQUE, btree (reset_password_token) Referenced by: TABLE "orders" CONSTRAINT "fk_rails_f868b47f6a" FOREIGN KEY (user_id) REFERENCES users(id) みたいな感じで表示されて確認できます。 終了方法 ○○_development=# \q で抜けることができます!! 最後に 調べていた記事には PostgreSQL利用時の操作になる と記載がありました!!Mysqlではできないのかもしれません。 備忘録として書きましたが誰かの為になることがあれば幸いです!!
- 投稿日:2021-12-24T22:50:34+09:00
mruby でもリファインメントを使いたい!!!
Rubyist の皆さんはリファインメント機能が好きですよね?僕も大好きです。 ですが残念なことに mruby では実装されておらず使えません。 まあ、実装されていないだけならば実装してみようと思い立ち、作業を始めてすでに3年ほどが経ったと思います。その作業途中で行き詰まって数ヶ月停滞することが何度もありましたが、それなりに完成してきたこともあってどこまで出来ているのかをまとめてみようと思いました。 なおすでにその途中の成果の一部は本当の背景を明かさずにそれっぽい理由をつけてプルリクを送ったところ、だいたい取り込まれています?。 直近だと OP_ASET に関する「Fixed a discrepancy in OP_ASET #5579」がそれに当たります。これは Symbol#to_proc メソッドの再実装作業中に見つけたものです。キーワード引数が引き継がれないことがある問題に関しても、この作業途中で発見したことでした。 早速使ってみよう 使ってみようとは言っても、まだ本家には取り込まれていません。開発途上版であれば 僕の開発ブランチ から試すことが出来ます。 リファインメント機能は mrbgems/mruby-refinement を取り込んで使うようになっています。つまり既定のままビルドすると「なにも出来ねーじゃん」状態となってしまうので気をつけて下さい。なので build_config へ明示的に追加する必要があります。 MRuby::Build.new do toolchain gembox "default" gem core: "mruby-refinement" ## 超大事なオマジナイ!!! end このように超大事なオマジナイを追加してビルドすれば、ESP32 上でもリファインメントが使えるようになります! なお開発ブランチでは一時的に mrbgems/mruby-refinement を有効としているので、そのままビルドするだけです。 % curl -L https://github.com/dearblue/mruby/archive/refs/heads/refinements.tar.gz | tar xf - % cd mruby-refinements % rake -m ビルドできたらすぐにお試し出来ます! % cat <<RUBYCODE > test.rb class Girl def age = 12 def place = "小学校" end module Fake refine Girl do def age = 20 def place = "ふくしの……大学?" end end girl = Girl.new 2.times do puts "#{girl.age}歳なんですけど! #{girl.place}に通ってるんですけど!" using Fake end RUBYCODE % bin/mruby test.rb 欠点 RAM をいっぱい消費するようになります。すぐ上で ESP32 でも使えると書きましたが、RAM が 1 MiB 以下の機器で使うことはおすすめできません。それにリファインメントが有効な範囲ではメソッド探索が遅くなる上に、メソッドキャッシュ処理も迂回されます。 メソッドを定義できるようにしよう (課題あり) メソッドを登録するのであれば struct RClass を使うようにすればいいはずです。MRB_TT_MODULE と区別するために MRB_TT_REFINEMENT を追加したのは作業を始めた最初期の頃でした。 リファインメントオブジェクトが include / extend 出来ることを知らなかったので、まあしゃあない。そのうち MRB_TT_MODULE と struct RClass::flags |= MRB_MODULE_REFINEMENT みたいな形に変えなきゃ。 module M R = refine Object do end module R # => M::R is not a module (TypeError) # CRuby は問題なし end end リファインメント情報を保存しよう using メソッドで有効化させますが、どうやってその情報を保存させるかは悩みどころでした。 リファインメントはスコープ途中で動的に有効となります。その仕組み上、現在呼び出し中のスコープでリファインメント情報を更新する必要があります。どうしても呼び出し情報 (mrb_callinfo) にメンバを増やす必要がありますが、こちらはまだいいです。 メソッドとして登録されることを考えるとブロック (struct RProc) に保存する必要がありますが、追加する余地がありません。 なので、struct RProc::body メンバに目をつけて、これを拡張しました。リファインメントが有効な時は struct RProc::body.extra メンバを経由して、ここに irep と attached_refinements を保持するための構造体を新設します。 これまでは struct RProc::body.irep メンバを直接参照することがありましたが、body.direct_irep という名前に変更しました。struct RProc::body.irep を直接参照するのではなく、MRB_PROC_IREP() マクロを使うようになります。 こうすることで struct RProc::body.extra を想定していない古いソースコードはコンパイルエラーになります。互換性は犠牲になりました。 struct RProc { MRB_OBJECT_HEADER; union { const mrb_irep *irep_direct; // irep から変更❕ mrb_func_t func; #ifdef MRB_USE_REFINEMENT // ?❕ struct mrb_proc_extra *extra; // ?❕ #endif // ?❕ } body; const struct RProc *upper; union { struct RClass *target_class; struct REnv *env; } e; }; リファインメントを実現するためのデータ構造を整理・把握するために図を作りました。作業を投げ出した後で再開する時にとても有用です。 コールスタックの増加を抑えよう 有効化したリファインメント情報を保持する 1 ワード (struct RArray *) を mrb_callinfo へと追加したことを前述しました。 作業を始めた時期が mruby-1.4.1 の頃だとすると、この時点で sizeof(mrb_callinfo) は 32 ビット CPU で 12 ワードです。ここでさらに 1 ワードを追加することは躊躇してしまいますね。mruby-2.1.2 の時点では 10 ワードですが、この頃には減らす算段がついていました。 ある時 mruby のリポジトリへファイバーに関する問題が報告され、いい切っ掛けと思って送ったプルリクエストが「Reorganize mrb_callinfo #5272」です。この変更によって、6 ワードにまとめることができました。これなら 1 ワードくらい増えても文句が出にくいことでしょう。なお MRB_USE_REFINEMENT が未定義であれば増やさずに 6 ワードを維持します。 子スコープ・ブロックへリファインメント情報を伝搬させよう 新しいブロックやスコープが生成された場合、リファインメント情報を継承します。現在の呼び出し情報 (mrb_callinfo *ci = mrb->c->ci) からリファインメント情報を取得し、mrb_proc_new() (あるいは mrb_closure_new()) にそのまま渡します。このリファインメント情報は、リファインメントが定義されている配列オブジェクトである struct RArray へのポインタです。 ここでモジュールやクラスのスコープでリファインメントの適用範囲が変わってくることを思い出して下さい。配列オブジェクトを共有することは望ましくないと思うかもしれません。 親スコープも子スコープも共有できるうちは共有して、#using メソッドが呼ばれたらその中でコピーと追加を行うようにしました。そのため #using が連続する場合、一時的に配列オブジェクトのコピーが多発することになります。 このあたりも多くの RAM を必要とする理由になります。 refine ブロックの中で自身のリファインメントを影響させよう #refine メソッドに与えたブロックの内部では、それ自身で定義したメソッドも呼び出せる仕様になっています。つまり暗黙のうちに #using メソッドが限定的に使われているわけですね。 mruby では Ruby 空間で呼び出し情報 (mrb_callinfo *ci) を細工出来ないので、C 空間で ci をいじってから渡されたブロックに制御を渡しています。 ところで mruby だとメソッドの可視性が常に public なので、直接 Kernel.refine を呼んでリファインメントを定義できてしまいますよね。 どうにかしようかなと思いましたが、Ruby-3.1 preview1 で class Module public :refine end Kernel.refine Object do def ほげ end end というコードで確認したら、まあいいかという気になってそのままです。 refine メソッドの戻り値をリファインメントオブジェクトにしよう (課題あり) Module#refine メソッドの戻り値はリファインメントオブジェクトです。引数として与えたブロックの戻り値ではありません。 すでに普通のブロック呼び出しを行えないことは述べました。なのでさらに特殊な呼び出し方を行っています。 mrb_irep_exec() による末尾呼び出しを用いますが、さらに手を突っ込んで予め return self するだけのブロックで refine メソッドの呼び出しをすり替え、その上にブロック引数を呼び出すようにしています。こうすることでブロックの戻り値に関係なくリファインメントオブジェクトを返すようになっています。これは以前 Binding を実現するときに取ったフックブロックのような手法です。 ただ、これを書いている時に mrb->c->ci->stack を一つずらすだけでリファインメントオブジェクトを返せるなと思い至りました。そうすれば mrb_cipush() を露出させる必要もなくなるし。 モジュールでもリファインメントを使えるようにしよう (課題あり?) Ruby-2.4 からはモジュールに対してもリファインメント機能が有効になっています。mruby でもモジュールに対してリファインメントを対応させました。 特に悩んだのは prepend と super を組み合わせた場合のメソッド探索です。作業をちょっとして数ヶ月投げ出してを何度か繰り返し、結局満足行く出来になるまで1年を越えることになりました。 根本的な原因は継承やミックスインの実現方法を分かった気になっていたことです。そのため、途中でどのようにポインタが継っているのかを mrb_prepend_module() の動作から確認することにしました。 これでもまだ試行錯誤を繰り返しましたが、どうにか満足の行く結果を得ることが出来るようになりました。 そのあと忘れていた CRuby の挙動と確認してみたら……あれ、結果が違う? refine-super.rb module A end module M refine A do def downcase "<<#{super}>>" end end end class String prepend A end using M p "ABCDEFG".downcase % bin/mruby refine-super.rb "<<abcdefg>>" % ruby31 refine-super.rb refine-super.rb:7:in `downcase': super: no superclass method `downcase' for "ABCDEFG":String (NoMethodError) Did you mean? downcase! from refine-super.rb:17:in `<main>' gem に分離しよう 最近になってリファインメント機能の大部分をまとめてコアから分離できることに気が付きました。ソースコードの見通しも良くなるし、build_config に conf.defines << "MRB_USE_REFINEMENT" とするよりは conf.gem core: "mruby-refinement" とした方が好ましいだろうという判断もあります。 なので、リファインメント関連の以下のメソッドは gem で定義されます。 main#using Module#using Module#refine Module.used_modules そのため mruby のコアには、リファインメントを実装するためにどうしても必要となる拡張部分のみが残ることとなりました。その拡張部分は多くが #ifdef MRB_USE_REFINEMENT 〜 #endif で囲まれているので、リファインメントを使わなければバイナリサイズの増大は最小限に抑えられているはずです。正確な比較はそのうち……。 Symbol#to_proc をリファインメントに対応しよう Ruby ではシンボルをブロックとして与えることが出来ます。 p [1, 2, 3].reduce(&:+) この時の + メソッドはリファインメント対象となります。 mruby において Symbol#to_proc メソッドは mrblib/symbol.rb ファイルで定義されていますが、ファイル拡張子から分かるように Ruby で書かれています。ここで生成されるラムダオブジェクトはリファインメントの影響を受けるブロック呼び出しが行なえません。 # 引用元: https://github.com/mruby/mruby/blob/39191cd9fadfea4940298cbcac2533597f76aa71/mrblib/symbol.rb#L2-L6 def to_proc ->(obj,*args,**opts,&block) do obj.__send__(self, *args, **opts, &block) end end リファインメントを有効にした状態のラムダオブジェクトを返せばいいので、Symbol#to_proc は C で再実装し、生成されるラムダオブジェクトに細工をすれば目的は達成できます。このラムダオブジェクトは Ruby で書いたものを bin/mrbc でコンパイルしたバイトコードから構成されています。 000 OP_ENTER 1:0:1:0:0:0:1 (0x41001) 004 OP_MOVE R4 R1 ; R1:obj 007 OP_LOADSELF R5 009 OP_ARRAY R5 R5 1 013 OP_MOVE R6 R2 ; R2:args 016 OP_ARYCAT R5 R6 019 OP_MOVE R6 R3 ; R3:block 022 OP_SENDB R4 :__send__ n=*|k=* (0xff) 026 OP_RETURN R4 だけどスプラッタ引数のための配列オブジェクトを再利用できないかな? ということで新しい命令を加えて、ハンドアセンブルします。 000 OP_ENTER 0:0:1:0:0:1:1 (0x1003) 004 OP_JMPEMPTY R1 +17 ; to 025 008 OP_AREF R4 R1 0 012 OP_ASET R0 R1 0 016 OP_MOVE R0 R4 019 OP_SSENDB R0 :__send__ n=*|k=* (0xff) 023 OP_RETURN R0 025 OP_GETCONST R1 ArgumentError 027 OP_STRING R2 L[0] ; "no receiver given" 030 OP_RAISEEXC R1 (R2) 032 OP_RETURN R0 ; not reached ここで2つの新命令を追加しました。レジスタのオブジェクトが空 (obj.empty?) であれば分岐する OP_JMPEMPTY 命令、そして指示された例外を発生させる OP_RAISEEXC 命令です。 最初にちょろっと紹介した OP_ASET 命令を使っているのがわかるでしょうか? この時はいいアイディアだと思いましたが、間もなく気が付きます。__send__ メソッドまでリファインメントの餌食となってしまうことに。 それで結局は次のようになりました。 000 OP_ENTER 1:0:1:0:0:1:1 (0x41003) 004 OP_FORWARD R1.__send__(R0, *R2, **R3, &R4) 005 OP_RETURN R0 OP_FORWARD 命令を追加していますが、とてもシンプルです。Symbol#to_proc に関してはこれでよさそう。 Proc のリファインメント情報を更新しよう 2.times { a.namamugi using Namagome } こんなコードがあった場合、一度目は普通の #namamugi メソッドが呼ばれます。二度目はリファインメントが効いた状態の #namamugi メソッドが呼ばれます。 Proc オブジェクトが ROM に置かれていたら対応できませんが、書き換え可能であれば対応するようにしました。 文字列展開される時の暗黙の変換に対応しよう (課題あり) Ruby では文字列の中でオブジェクトが暗黙的に #to_s されますが、これを行う時の処理をどうするか脳内で審議中です。とりあえず OP_SEND を OP_STRCAT の前に追加するようにしていますが、正直望ましくないよなあということでこの案は没になります。 # "1#{2}3" 000 OP_STRING R1 "1" 003 OP_LOADI_2 R2 005 OP_SEND R2 :to_s 0 ; 本当はいらない 009 OP_STRCAT R1 011 OP_STRING R2 "3" 014 OP_STRCAT R1 ; => "123" その後、OP_SEND 命令の代わりに OP_TOSTRING 命令を追加しましたが、やっぱり望ましくありません。 現在は OP_STRCAT で文字列以外 (mrb_type(obj) != MRB_TT_STRING) が与えられたら、VM の中から #to_s を強制するようなブロック呼び出しを行い、再度 OP_STRCAT を実行するようにしようかなと思案中です。この時変換しても String インスタンスでなければ OP_STRCAT で永久ループになるため、型の確認を行う新規命令も必要そうです。 000 OP_SEND R0 :to_s 0 004 OP_GETCONST R1 :String 007 OP_CHECK R0 (R1) ; 新規命令 009 OP_RETURN R0 でも String 定数を入れ替えられたらすぐに無限ループするし、代わりに enum mrb_vtype を使うと mruby/c のような第三者実装系で問題になりそうだしどうしたもんだか……。専用命令にするしかなさそう? #const_missing メソッドにもリファインメントを適用しよう (課題・放置中) mruby 仮想機械の中では定数を取得する時に OP_GETCONST あるいは OP_GETMCNST という命令が使われます。この命令は mrb_vm_const_get() または mrb_const_get() という関数を呼び出すだけです。その関数の中で定数が見当たらなければ、mrb_funcall() を通して #const_missing メソッドが呼び出されます。 これは別のブランチで mrb_vm_exec() 関数の再呼び出しを避ける目的で書き換え作業中となっています。機能的には問題がない状態ですが、全体的なパフォーマンスが5%〜10%落ちるという問題があって、現在絶賛放置中です…………。 <閑話> mrb_vm_exec() 関数の再呼び出しを避ける関連でもう一つ。 case obj; when *ary; ...; end みたいなコードがある時、when *ary の部分は #__case_eqq メソッドが使われます。 OP_SEND R2 :__case_eqq n=1 (0x01) この #__case_eqq メソッドの実体である src/kernel.c:mrb_obj_ceqq() を見てみると、内部で mrb_funcall() を経由した mrb_vm_exec() への再呼び出しが確認できます。これを専用命令 OP_CASE_EQQ (仮) で仮想機械上のメソッド呼び出しに置き換えてやれば、mrb_vm_exec() への再呼び出しを抑制できるかもしれません。というか、これを書いていたら、ここもリファインメントが適用されてしまうことに気が付きました。 リファインメントの効かない特殊な OP_SEND が必要となるかも……。 </閑話> その他の課題を確認しよう まだ全く手を付けていないものもまだあります。 #method メソッド #methods メソッド #respond_to? メソッド 他にもあるけど、それは追々 @pink_bangbi さんのブログを確認しながらにします。 https://secret-garden.hatenablog.com/entry/2018/12/03/231925 https://secret-garden.hatenablog.com/entry/2018/12/25/235407 それと公式によるリファインメントの仕様書も改めて確認する必要があります。 https://bugs.ruby-lang.org/projects/ruby-trunk/wiki/RefinementsSpec 一番難しい課題はテストを書くことですね。 で、いつ来るの? この先1年の間にはプルリクエストを送れるようにしたいなあ……。
- 投稿日:2021-12-24T21:26:55+09:00
RailsでDDDのバリューオブジェクトを実装してみる
この記事は何 DDDを学んだ事がある方は「バリューオブジェクト」という名前を一度は聞いた事があるのではないでしょうか・ この記事ではRailsでバリューオブジェクトを実装するならどのような実装にすると良さそうかを紹介します。 なお、バリューオブジェクトの具体的な実装方針は千差万別だと思うので、この記事で紹介する実装はあくまで一例だと思っていただけるとありがたいです。 具体的なバリューオブジェクトの解説は↓の記事などが参考になると思います。 バリューオブジェクトの条件 バリューオブジェクトは以下の条件を満たしている必要があります。 条件は以下の通りです。 オブジェクトはイミュータブルである 値の変化はインスタンスの再生成で実現する オブジェクトの同一生はバリューオブジェクトが持つattributeの一致で定義される Railsで実装する際も、これらの条件を満たす必要があります。 そしてバリューオブジェクトとして実装する以上、バリデーションも行えるようにしたいです。 これらの要件を満たすインターフェースを実装してみます。 実装物 実装物はズバリ↓のようなクラスです。 class Value::Base include ActiveModel::Model def initialize fail ActiveRecord::RecordInvalid unless valid? end # @param other [::Value::Base] # @return [true, false] def ==(other) evaluate_attributes.all? { |variable| try(variable) == other.try(variable) } end private # @return [Array<String>] attributes used to equal evaluation def evaluate_attributes fail NotImplementedError end end このクラスは以下のように継承し、利用します。 class Value::Name < Value::Base attr_reader :first_name, :last_name validates :first_name, presence: true validates :last_name, presence: true def initialize(first_name:, last_name:) @first_name = first_name @last_name = last_name super() end def full_name "#{first_name} #{last_name}" end private # @note Override {::Value::Base#evaluate_attributes} def evaluate_attributes %i(first_name last_name) end end このクラスの機能について説明します。 オブジェクトの不変性 このクラスでは、基本的にsetterを提供せずに、getterのみを公開します。 こうすることで、外部からインスタンスの変更を行うことはできません。 オブジェクトの同一性 このクラスにはevaluate_attributesというメソッドを実装しています。 また、==メソッドをオーバーライドし、各attributesの値の同一性を検証することでオブジェクト自体の同一性を判定するようにしています。 このようにすることで、==メソッドを用いた同一性検証はバリューオブジェクトが満たすべき条件と同じになります。 値のバリデーション このクラスではActiveModel::Modelモジュールをincludeしています。 また、initializeのタイミングでvalid?がfalseを返すときにエラーを返すようになっています。 このように実装をし、子クラスで最後にsuper()を実行することで、インスタンス生成時に検証まで行えるようになっています。
- 投稿日:2021-12-24T17:21:43+09:00
[py2rb] dict の for
はじめに 移植やってます for (Python) h = { 1: 2, 3: 4, 5: 6 } for x in h: print(x) # 1 # 3 # 5 知っている人にとって当たり前かもしれませんが、dictのforで変数が1つの場合、キーのみを返します。 知らなかったため、人生初のstack level too deepを頂きました。 とんでもないクリスマスイブになるところでしたが、デバッグの甲斐あって普通のクリスマスになりそうです。 each_key (Ruby) h = { 1 => 2, 3 => 4, 5 => 6 } h.each_key do |key| puts key end h.keys.map{ puts _1 } mapやselectのときは、keysを使用することも可能です。 メモ Python の for を学習した 道のりは遠そう
- 投稿日:2021-12-24T17:16:37+09:00
MySQLが立ち上がらないときの解決策
rake aborted! ActiveRecord::ConnectionNotEstablished: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (38) というエラーが発生した。/tmpディレクトリを確認してもmysql.sockはしっかりと存在する。 mysqlを起動してみると下記のエラーが起こっていることが判明した。 $ sudo mysql.server start Password: Starting MySQL .Logging to '/usr/local/var/mysql/fujiwaratakuminoMacBook-Pro.local.err'. ERROR! The server quit without updating PID file (/usr/local/var/mysql/fujiwaratakuminoMacBook-Pro.local.pid). というエラーが起こっていることが判明した。 pidファイルとは? 「.pid」で終わっているファイルがpidファイルです。このファイルには、該当するプロセスのプロセスIDなどの情報が記述されています。たとえば、/var/run/crond.pidファイルには、crondのプロセスIDが記述されています。 このファイルは、スクリプトやほかのプロセスで利用されます。プロセスIDが記述されているので、このファイルはプロセスの制御(再起動や停止など)、プロセス同士の連携などに利用されます。 参照先 「/var/run」ディレクトリ ググった末にmysql起動時でエラーが起きた時の対処の記事を参考に $ sudo rm /tmp/mysql.sock $ chown -R _mysql:_mysql mysql を実行。再度sudo mysql.server restartを実行するが、 $ sudo mysql.server restart ERROR! MySQL server PID file could not be found! Starting MySQL .Logging to '/usr/local/var/mysql/fujiwaratakuminoMacBook-Pro.local.err'. ERROR! The server quit without updating PID file (/usr/local/var/mysql/fujiwaratakuminoMacBook-Pro.local.pid). と違うエラーが発生した。/tmp/mysql.sockを消したのがまずかったのかと思った。 今度は、mysql 起動時のThe server quit without updating PID file エラーの回避法を参考にした。 $ ls /usr/local/var/mysqlを実行してみると*****.local.pidがなかった。 $ touch /usr/local/var/mysql/*****.local.pid を実行。sudo mysql.server restartを実行してみるが、*****.local.pidに権限がなかったので、 sudo chown -R _mysql:_mysql /usr/local/var/mysql/ これで無事動くことができました! 参考にさせて頂いていたサイト 「/var/run」ディレクトリ mysql起動時でエラーが起きた時の対処 mysql 起動時のThe server quit without updating PID file エラーの回避法
- 投稿日:2021-12-24T16:38:37+09:00
OpenStructを見ながら感じる、ちゃんとしたRubyのクラスを実装するために必要なこと
はじめに Rubyは人間にやさしい言語なので、雰囲気でRubyを書いていても、それなりに動作してくれます。 雰囲気でコードを書いている自分のために、どうすれば今よりもちゃんとしたRubyのクラスを書けるのか考えてみます。 題材としてOpenStructというクラスをとりあげます。 OpenStructとは? オープンストラクトとは、あとから自由にメンバを追加できる構造体です。Rubyの場合は、Hashがあるので、オープンストラクトを使う機会は少ないと思います。 けれどもOpenStructはすべてRubyで実装されていています。C拡張を使っていません。コード量も1ファイル472行で、そのほとんどはコメントです。OpenStructを読めば最低限「きちんとした」Rubyのクラスの書き方がわかるはずです。(19人もコントリビュータがいて、よく保守されています) OpenStructを読む 気になったポイントをコードのスニペットとともに挙げていきます。 1. 文字列は変更不可能にするべきか? # frozen_string_literal: true このコメント、入れるかどうか迷うところもありますが、Ruby標準ライブラリのOpenStructでは入れる方針となっているようです。最近は bundler で gem のテンプレートを作成した時や、rubocop -A で自動修正をかけたときも、このコメントが入るようになっていますね。 メリットは、文字列の破壊的操作を禁止することでバグを減らすことができます。また高速化できるケースもあるかもしれません。デメリットとしては、"aaa" << "bbb" という便利な文字列の結合イディオムが使えなくなってしまうことです。私はこの文字列の結合が結構好きなので、コメントを入れないことが多いのですが、「ちゃんとしたRubyのクラス」を実装するときには文字列は変更不可能にしておいたほうが良いでしょう。 2. オブジェクトのディープコピーへの対応するべきか? Rubyのcloneやdupはshallow copyで参照先まではコピーしません。しかし、それでは不便な場合もあるのでinitialize_cloneやinitialize_dup などのメソッドが定義されているようです。clone と dup にも微妙な違いがあるようで、「dup はオブジェクトの内容, taint 情報をコピーし、 clone はそれに加えて freeze, 特異メソッドなどの情報も含めた完全な複製を作成します。 」と書かれています。initialize_dupはupdate_to_values!をMarshal.loadと一緒に使うようになっています。 initialize_clone # Duplicates an OpenStruct object's Hash table. private def initialize_clone(orig) # :nodoc: super # clones the singleton class for us @table = @table.dup unless @table.frozen? end initialize_dup private def initialize_dup(orig) # :nodoc: super update_to_values!(@table) end private def update_to_values!(hash) # :nodoc: @table = {} hash.each_pair do |k, v| set_ostruct_member_value!(k, v) end end ディープコピーに対応するべきかどうかは、コピー先のオブジェクトが参照しているインスタンス変数を変更した時に、コピー元のオブジェクトの動作が変化してほしいかどうかによって変わってくると思います。OpenStructはコピー先を編集したときに、コピー元の挙動が変わると都合が悪いので、ディープコピーに対応するようになっていると思います。 しかし、Rubyにおいては、そもそもオブジェクトのクローン自体があまり良いパターンではなくて、できるだけコピーはせず、newを使うべきだと思います。 3. 後方互換性に配慮するべきかどうか? if文を使って、Rubyのバージョンによって異なるメソッドを定義しているところがあります。 if {test: :to_h}.to_h{ [:works, true] }[:works] # RUBY_VERSION < 2.6 compatibility def to_h(&block) if block @table.to_h(&block) else @table.dup end end else def to_h(&block) if block @table.map(&block).to_h else @table.dup end end end あまりきれいなコードではないと思うので、Rubyバージョン2.6以降にしか対応しないと決めて、バッサリと切ってしまうのも一つの手です。しかしRuby標準ライブラリでは後方互換性を捨てられないので、こうしたテクニックが必要になるでしょう。 4. 戻り値はコピーしてから渡すべきかどうか? 先程のメソッドですが、単にクラスの内部の@tableを渡すのではなく、きちんと dup してから渡すようにしていますね。Rubyだと参照渡しをあまり意識することはありませんが、それが原因でバグを作ることもあります。これも先程のディープコピーと同じで、渡した先で変数が変更された時に、元のオブジェクトの動作が変更されるべきかどうかによって、コピーすべきかどうかは変わってくると思います。 5. ブロックを引数にとらないときに、Enumeartorを返すイテレータ def each_pair return to_enum(__method__) { @table.size } unless block_given! @table.each_pair{|p| yield p} self end これも「まれによく使う」パターンのコードだと思います。ブロック引数がないときにEnumeratorを返すパターンです。このパターンは忘れてしまいがちで、必要になるとその都度調べている気がします。 # まれによく使う記法 return to_enum(__method__) { @table.size } unless block_given! __method__ などのイディオムも興味深いですね。こういのを調べずにササッと書けるとプロだなぁという感じがします。(個人の感想です) 6. オブジェクトの読み込み・保存に対応するべきかどうか? 余裕があれば marshal_dump を実装してオブジェクトの保存にも対応しておきたいものです。 def marshal_dump # :nodoc: @table end alias_method :marshal_load, :update_to_values! # :nodoc: update_to_values は initialize_dup でも見かけました。dup とセットにして実装するといいかもしれません。 marshal_dump と marshal_load を実装するメリットはデータをファイルに保存することができることです。ほかの用途としては、drubyへの対応が思い浮かびます。 問題は、データをファイルに保存する必要があるクラスはさほど多くないことです。一部のクラスでは、Marshal.dump は頻回に使われる可能性があります。具体的には数値計算やワークフロー的な操作が絡むもので、途中経過や状態を保存したい場合です。このケースではMarshal.dumpは大活躍すると思います。自分の作成するクラスをdump するシーンがあるかどうか考えて、ありそうなら実装するぐらいのスタンスだと思います。 7. Ractorに対応するべきかどうか? def new_ostruct_member!(name) # :nodoc: unless @table.key?(name) || is_method_protected!(name) if defined?(::Ractor) getter_proc = nil.instance_eval{ Proc.new { @table[name] } } setter_proc = nil.instance_eval{ Proc.new {|x| @table[name] = x} } ::Ractor.make_shareable(getter_proc) ::Ractor.make_shareable(setter_proc) else getter_proc = Proc.new { @table[name] } setter_proc = Proc.new {|x| @table[name] = x} end define_singleton_method!(name, &getter_proc) define_singleton_method!("#{name}=", &setter_proc) end end Ractorがよくわからないのですが、Ractorに対応しているかどうかでコードを分けているようです。 しかし、nil.instance_eval{} は難しくて何をやっているのかよくわかりません。Ractorに対応したコードを書くのはなかなか大変そうです。 8. freezeへ対応するべきかどうか? def freeze @table.freeze super end これも野良のクラスは対応していないものが多いんじゃないかと思うんですよね。しかしRuby標準ライブラリのクラスはしっかり対応します。 9. method_missingのエラー処理 private def method_missing(mid, *args) # :nodoc: len = args.length if mname = mid[/.*(?==\z)/m] if len != 1 raise! ArgumentError, "wrong number of arguments (given #{len}, expected 1)", caller(1) end set_ostruct_member_value!(mname, args[0]) elsif len == 0 @table[mid] else begin super rescue NoMethodError => err err.backtrace.shift raise! end end end def dig(name, *names) begin name = name.to_sym rescue NoMethodError raise! TypeError, "#{name} is not a symbol nor a string" end @table.dig(name, *names) end 別に変わったことはしていないのですが、さすが標準ライブラリだけあってエラー処理の網目がきっちりしてると感じます。ミスすることが多いのでそもそもできればmethod_missingは使うなという話もあります。 10. 引数には文字列もシンボルも取れるようにするべきか? def [](name) @table[name.to_sym] end 一般的にはRubyのライブラリでは両方とも取れるようにしておく方が行儀が良いと思います。しかし、個人的には、これは必ずしも守る必要はないかなと思います。シンボルしか取らないメソッド、文字列しか取らない方が実装するのは楽ですし、ユーザーも間違いに気が付きやすくなります。しかし、Rubyの標準ライブラリは、きちんと両方とも取れるようにしています。 11. 不要になったメソッドを忘れずに消去するべきか? def delete_field(name) sym = name.to_sym begin singleton_class.remove_method(sym, "#{sym}=") rescue NameError end @table.delete(sym) do return yield if block_given! raise! NameError.new("no field `#{sym}' in #{self}", sym) end end remove_method を使って不要になったメソッドを消しています。特に変わったことはしていないと思うのですが、人間は「必要なものを付け加える」ことは得意なのですが、「不必要なものを適切なタイミングで除去する」ことは大変苦手としています。 動的にメソッドを追加することはよくありますが、動的にメソッドを削除する機会はそれほど多くないと思うんですよね。しかし、それは本当はおかしくて、動的にメソッドを追加するシーンがあるのならば、それと同じぐらい動的にメソッドを削除するシーンがあってしかるべきだと思います。OpenStructの場合は、不要になったメソッドを除去するのは必須なので忘れないと思いますが、「消しても消さなくてもどちらでもいい」みたいなケースは結構あると思います。動的に追加したメソッドの消し忘れには注意したいと思いました。 12. inspectへ独自に対応するべきか? しておいたほうがいいですよね。 def inspect ids = (Thread.current[InspectKey] ||= []) if ids.include?(object_id) detail = ' ...' else ids << object_id begin detail = @table.map do |key, value| " #{key}=#{value.inspect}" end.join(',') ensure ids.pop end end ['#<', self.class!, detail, '>'].join end alias :to_s :inspect 実はこのコードを読むまでは、Rubyの標準ライブラリのオブジェクトのinspectってもっと自動的に生成されているのかなと思ってました。でも実際にはそうではなくて、こんな風に何行もコードを書いて普通に生成していたのですね。inspect は野良のライブラリでもちゃんと実装しているケースがしばしば見かけますが、きちんと実装するときはこのようにすればよいということで参考になります。 一方で、実用上はinspectを独自に実装しなくても問題がない場合が多いです。必要がないものは実装しないほうがコードがスッキリするので、必要だと感じた時だけ実装すれば十分だろうと思います。 13. 比較演算子へ対応するべきかどうか? def ==(other) return false unless other.kind_of?(OpenStruct) @table == other.table! end def eql?(other) return false unless other.kind_of?(OpenStruct) @table.eql?(other.table!) end これも、個人的にはほとんど実装したことがないのですが、オブジェクト同士を比較したい場合は確かにあります。けれども、ほとんどのオブジェクトというのは、new した時点でそれぞれ全然別のものであることが多く、同一あるいは大小の判定が必要になるものは限定的だと思います。なので、そういうシーンがありそうかどうかで、実装するべきか否かを決めればよいのではないでしょうか。 YAML対応 encode_with と init_with メソッドを実装しておけばいいらしい。YAMLに書き出す可能性がある場合は実装。これは必ずしもちゃんとしたクラスなら実装しなければならないというよりは、OpenStructで必要として実装されたものだと思います。 14. メソッド名の微調整をどう行うか? メソッドをあとからまとめてmapみたいに調整する需要ってまぁまぁあると思うんですよね。メソッド名の語尾を微妙に変化させたりとか。そういう場合は基底クラスを作って「継承」を使ったりしますが、ここではインスタンスメソッド一覧を取り出して、正規表現でフィルタリングし、あたらしい名前のメソッドをエイリアスで作っています。そこまで行儀が良くない方法のような感じもしますが、標準ライブラリで使われている方法なら、そうやってもいいんだなという安心感はあります。 # Make all public methods (builtin or our own) accessible with <code>!</code>: give_access = instance_methods # See https://github.com/ruby/ostruct/issues/30 give_access -= %i[instance_exec instance_eval eval] if RUBY_ENGINE == 'jruby' give_access.each do |method| next if method.match(/\W$/) new_name = "#{method}!" alias_method new_name, method end 15. 複数のRuby実行環境に対応するべきかどうか? 上のコードには、if RUBY_ENGINE == 'jruby' でJRubyを場合分けしています。自分はJRubyのコードを実行する機会はほとんどないので、JRubyの特徴がよくわかっていません。 これに関しては考え方が難しいですが、一般論としてはJRubyへの個別の対応は必要ないと思っています。JRubyに個別に対応するぐらいであれば、しっかりとリファクタリングを行って機構を単純化して、Ruby2.4ぐらいまでの保守的な記法に徹して、さまざまなRuby処理系で動くことを期待するという方が常道ではないかなと思います。しかし、視界の届く範囲のユーザーにJRubyユーザーが存在するとか、GithubのissueにJRubyに関するポストが寄せられたとか、個人的にCRuby以外の実装に興味があるとか、仕事でJRubyを使っているとかそういうケースでは、きちんと対応していくことが求められると思います。 16. ドキュメントを書くべきかどうか? OpenStructのコード全体を見て感じるのは、ドキュメントが非常に多いということでしょう。Rubyの標準ライブラリは、たくさんのユーザーがいます。多くのユーザーにライブラリの使い方を説明するために、丁寧なコメントが書き込まれています。通常私たちが書くコードでここまでドキュメントが多いものはほとんどないでしょう。 これもユーザーの総数を考えて、Rubyの標準ライブラリのように「のべ」ユーザー数が非常に多くなりそうな場合には丁寧にコメントを追加して、あまりユーザーが多くないような場合は、ドキュメントの作成を省いてコードに語らせる、といったバランスになると思います。 おわりに どこまでやるべきか? 答えなき問い こうやってみるとOpenStructはかなり細かく実装していますけど、「きちんとしたクラス」を目指しているわけでなければ、ここまでしっかり実装する必要はないと思います。物事にはすべてメリットと、デメリットがあり、きちんとしたクラスを目指して実装すれば、実装のスピードは犠牲になりますし、複雑さも上昇することになるので、場合によっては不要なバグを埋め込んでしまう結果にもなりかねません。一方で、「きちんとしたクラス」を意識することは不特定多数のユーザーが使うようなライブラリを作成するときには、頭の片隅に置いても損はないと思われます。(もしもそんな機会があれば。) そもそも「どこまでやるべきか?」というのは答えのない問であり、Rubyistの数だけスタイルがあるかもしれません。 そのようなことを考えること自体が、ある種の文化的な営みで悪いことじゃないんじゃないかなと思います。 この記事は以上です。
- 投稿日:2021-12-24T12:17:33+09:00
Railsでエラーメッセージを表示させる方法
手順 ①バリデーションの定義 ②エラーメッセージの記入 ③エラーメッセージを日本語化 ①バリデーションの定義 エラーメッセージはエラーが発生すると、error.full_message内に格納される。 バリデーションはモデル内で定義。 以下のように書くと、nameかemailを空白で登録した際は登録できないようにするバリデーションを追加することができる。 class User < ApplicationRecord validates :name, presence: true validates :email, presence: true end ②エラーメッセージの記入 エラーメッセージを記入する前に、前提として以下の3つを注意する。 ・エラーメッセージ表示部分をformのテンプレートに記載せず、専用のパーシャルが作られていること。 ・特定のモデルに依存せず汎用的なつくりにする必要がある。格納場所はshared配下にすること。 ・エラーメッセージの表示方法はアプリケーションごとに異なるため、エラーメッセージを直接生成するようなビューヘルパーはRailsに含まれてない。 表示が必要な場合はパーシャル化して、汎用的に使えるようにする。 記入例 #shared/_error_messages.html.erb <% if object.errors.any? %> <div class="alert alert-danger"> <ul class="mb-0"> <% object.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> #app/views/boards/_form.html.erb(パーシャルを呼び出し) <%= form_with model: board, local: true do |f| %> <%= render 'shared/error_messages', object: f.object %>※ <div class="form-group"> <%= f.label :title %> f.objectとは f.objectのobjectは、form_withやform_forなどのオプションであり、formのブロック変数に対して使用する事でブロック内でモデルオブジェクトを呼び出す事が出来る。 同じコードを流用する際に、変数を変える必要が無くなる。 ③エラーメッセージを日本語化 エラーメッセージを入れる前に、railsアプリを既に日本語化していた場合は特に必要なし。 ・config/application.rbにconfig.i18n.default_locale = :jaを追加。 ・gem 'rails-i18n'をインストール。 ・activerecord/ja.ymlにモデルの日本語化を記入。 例: ja: activerecord: models: user: ユーザー attributes: user: first_name: 名 last_name: 姓 email: メールアドレス password: パスワード password_confirmation: パスワード確認 これらを設定すると、「タイトルを入力してください」「本文を入力してください」のようなエラーメッセージが表示される。 参考記事 Ruby on Railsでエラーメッセージを表示する方法【初心者向け】 Rails フォーム入力時エラー情報を個別表示(f. objectについて) Rails6 エラーメッセージの日本語化
- 投稿日:2021-12-24T08:10:09+09:00
ActiveRecordのNullリレーションが便利そう
はじめに Railsガイドを読み返していたら便利そうなものを見つけたので備忘録代わりに残しておきます。 Nullリレーション noneメソッドは、チェーン (chain) 可能なリレーションを返します (レコードは返しません)。このメソッドから返されたリレーションにどのような条件をチェーンさせても、常に空のリレーションが生成されます。これは、メソッドまたはスコープへのチェーン可能な応答が必要で、しかも結果を一切返したくない場合に便利です。 irb(main):001:0> User.none => [] irb(main):002:0> User.none.class => User::ActiveRecord_Relation こんな感じで、空のリレーション(Nullリレーション)を返してくれる。 irb(main):003:0> User.none.limit(1) => [] リレーションを期待しているコードに対しても正常に応答するので、より保守性の高いコードが書けそう。