2020-12-12 [長年日記]
_ ActiveRecord ERDの代わりを求めて
いろいろ言われることはあるけれど、何はともあれ ActiveRecord はエコシステムの強力さが魅力。最近はあんまりやってなかったけど動くコードでモデリングするという方法を採用することがあって、その際に使えるのがコレ。
amatsuda/erd: A Rails engine for drawing your app's ER diagram
今回は ActiveRecord 以外の環境でこれに近いことができないかと思い探してみたところ、以下の組合せでそれなりに実現できそうということが分かった。
- PostgreSQL + schema dump + sql_graphviz ( Python )
- rom-sql + rom_sql_graph ( Ruby )
あくまでそれなりではあるけど。Rails + ActiveRecord から距離を置くことは実現できている。
sql_graphviz ( Python )
SQL を dot 言語に変換するアプローチ。要 Python. これなら DDL としての SQL があればなんとかなるので、ORM にも開発言語にも依存しない。
本当は Graphviz 本家に Java で動くものがあるんだけど、
これはどうにも思ったような動作をしなかった。対応している SQL にいろいろ制限があるのかもしれない。
sql_graphviz を手元で試したところ、SQLite 3 の .schema で出力した SQL では table 情報は出力できるが relation を追うことができなかったため、目的を果たすことができなかった(table 情報を出力したかったわけではない)。
PostgreSQL の schema dump の場合は意図通りに relation を辿って graph を生成することができたが、生成された dot の label に各 column の詳細な情報が含まれてなかなかゴツイ graph になってしまうので、entity が多くなると ER 図としては結構きつそうに見える。
とは言えシンプルな Python スクリプトなので、label に生成される HTML をカスタマイズすればよいだけとも言える。例えば以下のような感じ。
def field_act(s, loc, tok): fieldName = tok[0].replace('"', '') - fieldSpec = html.escape(' '.join(tok[1::]).replace('"', '\\"')) + fieldSpec = html.escape(tok[1].replace('"', '\\"'))
cf.
- macos - sqlite3: dump schema into .sql file from command line - Stack Overflow
- PostgreSQL: Documentation: 13: pg_dump
rom-sql + rom_sql_graph ( Ruby )
- rom-rb/rom-sql: SQL support for rom-rb
- davydovanton/rom_sql_graph: Display your DB (sql) association graph
rom-sql は ROM.rb の SQL Adapter のことで、Hanami::Model の実質的な正体。つまりこの組み合わせは Hanami::Model に対しても使える。
気をつけないといけなのは 2020-12-12 時点で GitHub 上の README は master 時点のものでリリース済みの 0.2.0 のものとは異なり、
# First: create rom repository object repo = UserRepository.new # Next: create Rom::Sql::Graph object graph = RomSqlGraph.generate(repo) # Generate image or html file with your db associations graph.generate_image graph.generate_html
として ROM::Container ではなく Repository を RomSqlGraph に与えないといけない。
また、出力は entity 名だけになってしまうので、前述の sql_graphviz とは反対に情報量が減りすぎてしまうという問題がある。これも中を覗いて直していってもよいが多すぎる情報を削っていくより存在しない情報を追加していく方がつらそうではある。
ちなみに ROM.rb には Rails 向けのプロジェクトもある
Rails を ActiveRecord ではなく ROM.rb と組み合わせて Repository パターンを基本に実装することもできるようだ。多少コード量が増えても Rails で影響範囲の小さなコードを実現したいという場合に利用できるかもしれない。(もっとも、今なら新規に立ち上げられるなら素直に Hanami を使った方がよさそうだとは思うが。)
cf.
2020-12-20 [長年日記]
_ Hanami::Controllerで共通の処理を差し込む
Meetup #100 - Kanazawarb に参加したらちょっと高まったので!
Hanamiは比較的継承が少ない
Hanami のコードの一つの特徴として、「継承を避けて mixin を使いがち」というものがある。もう少し具体的には
パス | 実現方法 |
apps/ | Action も View も moulde の mixin |
lib/ | Hanami::Entity も Hanami::Repository も継承 |
lib/*/mailers/ | mixin |
db/migrations/ | 単なる DSL |
のようになっていて、簡単に言うと Rails のようには
ApplicationController にメソッドを追加したらすべての controller で使える、みたいな素朴な OOP が通用しない
ようになっている。
これはこれで巨大な superclass を作らないようにさせる矯正ギプスとして面白いなと思う反面、mixin が多くなるのは Ruby 力を要求しやすいのでは? という気もしないでもない。
ApplicationControllerの代わりになるもの
application.rb の中の
module Web class Application < Hanami::Application configure do .. # Handle exceptions with HTTP statuses (true) or don't catch them (false). # Defaults to true. # See: http://www.rubydoc.info/gems/hanami-controller/#Exceptions_management # # handle_exceptions true controller.prepare do # include MyAuthentication # included in all the actions # before :authenticate! # run an authentication before callback end end end end
ここ。
Controllerのbefore, after callback
Actions: Control Flow | Hanami Guides
Controller には before / after の callback があって、Symbol を使ってメソッドを呼んだり、直接 block を与えたりすることができる。これは Rails の Controller によく似ていて、この before / after メソッドを 上の controller.prepare block にも与えることができる。
もし ApplicationController の before / after で処理を加えていたのなら、ここに同じように書くことができる。
configure do controller.prepare do before do .. end end end
また、この中で self を参照すると実際に request を受けた Controller を取得できる。
例外を拾う
例外を拾う方法は以下のようになっている。
フレームワーク | 例外を処理するメソッド |
Rails | rescue_from |
Hanami | handle_exception |
Rails の ApplicationController で言う rescue_from は Hanami では handle_exception になるんだけど、これも上の before / after と同じように Action に該当する class 内に書いても controller.prepare に与える block 内に書いても同じように動作する。
ただし、development では handle_excepions が false になっているので、
configure :development do - handle_exceptions false + handle_exceptions true end
に変更しておかないと実際のエラーを目視しながら開発することはできない。これも Rails と同じだと思う。
2020-12-31 [長年日記]
_ 自分が思うRubyの「動的さ」が世間とずれているっぽいので書きとめておくメモ
何かに強く反論したいとかじゃなくて、自分で引用しやすいようにpermalinkを作っておく、くらいの意味。
Rubyは動的型であるという言い方
動的型というのは実行時にならないと型が決まらないという意味なので、それはそう。例えば Ruby には
int a;
のようなコードは存在しないので、
a = 1 a = '1'
も正しく動く。この時 a の型は実行時にしか決まらず、動的である。間違いない。
※ 1 や '1' は実行前に決まっているけど、こっちの話題は今回は取り上げない。
動的型言語に動的型変換の機能があるけど、Rubyは意外と厳格
1 + "2"を計算する
awk
$ awk 'BEGIN { print 1 + "2" }' # -> 3
3 が返ってくる。これは数値の 1 に対して文字列の 2 が数値の 2 に動的に変換されて計算された結果である。
PHP では
$ php -B 'print 1 + "2";' # -> 3
結果は同じ数値としての 3 になる。
JavaScript は
$ node -p '1 + "2"' // -> 12
これは文字列として連結して 12 になっている。
Ruby
$ ruby -e 'p 1 + "2"' Traceback (most recent call last): 1: from -e:1:in `<main>' -e:1:in `+': String can't be coerced into Integer (TypeError)
で実行できない。
これは Integer である 1 の + というメソッド(演算子のように見えるけど)が String である "2" を強制的に Integer にすることができず、Type が合わないという例外で死んでしまっている。
"1" + 2を計算する
awk では
$ awk 'BEGIN { print "1" + 2 }' # -> 3
先ほどと同じ 3 になる。awk では + に文字列の連結としての機能は存在せず、常に数値で解釈される。
PHP では
$ php -B 'print "1" + 2;' # -> 3
先ほどと同じ 3 になる。PHP も + は必ず数値の演算として解釈される。文字列の連結には . という専用の演算子がある。
JavaScript では
$ node -p '"1" + 2' // -> 12
JavaScript では + は数値の演算にも文字列の演算にも利用できるが、混ざると文字列としての演算が優先される設計になっている。
Ruby では
$ ruby -e 'p "1" + 2' Traceback (most recent call last): 1: from -e:1:in `<main>' -e:1:in `+': no implicit conversion of Integer into String (TypeError)
先ほどと同じように思いっきり TypeError となる。若干エラーメッセージは異なるが、String である "1" の + メソッドが 2 という Integer を強制的に String にできず Type が合わないという例外で死ぬ。
もちろん揃っていれば普通に動く。
$ ruby -e 'p "1" + "2"' # -> "12"
実はRubyは型について厳しいのだが、静的に強制できないことが問題視されるようになった
Rubyの挙動を整理し直すと、
- Ruby は文字列の連結も数値の加算も + を利用する
- ただし + は演算子ではなくメソッドであり、メソッドは左辺に当たる値の class に定義されている
- String#+ は Integer を受け取っても文字列の連結を行うことはできず、Integer#+ は String を受け取っても数値として加算することはできない
オブジェクト、メソッド、class で考えると至極自然な動作をしている。
このように Ruby は型については実は意外と厳しい。よく分からない挙動で悩まされることは少ない。少なくとも動的変換で変な踏み抜き方はしないので、そういう意味では「型に関する挙動ではだいぶ安全」である。もちろん明示的に変換を指示する際にヘマをすることはできる。そこはプログラマの自由だ。
ただし、標準の機能では変数への代入、関数の引数への割り当て、戻り値について型を強制することができないという問題はある。(動的に死ぬようなコードを作ることはできる。)
逆に、PHP や JavaScript はエラーが起きずに意図しない動作をすることが問題となり、開発規模の拡大とともに型を指定したいという要求が高まり、TypeScript や PHP 7 以降の型の扱いに結実した。結果、Ruby より静的に解釈しやすくなり「より安全に『開発できる』」と言われるようになった。
「動作としての安全性よりも人間の頑張りに期待する安全性の方がよりよいものとして評価されている」ような状況なので個人的にはあまり納得はいっていないのだけど、周辺のツールを含めて「結果としてどうなのか」だけに注目すると「実際に動作させる前に検知できる」のはバグの発見の早期化という意味でよいことだと思う。
これについての Ruby の 2020 年時点での回答は Ruby 3 なので、Ruby 3.0.0 Released など、関連情報を漁ってもらうとよいと思う。少なくとも TypeScript 的なアプローチで静的に問題を検知することはできるようになっている。
エコシステムとしては TypeScript ほどの充実はまだ実現できていないけれど、システム自体は Ruby 2.6 以降でも利用できるので、今すぐ実践投入することもできる。
Rubyの本当の動的さはそっちじゃない
上の例に挙げた String や Integer では不可能だが、PHP や JavaScript のようなふわふわした挙動の演算子のようなものを持った値も以下のように作ることができる。
class AmbigiousValue def initialize(val) .. end def +(other) .. end end
さらに、上のように普通に class で定義したものについてはインスタンスの状態でメソッドを上書きできるので、あるオブジェクトだけ + の意味が違う、みたいなこともできる。
※ 残念ながら(?) Integer ( Numeric ) や String は継承ツリーに Class を持っておらず、Module に定義されている動的なメソッド定義を実現するメソッド群を持っていないので、Ruby の構文解析を維持したままいきなり 'a' + 1 が 'a1' になるようなコードを作ることはたぶんできない。
もちろんオブジェクト単位で挙動が異なるようなことは「できる」というだけで推奨してる人はたぶんいないけど、この点については自分は以下のようなことだと理解している。
午前4時。あと数時間でユーザが出勤してくる。それまでにシステムをまがりなりにも 動く状態にしておかねば… どうもサードパーティ製のあるライブラリ(ソース無し)の 挙動が怪しいのが問題の原因のようだが、APIにどういうパラメータを与えた時に バグが再現できるか絞りこめていない。もちろんサポートが開くのは明朝、それでは 間に合わないッ。だがッ! ライブラリの内部のみで使われる ある関数に、まれに異常な引数が渡っていることが分かったッ! この内部関数の呼び出しをフックして引数を修正すればとりあえず動かせるッ!---とか、
そういう状況において、「どんなに汚くても、打てる手段がある」というのは 何物にも替え難い救いなのです。というより、そういう予想外の事態に対して エスケープポッドが備えられていない処理系を使うなんて恐くて出来ません。 Lisperは臆病なんです。
Ruby が安全なだけの言語でないのは間違いないと思う。