Rails 5.0.2 => 5.1.4 へのアップデートで地味に面倒くさかったこと
ActionController::Parameters がpermittedでないときに例外出すようになった
ちゃんと指定してpermitしたほうが良いんだろうけど、ログとかでpermitしなくても問題ない場面やpermit対象が動的に変わるものがある。 そういときに
params[:hoge].to_h
みたいなのはpermittedフラグが立ってないので例外が発生する。
params[:hoge].permit!.to_h # もしくは params[:hoge].to_unsafe_h
のようにする。
ActiveRecord::AttributeMethods::Dirty の一部仕様が変わった
save前後で呼び出す attribute methods が変わった。 https://github.com/rails/rails/pull/25337
使い分けは面倒だが、save前後での違いが明確になってスッキリした感じ。
リリースノートにないので注意。
save後
attribute_changed?
とかをsave後に利用することがdeprecatedとなった。
after_save コールバック内で呼び出したりするのも同じ。
たとえば以下のような感じに対応する。
attribute_changed?
=>saved_change_to_attribute?
attribute_was?
=>attribute_before_last_save
※ attribute method なので attribute
の部分をカラム名で置き換える。nameの場合は saved_change_to_name
のようになる。
save前
今までと同じ感じで使える。
ただ、save前用の新しいメソッドも生えてる。
たとえば attribute_changed?
に対しては will_save_change_to_attribute?
が使える。
新しい方は内部的な扱い方がちょっと違うけどやってることは同じっぽい。
create_table のプライマリキーがデフォルトBIGINTになった
ActiveRecord::Migration[5.1]
以降を継承しているマイグレーションが対象。
ActiveRecord::Migration[5.0]
以前のバージョンのマイグレーションはBIGINTにならないよう互換性の配慮がされている。
具体的には schema.rb
での create_table
のオプションに id: :serial
(pgの場合)が付くようになる。
5.0以前と以後のマイグレーションではプライマリキーの型で違いが出るようになってしまう。 これに対して2通りのアプローチがある。
- Migration[5.0] 以前で作ったプライマリキー(及びそれを参照する外部キー)も順次BIGINTにしていく
- Migration[5.1] 以降のマイグレーションではすべて
id: :integer
をつけるルールにする(デフォルトに従わない)
1 が順当そうで、このPR投げた人もおすすめしてる ( http://www.mccartie.com/2016/12/05/rails-5.1.html )。 BIGINTが必要にならない場合は 2 もありなんだけど、ルールを忘れられないようにするか、自動でなんかするかしたほうがいい。
BIGINTとINTが混ざった状態も許容するのも考えられるが、後々の負の遺産となりそうなのでどちらかに統一したほうがよさそう。
互換性の考慮漏れが一部ある(Rails 5.1.4)
ふつうに create_table
していれば前述したような互換性対策が走るので問題ないのだけど、一部考慮が漏れているところがある。
具体的にはプライマリキーを add_column :articles, :id, :primary_key
のようにして作ったとき、BIGINTになってしまう。
masterでは対応が入っている ようなので、Rails 5.1.5 では問題にならなさそうだけど、
一応それまでは db:migrate:reset
によるschema全更新ができないことは注意したい。
Webpacker 3.0 でのWebpackの設定カスタマイズ
Webpacker 3.0 以降ではWebpackの設定を直接書かずに、npmモジュールに定義されるデフォルトを上書きする形になった。 いい感じにデフォルトがそのまま使えて楽になった反面、すでに設定を結構いじったりしている場合にバージョンアップをするのがちょっと面倒だった。
カスタマイズの基本的な方法はドキュメントに書いてある。
https://github.com/rails/webpacker/blob/v3.0.1/docs/webpack.md
が、書いてあること以外で考えたことを以下にメモ。
環境ごとにPlugin設定の一部を上書き
// config/webpack/production.js const environment = require('./environment'); const UglifyJsPlugin = environment.plugins.get('UglifyJs'); UglifyJsPlugin.options.sourceMap = false; module.exports = environment.toWebpackConfig();
プラグインのoptionsオブジェクトに入っているのを上書きする。 Webpackプラグインの作りは理解してないけど、おそらくこのoptionsがあるという構造は統一されているのかな。
なお、この例ではプロダクションでソースマップを無効にしている。 (ソースマップ生成って結構パワー使うので)
一度プラグインを消しちゃって追加し直す方法もある。ぜんぜん違う設定にするときは楽かも。
参考: https://github.com/rails/webpacker/issues/771
environment.plugins.delete('UglifyJs') environment.plugins.set( 'UglifyJs', new UglifyJsPlugin({ sourceMap: false }) )
動作確認は NODE_ENV
環境変数を利用するとかんたん。
$ NODE_ENV=production bin/webpack
staging環境
npmパッケージからデフォルトで提供されるenvironmentsは、 development, test, production のみ。 それ以外の環境はまっさらなEnvironmentオブジェクトがそのまま渡ってくる。
https://github.com/rails/webpacker/blob/v3.0.1/package/index.js#L10
めんどうなのでproductionと同じにしたいのだけれども、デフォルトで参照するenvironmentは環境変数 NODE_ENV
で切り分けるため、NODE_ENV=production
で起動する以外なさそう。
とはいえstaging固有の設定もしたいわけで、ひとまず以下のようにproduction相当の設定をべた書きする形となった。
const environment = require('./environment'); const webpack = require('webpack'); const CompressionPlugin = require('compression-webpack-plugin'); environment.plugins.set('ModuleConcatenation', new webpack.optimize.ModuleConcatenationPlugin()); environment.plugins.set('UglifyJs', new webpack.optimize.UglifyJsPlugin({ sourceMap: true, compress: { warnings: false, }, output: { comments: false, }, })); environment.plugins.set('Compression', new CompressionPlugin({ asset: '[path].gz[query]', algorithm: 'gzip', test: /\.(js|css|html|json|ico|svg|eot|otf|ttf)$/, })); module.exports = environment.toWebpackConfig();
もうちょっとうまい感じの方法はないかな…。
redux-thunk で雑に getState するのは控えめにしたほうが良いかも
たとえばこういう Store があるとき。
const reducers = combineReducers({ foo: fooReducer, bar: barReducer }); const store = createStore( reducers, applyMiddleware(thunk) );
こんな感じの雑さで getState()
して現在の state を取っていたとする。
const fooAction = () => (dispatch, getState) => { const hoge = getState().foo.hoge; dispatch({ type: FOO, hoge }); };
ここでは combineReducers()
している通り、 getState().foo.hoge
と foo
キーを指定して書かねばならない。
なぜなら getState()
は全体の state を返すので。
でもこうすると特定の reducre に依存した Action Creator になってしまう。 最初のうちは良いかもしれないけど、combineReducers を修正するとか、他の文脈でその Action Creator 使うってときに困る。 つまり Action Creator の使い回しが難しくなる。 (他の文脈で Action Creator を使いまわすというのは気をつけたほうが良いかも。必要に応じて中身のビジネスロジックだけ切り出すほうが適切なこともある。知見ほしい)
こうならないためには Action Creator の呼び出し時に引数で受け取るとよさそう。
const fooAction = hoge => (dispatch, getState) => { dispatch({ type: FOO, hoge }) }; // Component const FooComponent = ({ hoge, fooAction }) => { const onClick = () => { fooAction(hoge) }; return <a onClick={onClick}>Foo</a>; ); export default connect( ({ foo }) => ({ hoge: foo.hoge }), dispatch => ({ fooAction: dispatch(fooAction) }) )(FooComponent);
getState を使うのは、どうしてもそうしないと state が取れないってときに割り切る?
どう Store を構成するかっていうのはなるべく Action Creator に入り込まないようにして、Container側で吸収したほうが良さそう。
React の Component 間通信について色々実験してみる
React さんのこといまいちよく理解していなかったので、コンポーネントのやり取りを題材に色々試してみました。 コード例は載っていますが思考実験的なのでかなり雑な点ご了承ください。
対象課題
たとえばモーダル表示切り替えを考える。 DOM構造は下記のようなイメージ。
main main-content modal-open-button modal modal-content modal-close-button
modal は全体に覆いかぶさる要素なので、CSS的に main 要素と同列の構造で扱いたい。 (ルートスタックコンテキストを適用したい)
modal-open-button により modal が開き、 modal-close-button により modal が閉じる。 main コンポーネント(もしくは modal-open-button コンポーネント)と modal コンポーネントの間での相互作用が必要になるが、これらは親子関係にないので props の受け渡しでは単純に表現できない。
SPA (Single Page Application) ではなく、サーバサイドのテンプレートエンジンで render した結果に対してJS制御を付与する。 つまりサーバーサイドのテンプレートエンジンの実行と局所的な React コンポーネントが共存する環境を想定する。
親コンポーネントを作成
大本の親(コンテナ)コンポーネントを作り、その子コンポーネントとして含めるアイデア。
container main modal-open-button modal modal-close-button
container に modal の状態を持たせ、 main や modal コンポーネント内でその状態を変えるコールバックを実行し伝播する形になる。
おそらく最もポピュラーな方法だが、コンポーネントツリーが深くなってくると問題になる。 何をするにしてもルートコンポーネントを辿って反映しないといけない。
また今回の例では他の問題もある。
これらは、SPAのような全体をReactコンポーネントを組み合わせたアプリケーションなら問題なさそう。
Flux パターン
有名なやつ。 実装としては Flux や Redux などがある。 公式ドキュメントやサンプルが異様にしっかりしているのと、情報もたくさんあるのでここでは省略。
Flux と Redux 両方試してみた感触としては、Redux を react-redux なしで使い始めるのが最初はわかりやすさの面で良いかも?と思った。 (react-redux はパフォーマンス最適化されてるので必要性が増えてきたら積極的に導入すると良さそう)
Observer パターン
Flux パターンに乗っておけばとりあえずは大丈夫そうだけど、シンプルな用途には大げさ感がある。 そういうときは最小限のエッセンスだけを真似してみても良い。
Flux は Observer パターンと、 PubSub パターンのようなメッセージパッシングを組み合わせて、単方向データフローとなるように整えた感じのやつと考えられそう。 状態を保持する Store とコンポーネントの View を分け、その間を Observer パターンで通信することだけを真似してみる。
下記がその実装例。 各 Component が Observer で、 Store が Observable (Subject) にあたる。
import React from 'react'; import ReactDOM from 'react-dom'; import classNames from 'classnames'; // Observable class ModalStore { constructor() { this.state = { active: false }; this.observers = []; } activate() { this.state.active = true; this.notify(this.state); } deactivate() { this.state.active = false; this.notify(this.state); } registerObserver(observer) { this.observers.push(observer); } notify(state) { // TODO: register でコールバックを登録するように実装すると依存が減らせる this.observers.forEach(l => { l.setState({ active: state.active }) }) } } class Modal extends React.Component { constructor(props) { super(props); this.handleClose = this.handleClose.bind(this); this.state = { active: props.active } } componentDidMount() { this.props.store.registerObserver(this); } render() { const modalClass = classNames( 'modal', { 'modal_active': this.state.active, } ); return ( <div> <div className={ modalClass }> <div className='modal__text'> <div>active: { `${this.state.active}` }</div> </div> <div className='modal__close' onClick={this.handleClose}> [x] close </div> </div> </div> ) } handleClose(e) { this.props.store.deactivate(); } static get defaultProps() { return { active: false } } } const ModalActivator = (props) => { const handleOpen = (e) => { props.store.activate(); } return ( <div className='modalFull__open' onClick={handleOpen}> [x] open </div> ) }; document.addEventListener('DOMContentLoaded', () => { const modalStore = new ModalStore(); ReactDOM.render( <Modal store={modalStore} />, document.body.appendChild(document.createElement('div')) ); ReactDOM.render( <ModalActivator store={modalStore} />, document.getElementById('main') ); });
EventEmitter を使った Observer パターン
Observer パターンを自前で用意するよりは EventEmitter みたいなのを使っても良い。
https://www.npmjs.com/package/wolfy87-eventemitter
import EventEmitter from 'wolfy87-eventemitter'; class ModalStore extends EventEmitter { constructor() { super(); this.state = { active: false }; } activate() { this.state.active = true; this.emitChange(); } deactivate() { this.state.active = false; this.emitChange(); } registerObserver(observer) { this.on('change', () => { observer.setState({ active: this.state.active }); }); } emitChange() { this.emit('change'); } }
表現力が上がったほか、手段が統制された。
なお、 registerObserver(observer)
のかわりに addChangeListener(callback)
のようにすると、呼び出し側への依存を減らせる。
Mediator パターン
これまでの例には Flux と違ってメッセージ配送を制御する Action や Dispatcher がないので、 Store - Component 間が複雑(多対多とか)になるとコードの治安が悪くなるかもしれない。 そのときは Mediator パターンを使ってもよさそう。というわけで実験。
class ModalStore { constructor() { this.state = { active: false }; } activate() { this.state.active = true; } deactivate() { this.state.active = false; } } class ModalMediator { constructor(store) { this.stores = [store]; // 雑に多対多を想定してみる this.listeners = []; } activate() { this.stores.forEach(s => { s.activate(); }); this.notify({ active: true }); } deactivate() { this.stores.forEach(s => { s.deactivate(); }); this.notify({ active: false }); } register(listener) { this.listeners.push(listener); } notify(state) { this.listeners.forEach(l => { l.setState({ active: state.active }) }) } } class Modal extends React.Component { componentDidMount() { this.props.mediator.register(this); } handleClose(e) { this.props.mediator.deactivate(); } // ... } const ModalActivator = (props) => { const handleOpen = (e) => { props.mediator.activate(); } // ... }; document.addEventListener('DOMContentLoaded', () => { const modalStore = new ModalStore(); const modalMediator = new ModalMediator(modalStore); ReactDOM.render( <Modal mediator={modalMediator} />, document.body.appendChild(document.createElement('div')) ); ReactDOM.render( <ModalActivator mediator={modalMediator} />, document.getElementById('main') ); });
うーん、この例ではあまり必要性が実感できない…。 非同期処理とか複数 Store の順序依存とかが出てくるとメリットがわかりやすそう。 そうなる前に Flux, Redux に移れという話になるのかな…。
感想
上記はいづれも粗い実装ではありますが、最低限PDSは守られているので Flux に乗せるのも簡単のはずです。 というか複雑性に対処しようとしていろいろやっていくと Flux に近づいていく気がします。
では最初から Flux でいいのかというとそうでもなくて、状況(複雑性、リソース、スケジュール、コードの寿命)に合わせて引き出しをいくつか持っておくと良さそうです。
そしていろいろ試した結果、こういう一部だけコンポーネントを使うようなケースには、 React じゃなくて Vue 等のほうが向いているな、と思ったのでした…。 次は Vuex を触ってみよう。
参考
明日から使えるテスト技法をざっくり社内で紹介した
弊社では月一で帰社日という全社員が集まるイベントがあります。
そこで有志が発表する枠があるのですが、そのなかでも「基調講演」と銘打ってまあちょっとだけ気合い入れて発表するコーナーがあるのですね。それが今月は僕の番ということで、テスト技法についてざっくり発表してきました。 以下がそのときの資料です。
…正直ざっくりとはいえ結構ボリュームあって規定の時間を大幅にオーバーして喋ってた気がします。
テスト技法と一言に言ってもかなり広くて奥が深い世界で、どこをどう説明しようか悩みました。主に仕事で使いやすいところを選んでみたつもりです。一見すると知ってるよ、という知識かもしれませんがちょっと踏み込むと新しいことが知れてたのしいです。
なんで今テスト技法なの、と思われたかもしれません。 テストってなんか地味なイメージあるけどやっぱりソフトウェアと切っては切れない関係なわけで、どうせテストするならしっかり基礎は押さえておきたいと思って、最近勉強していたのです。 鉄は熱いうちにということで、こうして発表ネタにしたというわけですね。そのかいあって自分自身学んだ知識や不明点が整理できて良かったです。
リソースの制約があってテストは手を抜かざるを得ない場面というのに出会うこともある思うのですが、手を抜くにしても分かった上でやるのとそうでないのとでは違いますよね。どう手を抜くの、どこにリスクを置いてどうカバーするのみたいなことをきちんと考えていきたいものだなーと思っています。
なお
弊社では一緒に働いてくれる仲間を募集中です!
native extension に openssl を使う gem のビルドに失敗するときの対処法
homebrew で openssl をインストールした場合、今までだとbrew link
するだけで特に問題なく使えていたが、最近ではbrew link
ができなくなった。
$ brew link openssl --force Warning: Refusing to link: openssl Linking keg-only openssl means you may end up linking against the insecure, deprecated system OpenSSL while using the headers from Homebrew's openssl. Instead, pass the full include/library paths to your compiler e.g.: -I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib
その結果、 native extension のビルドに openssl を使う gem が正しく include できなくなった。 なので次のように設定した。 (参考: rbenv/ruby-build: Compile and install Ruby)
$ cat ~/.zshrc if [ $(uname) = "Darwin" ]; then export CONFIGURE_OPTS="--with-opt-dir=`brew --prefix openssl`" export RUBY_CONFIGURE_OPTS="--with-opt-dir=`brew --prefix openssl`" fi $ source ~/.zshrc
それでruby再ビルドしたらいけた。
Rails 5.0.1 の変更点で個人的に気になったところ雑まとめ
先日12/21に Rails 5.0.1 が出ましたね。 CHANGELOG をざっと読みながら、個人的に気になったところを雑にまとめます。 雑まとめなので間違ってるかもしれません。正確な情報はリンク先を見てください〜。
3行で
ActionCable
https://github.com/rails/rails/blob/v5.0.1/actioncable/CHANGELOG.md
- Buffer writes to websocket connections, to avoid blocking threads that could be doing more useful things.
- 遅いクライアントからのリクエストでブロックされてしまってDoS攻撃とかに脆弱性があったのを対応
- https://blog.phusion.nl/2016/12/21/actioncable-under-stress-protecting-your-application-against-slow-clients-using-passenger/
ActionPack
https://github.com/rails/rails/blob/v5.0.1/actionpack/CHANGELOG.md
- Add ActionController::Parameters#merge!, which behaves the same as Hash#merge!.
- Added ActionController::Parameters#deep_dup which actually creates a params copy, instead of refereing to old references in params.
- Add to_param to ActionController::Parameters deprecations.
- AC::Parameters がHash継承しなくなった影響でto_paramsが従来の期待通りに動かなくなった(AC::Parametersを返す)
to_h
してからto_param
使ってくれとのこと- https://github.com/rails/rails/pull/26328
- SSL: Changes redirect behavior for all non-GET and non-HEAD requests
- Deprecated omitting the route path. Specify the path with a String or a Symbol instead.
- 内部実装の都合でこうなったっぽい?
- https://github.com/rails/rails/pull/25693
- Added new ActionDispatch::DebugLocks middleware that can be used to diagnose deadlocks in the autoload interlock
- ロックを検出できるミドルウェアができた
- Pumaがデフォルトになったので必要性が増えたっぽい
- もしデッドロックっぽいのが発生したら、ミドルウェアスタックにこいつを追加して
/rails/locks
でスレッド情報が見れる - https://github.com/rails/rails/pull/25344
ActionView
https://github.com/rails/rails/blob/v5.0.1/actionview/CHANGELOG.md
- Render now accepts any keys for locals, including reserved words
- renderに予約語もlocalとして渡せるようになった。
local_assigns[:class]
のようにして参照する
- renderに予約語もlocalとして渡せるようになった。
- Changed partial rendering with a collection to allow collections which implement to_a.
- Collection Rendering として Enumerable ならなんでも渡せるようになった
ActiveJob
https://github.com/rails/rails/blob/v5.0.1/activejob/CHANGELOG.md
- Added instance variable @queue to JobWrapper.
- rescue-scheduler への対応だけど、もしかしたらsidekiqとかでも便利になる?
ActiveRecord
https://github.com/rails/rails/blob/v5.0.1/activerecord/CHANGELOG.md
- バグフィックスたくさん。特に複数スレッドでうまく動くようにしてるのが多い(クエリキャッシュとかコネクションプーリングとか)
- Introduce Model#reload_
to bring back the behavior of Article.category(true) where category is a singular association. article.reload_category
のようにして特定のhas_one関連だけリロードできるようになった- 今までhas_one関連については
article.category(true)
でリロードできてたけど、それがRails5でdeprecatedになっていた。article.reload.category
では完全な代替にはならない(未保存の他の関連インスタンスの変更が消えるとか)という背景があった - https://github.com/rails/rails/pull/27133
- Always store errors details information with symbols.
- autosaveでバリデーションエラーが発生したときに保存するためのkeyをSymbolにした
- いままではStringだったけどそれだとエラーを保存する他の処理でSymbolにしているのでずれてて扱いづらい
- 場合によっては breaking change になるかもだけどほとんどのケースは大丈夫かな
- https://github.com/rails/rails/pull/26552
- Avoid loading records from database when they are already loaded using the pluck method on a collection.
- 地味に嬉しい。reload考慮する必要ありそう?
- Doing count on relations that contain LEFT OUTER JOIN Arel node no longer force a DISTINCT. This solves issues when using count after a left_joins.
- むしろいままでDISTINCTだったのか
ActiveSupport
https://github.com/rails/rails/blob/v5.0.1/activesupport/CHANGELOG.md
- サマータイム系のバグフィックスが多い
- Fix
thread_mattr_accessor
subclass no longer overwrites parent.- 親と子で別々に属性持てるようになった
Railtile
https://github.com/rails/rails/blob/v5.0.1/railties/CHANGELOG.md
- バグフィックスだけ