- 投稿日:2019-12-05T23:38:36+09:00
【Rails】Rails⇔JavaScript間で時間データを渡す方法
【はじめに】
先日、こちらの記事を書きました。
【Rails】Rails側で定義した変数をJavaScriptに簡単に渡せるgem 「gon」を使ってみた - Qiitaただ、時間関係の変数についてはフォーマットがRubyとJavaScriptで異なってしまい、うまく動きません。
そこでRails⇔JavaScript間で時間データを渡す変換方法を調べたのでまとめてみました
【この記事が役に立つ方】
- Rails⇔JavaScript間で時間データをやりとりしたい方
【この記事のメリット】
- Rails⇔JavaScript間で時間データをやりとり出来るようになる。
【環境】
- macOS Catalina 10.15.1
- zsh: 5.7.1
- Ruby: 2.6.5
- Rails: 5.2.4
【ポイント】
1000を掛ける、割るで調整する。
【変換方法】
1.Ruby → JavaScript
t1 = Time.now => 2019-12-05 14:11:03 +0000 t1.to_f * 1000 => 1575555063351.479Ruby側で
to_f
メソッドを使い、1000を掛けることでミリ秒に変換。
↕
JavaScript側でnew Date()
の引数にそのミリ秒を渡す。let d1 = new Date(1575555063351.479) d1 // Thu Dec 05 2019 23:11:03 GMT+0900 (日本標準時)
2.JavaScript → Ruby
let d2 = new Date() d2 // Thu Dec 05 2019 23:20:04 GMT+0900 (日本標準時) d2.getTime() / 1000 // 1575555604.804JavaScript側で
getTime()
で拾った数値を1000で割る。
↕
Ruby側でTime.at()
の引数にその数値を渡す。Time.at(1575555604.804) => 2019-12-05 14:20:04 +0000【参考】公式リファレンスより
【Ruby】 Timeオブジェクト
秒
単位Time オブジェクトは時刻を起算時からの経過秒数で保持しています。起算時は協定世界時(UTC、もしくはその旧称から GMT とも表記されます) の 1970年1月1日午前0時です。なお、うるう秒を勘定するかどうかはシステムによります。
class Time (Ruby 2.6.0 リファレンスマニュアル)
【JavaScript】 Dateオブジェクト
ミリ秒
単位日付や時刻を扱うことが可能な、JavaScript の Date インスタンスを生成します。Date オブジェクトは、1970 年 1 月 1 日 (UTC) から始まるミリ秒単位の時刻値を基準としています。
【おわりに】
最後まで読んで頂きありがとうございました
特にJavaScriptと他言語は頻繁に関わってくると思いますが、言語間でエラーが出たらそもそも前提が違うという視点を持って対処していかないといけないですね
【参考にさせて頂いたサイト】(いつもありがとうございます)
- 投稿日:2019-12-05T22:57:31+09:00
現場で役に立つ!最小限の修正で仕様変更に対応する方法(Ruby)
この記事は Ruby Advent Calendar 2019 第6日目の記事です。
時間がないのにどうしても仕様変更しなくてはならないとき、どんな考え方で対応しますか?ってお話です。
はじめに
この記事では、主に大規模Webアプリケーションの開発に於いて、新規開発ではなく修正や拡張を行うシーンを想定して、無駄な工数をなるべく削減すべく自分なりに考えて実践しているベストプラクティスを書いている。
要は、時間優先で修正にあたるとき、実際に自分が頭の中で考えていることを分かりやすく書いた、みたいな内容だ。
また、紹介するコードはRubyに特化したものになっているが、概念としては特定のプログラミング言語に依存しない、考え方そのものについて書いているつもりだ。
対象とする読者層
対象とする読者は、プログラミング経験1年以上、設計工程よりも実装工程をメインに担当していて、コーディングしてテストしての毎日に追われている若手プログラマ、オブジェクト指向の言葉は知ってるけど実際には現場で使いこなせてない感が残る、初級から中級へステップアップしようとしているエンジニア、俗に言う IT土方 、といったところか。
特に、オブジェクト指向やアーキテクチャに関する書籍を読んで何となく理解したけども、実際に仕事でそれを活用できていない、具体的な実装例を見てみたい、といった人をターゲットとしている。
この記事を読んで「なるほど、そうすればいいのか」といった気づきが得られれば幸いだ。
コードベースの紹介
では始めよう。
とある大規模システムにて、何らかの外部APIの呼び出しをラップするメソッドがあったとしよう。
ソースコードは以下のような感じだ。
the_api.rbclass TheApi # APIを呼び出す # @return [String] 正常なら 'success'、エラーなら 'fail' を返す def self.call(param1, param2) ret = 'success' api = ApiClient.get_instance # 外部APIを呼び出すような処理を想定 api.call [param1, param2] # 処理結果は last_error で得られる。0 以外ならエラー。 if api.last_error != 0 ret = 'fail' end ret end end戻り値は
String
で、呼び出し側で成功か失敗かの判定を行なっている。呼び出し側# API呼び出し ret = TheApi.call(product_id, base_date) if ret == 'fail' # エラー発生。 raise 'API呼び出しでエラー発生' end外部API自体の詳細は不明で、
ApiClient
クラスはサードパーティ製であり自由に修正できない部分、ブラックボックスであるとしよう。システム全体では、上記のようにこのメソッドを利用している箇所が 50箇所ほど あるとする。どうやらコピペして作られたようだ。
コードの善し悪しはさておき、とりあえずこの様な状態のコードベースがあり、あなたはこのシステムの仕様変更を担当することになった。
仕様変更お願いします!
チームリーダーより、仕様変更の依頼がきた。
仕様変更の概要
- 外部APIの呼び出しがタイムアウトした場合に、リトライする様にしたい。
- ただし、50箇所ある呼び出し元のうち、まずは1箇所だけでタイムアウトの対応をいれたい。将来的には10箇所ぐらいまで増えるかも。
- 外部APIの仕様書によると、
api.lasr_error
が3
の場合に、タイムアウトを意味すると書かれている。- チームリーダー「やり方は任せるから、いい感じの実装を頼むよ。でも時間は余りない。 今日中に頼む 」
なるほど、なるほど。
要件自体は簡単なハナシだ。以下のようなイメージだ。
修正イメージclass TheApi # APIを呼び出す # @return [String] 正常なら 'success'、エラーなら 'fail'、 # タイムアウトなら 'timeout' を返す def self.call(param1, param2) # 省略 if api.last_error == 3 ret = 'timeout' elsif api.last_error != 0 ret = 'fail' end ret end end # 呼び出し側 ret = TheApi.call(product_id, base_date) #if ret == 'fail' # 判定方法を変更 if ret != 'success' # エラー発生。 if ret == 'timeout' # TODO: タイムアウトなのでリトライする! end raise 'API呼び出しでエラー発生' endよっしゃ、イッチョウあがり!!
いや、待て待て。
このメソッドの呼び出し箇所は他にもあと 49箇所 あるぞ。
現状では、タイムアウトだろうと他のエラーだろうと全部
'fail'
を返している。呼び出し側では
ret == 'fail'
で判定しているため、タイムアウトなら時に'timeout'
を返してしまうと、エラーとして処理されなくなくなってしまう(以下の部分)。[再掲]呼び出し側# API呼び出し ret = TheApi.call(product_id, base_date) if ret == 'fail' # 戻り値を変更するとここを全部変更しないといけない # エラー発生。 raise 'API呼び出しでエラー発生' endさて、どうしたら良いだろうか。
ちょっと考えてみてほしい。
まぁ、正攻法でいくなら、戻り値を
String
ではなく適切なクラスに変更するべきだろうな。
処理の結果をちゃんと表現できるようなクラスを定義してそのインスタンスを返す、とか。でも 「時間は余りない、今日中に頼む」 てことなので、そこまでやる時間がなさそうだ。
戻り値を変更してしまうと、50箇所を全て変更しないといけない。
とーぜん、変更した箇所は全てテスト(動作確認)しないといけない。これ、アカンやつやん。。。
おっと書き忘れていたが、 現在の時刻はなんと夜の9時 だ。
働き方改革とかゆーやつは一体何やったんや。笑
やばい、今日は帰れないかも!
こんな時間に急ぎのタスクを振ってくるなんて、エゲツない話だと愚痴の一つでも言いたくなるが、他のメンバーも全員残ってるし作業を振られているようだ。何かシステムトラブルでもあったのだろうか。
とにかく今は解決策を考えよう。
文句を言うのは任された仕事をやり遂げてからだ。
修正案1 グローバル変数を使う
メソッドの呼び出し箇所を変更せずに対応する方法としてまず思いつくのは、引数や戻り値とは別のところに結果を代入する方法だ。
the_api.rbif api.last_error == 3 $is_timeout = true else $is_timeout = false endこんな感じにグローバル変数としてフラグを作ってしまって、タイムアウトかどうかで真偽値をセットし、呼び出し元から参照すれば良い。
めちゃくちゃ簡単に対応できそう!よっしゃー!
ごめんなさい。
ウソです。冗談です。
実際、例えばC言語の組み込み系プログラムでもない限り、今どきのWebシステムで現実にこんな事するヤツいたら 一発レッドカード だろう1。
良い子は真似しないように。
グローバル変数とまではいかなくとも、Webならセッション変数に入れたり、せめて
Thread.current
を使ってスレッドセーフにしたいところだが、それでもどこからでもアクセス出来る変数という点では本質的に同じなので、極力使うべきでない。この方法は超簡単な反面、ソフトウェアの構造を一瞬で
ウンコに複雑にしてしまう諸刃の剣であり、 最後の手段 、ぐらいに捉えておこう。修正案2 引数追加
次はちゃんとした案です。
引数を追加し、デフォルト値を設定しておけば、呼び出し元を変更せずに済むよね。
the_api.rbclass TheApi # APIを呼び出す # @param param1 # @param param1 # @param detect_timeout [Boolean] タイムアウトを検出するかかどうか # @return 正常終了: 'success' # エラー時: 'fail' # タイムアウト時: 'timeout' ただし detect_timeout に true を指定した場合のみ def self.call(param1, param2 detect_timeout = false) ret = 'success' api = ApiClient.get_instance api.call [param1, param2] if detect_timeout && api.last_error == 3 ret = 'timeout' elsif api.last_error != 0 ret = 'fail' end ret end end簡単で分かりやすい方法だ。
この考え方は Ruby だけでなく、他のメジャーな言語でも使える一般的な方法なので、 必ず覚えておくべきイディオム と言ってもいいだろう。
しかし何らかの理由でこの方法が適用できない場合もある。
引数が既にオプション引数だらけで、追加したくない場合や、可変長引数になっていて特別な意味を持つ引数を追加できない場合だ。
可変長引数の例# @param APIに渡すパラメータ def call(*params) # 省略 end無理やり追加できなくもないが、スマートではない。できればやりたくない。
こういったケースにもスマートに対応できる方法はないだろうか。
※ここからは
call()
メソッドの引数が可変長だった場合について考察していく修正案3 ブロックを渡してエラー処理の仕方を追加する
引数を変更しない方法として、Ruby ならではのブロックを活用する方法を考えてみよう2。
the_api.rbclass TheApi # APIを呼び出す # ブロックを渡した場合は、エラーコードを引数にブロックが呼ばれる # @param params [Array] APIに渡すパラメータ # @return 正常終了: 'success' # エラー時: 'fail' def self.call(*params) ret = 'success' api = ApiClient.get_instance api.call *params # 処理結果は last_error で得られる if api.last_error != 0 ret = 'fail' end if block_given? # ブロックがあるときはエラーコードを引数にして呼び出す yield api.last_error end ret end end呼び出し側timeout_flag = false # タイムアウトフラグ ret = TheApi.call(product_id, base_date) do |error_code| if error_code == 3 timeout_flag = true # フラグを立てる end end if timeout_flag # TODO: タイムアウトなのでリトライする elsif ret == 'fail' # エラー発生。 raise 'API呼び出しでエラー発生' endどうだろう。
ちょっとムリヤリ感があるかな〜
引数を追加せずに済んだが、デメリットも多い。
- 呼び出し側ではエラー処理の方法が2つになってしまい、複雑度が上がっている
- TheApiクラスはせっかくAPIの処理をラップしていたのに、崩れてしまっている。呼び出し元がAPIの戻り値の仕様を知っている必要があり、開放閉鎖の原則に反している
- 今後、もっと適切な方法でブロックを使いたくなった時に、この仕組みをコールバック関数で置き換えるとなると相当な変更量になってしまう
今回は却下かな〜
もっといい方法ないですかね。あ、せや、終電の時間も一応調べておこう。
修正案4 戻り値に特異メソッドを付与
これも Ruby ならではの方法。
Java や C# などの静的型付け言語ではこんな発想自体あり得ない3。the_api.rbclass TheApi # APIを呼び出す # @param params [Array] APIに渡すパラメータ # @return [String] 正常終了: 'success' # エラー時: 'fail' さらに is_timeout? という特異メソッドが定義されています def self.call(*params) ret = 'success' api = ApiClient.get_instance api.call params # 処理結果は last_error で得られる if api.last_error != 0 ret = 'fail' # タイムアウト対応のための特異メソッドを定義 ret.define_singleton_method(:is_timeout?) do api.last_error == 3 end end ret end end呼び出し側ret = TheApi.call(product_id, base_date) if ret == 'fail' # エラー発生。 if ret.is_timeout? # TODO: タイムアウトなのでリトライする end raise 'API呼び出しでエラー発生' endこれなら、呼び出し元のエラー処理が分散することもないし、APIの仕様ラップをキープ出来ているし、引数やブロックの追加も必要ない。
変更量も少ないし、何より、呼び出し元の
ret.is_timeout?
が非常にシンプルで分かりやすい!4やはり処理の結果は戻り値で戻すのが自然に感じるし、コールバックや出力用の引数で渡すのは言語仕様上 仕方ないとしてもやはり違和感が残る。
これぞ Ruby の柔軟性って感じ!5
ただし乱用は禁物。俗に言う「黒魔術的」な手法なので、慣れていない人6が見ると気持ち悪く見える。僕もあまり使う機会は少ないが、実際にはハッシュに要素を追加するぐらいの(JavaScript的な)感覚なので、そこまで弊害はなさそうに思う(初めて見たという人は感想をコメントください)。
修正案5 別のメソッドを追加する
現状の
TheApi.call
はそのまま置いといて、TheApi.call_ex
みたいな、別のメソッドを作ってしまって、そっちで新しい仕様に対応しようという考え方。the_api.rbclass TheApi # APIを呼び出す(詳細なエラーを返すバージョン) # @param params [Array] APIに渡すパラメータ # @return [Integer] 0: 正常終了、それ以外はエラー(3: タイムアウト) def self.call_ex(*params) api = ApiClient.get_instance api.call *params # last_error をそのまま返す api.last_error end # APIを呼び出す(従来バージョン) # @param params [Array] APIに渡すパラメータ # @return [String] 正常終了: 'success' # エラー時: 'fail' def self.call(*params) # 残り時間によっては実装は変更しないままいくケースも考えられるが、 # これぐらいなら頑張ってリファクタリングしてほしい。 # 最適化するならこんな感じかな〜 call_ex(*params) == 0 ? 'success' : 'fail' end endオブジェクト指向設計としては、個人的にはこれが一番良さそうに思うが、メリットデメリットを整理してみよう。
メリット
- 既存のメソッドを残しつつ、新しい仕様にも対応できている。
- 今後タイムアウト以外のエラーコードにも対応したくなった時でも変更しなくて良い。ただし、そんな事が実際あるのかどうかは未確認。もしかしたらこのAPIは廃止になるかもしれない。
デメリット
- 呼び出し元でどちらのメソッドを使うべきか意識して使い分ける必要がある。考慮しないといけないことが一つ増えている。
- 外部APIの仕様をラップしていたはずが、崩れてしまっている。すなわち、APIの結果がどうだったか判定するには外部APIの仕様を分かっている必要がある。対策としては、呼び出し側がAPIの結果を表す専用のクラスを定義(下記)し、それを戻り値にすれば更に良くなりそうたが、そこまでやる時間があるかどうか。
the_api_result.rb# TheApi の呼び出し結果を表す class TheApiResult attr_reader :error_code def initialize(error_code) @error_code = error_code end def fail? @error_code != 0 end def timeout? @error_code == 3 end def success? !fail? end end時間とのトレードオフとなると、この案は意外にも中途半端な評価になるように思う。
ガッツリとリファクタリングするなら、もっと考慮すべき事(クラスメソッドをやめて、継承可能にする、テストコードを書くなど)がたくさんあるし、時間のあるときによく考えて設計したいところ。
結論
今回の状況では、修正案4 の特異メソッドでの対応が最もお手軽で、バランスの取れた方法と判断。
これで今日もなんとか家に帰れそうだぞ!
今後もしリファクタリングするにしても、以下の様なことを確認してから検討すべきだな。
- このAPIの利用は今後も続くのかどうか
- 続く見込みなら、さらに仕様変更がありそうかどうか
- ありそうなら、どんな変更を想定すべきか、どの程度か
まとめ
今回のように、(とくに大規模な開発では)純粋な設計の良し悪しだけでなく時間的な制約やチームの状況によって最適解は異なる。
本来であれば、APIの結果を
'success'
'fail'
で返すなんて稚拙な設計を改めるべきであるが、現場ではそうも言ってられない7。新規に設計する仕事よりも、既に構築されて運用中のシステムがあり、設計がイケてなくても、それを修正していくような仕事の方が圧倒的に多いのだ。
良くないコードをキレイにしたい気持ちはよく分かるが、それには時間(工数)がかかる。
つまり 費用対効果 だ。
今回の例ではほんの数行の小さなメソッドで、仕様変更の内容もたかが知れているが、実際の開発の現場ではもっと大変なことがいっぱい起こっている。そんな事ないという人は、気づいていないだけだ。本当はもっと様々な、ディスプレイの中と外の両方の状況を考慮してより良い方法を探し続けていくべきなんだ。
規模が小さいソフトウェアであっても、将来的に大きくなるのを見越しているなら考え方は同じ。
安易な考えでガチャガチャ修正しているようでは、家に帰れません。笑
ちょっとした変更であっても、複数の修正案を出して検討し(できれば誰かに相談し)、最もバランスのとれた解をチーム全体で模索することが大切だ。より良い実装で、より早く仕事が終わるのなら絶対そっちのほうがいい。
ソフトウェア開発のお仕事とは、 そういった状況判断の連続 なのだ8。
現場からは以上だ。お疲れ様でした!!
現実にはこれぐらいで出入り禁止になることはないだろうけど、要はそれぐらいヤバイ方法ってことだ。 ↩
Ruby 以外の言語ではコールバック関数を引数に追加するようなイメージが近い。 ↩
僕も実際にはじめてみたときは衝撃的すぎて度肝を抜かれた。笑 ↩
冒頭の修正イメージで掲載したコードに一番近い。僕は「こんな風に書けたらいいな」という発想で解決策を模索することが多いと思う。Ruby ならそれが実現しやすい。 ↩
この特異メソッドの使い方を紹介したくてこの記事を書いたのだ! ↩
特に Java や C# などの静的型付け言語から来た人 ↩
Ruby で開発しているようなハイスキルエンジニアならこんなことあり得ないのかもしれないが。 ↩
将棋や麻雀などのゲームをやっているときの思考に近いように思う。サッカーも近いのかも。下手くそだけど。 ↩
- 投稿日:2019-12-05T22:36:05+09:00
roo gemを使ってExcelからデータを読み込む
こんにちは、マイナビでエンジニア兼マネージャーをしている柴垣と申します。
マイナビ学生の窓口というメディアを担当しています。きっかけ
マイナビ学生の窓口というメディアは大学生に対してキャリアのきっかけを届ける記事がたくさんあります。
ある日ディレクターから、「記事のカテゴリを更新して」というチケットが届きました。
対象の記事はidで指定されていて、現在のカテゴリ、変更後のカテゴリが記載されていました。
その数、約5,000!
最初は気づかずに、1行ずつ、手作業で、マッピング用のHashを作っていましたが、その数に気づいた時、これはあかん!となり、excelをcsvに変換してマッピング用のHashを作るtaskを作成しようと思っていました。
そこへ、あるアドバイスが
「xlsxなどから読み込んで処理するなら roo gem を使えば、ある程度自動化できそうです。」
ありがとうございます!rooを使う
rooとは
https://github.com/roo-rb/roo
Excelやその他類似のファイルを読み込むためのgemですインストール
githubのREADMEにあるようにGemfileに追記します
Gemfilegem 'roo', '~>2.8.0'bundle installします。
現時点では2.8.2がインストールされましたExcelファイルを読み込む
ではExcelファイルを読み込みます。読み込むExcelファイルを
new
の引数に指定します。load_excel.rakeexcel = Roo::Excelx.new('Excelのファイルパス')Sheetを指定する
Sheetの指定は先ほど作成した
Roo::Excelx
のインスタンスにsheet
メソッドで指定します。load_excel.rakesheet = excel.sheet('Sheet名')指定したカラムのデータを読み込む
Sheetを指定したら、データを読み込みます。
データを読み込むメソッドはいくつかあるようですが、わたしはparse
メソッドがオススメです。
ヘッダーも取り除かれます(必要な場合はheaders:true
を指定)、取得したいカラムの文字列を指定するだけです。
ExcelのSheetが以下のようになっていたとします。
id old_category_name new_category_name 1 カテゴリー1 カテゴリー2 2 カテゴリー1 カテゴリー3 そのときの指定方法は以下のようになります。
load_excel.rakerows = sheet.parse(id: 'id', old_cname: 'old_category_name', new_cname: 'new_category_name')rowsを出力することで正しく読み込めていることがわかります
load_excel.rakerows.each { |row| puts row.inspect } => {:id=>1, :old_cname=>"カテゴリー1", :new_cname=>"カテゴリー2"} {:id=>2, :old_cname=>"カテゴリー1", :new_cname=>"カテゴリー3"}とてもカンタンですね!
番外編
今回はカテゴリの更新ということで、oldとnewが同じ記事のIDは配列にまとめる必要がありました
例えば
id old_category_name new_category_name 1 カテゴリー1 カテゴリー2 2 カテゴリー1 カテゴリー3 3 カテゴリー1 カテゴリー2 4 カテゴリー1 カテゴリー2 5 カテゴリー1 カテゴリー3 となっていた場合、期待する結果は以下のようになります。
[ {old_cname: 'カテゴリー1', new_cname: 'カテゴリー2', ids: [1, 3, 4]}, {old_cname: 'カテゴリー1', new_cname: 'カテゴリー3', ids: [2, 5]} ]最初は、
Array#find
を使って、old_cname
,new_cname
が結果の配列に存在していたらids
に追加という処理にしていました。
すると、レビュー時にまた神の声が聞こえてきました。基本的にはコンテナオブジェクト(ArrayやHashなど)はHashのキーにしない方が良いのですが、ここは Array#find ではなく〜(略)
教えていただいた内容は
old_cname
とnew_cname
の配列をHashのkeyにして、そのvalueにidを突っ込んでいけばよいということでした。そのために、まずHashのデフォルト値を指定します
output = Hash.new{|h, k| h[k] = []}こうすることで、デフォルト値が
[]
になりました
あとはもう突っ込んでいくだけですrows.each do |row| output[[row[:old_cname], row[:new_cname]]] << row[:id] end ouptput => {["カテゴリー1", "カテゴリー2"]=>[1, 3, 4], ["カテゴリー1", "カテゴリー3"]=>[2, 5]}結果がHashになったことにより、カテゴリーの取得は少し工夫が必要で
output.each.each do |(old_cname, new_cname), ids| endとすることで
old_cname
,new_cname
,ids
それぞれの変数に代入ができます
- 投稿日:2019-12-05T22:25:07+09:00
Rails 初学者のアソシエーションの解説
has_manyとかいまいちわからない人向け!
これがわかる人は不要 Rails ガイド
1.has_manyとbelongs_toはセットではない。一緒に使うことが多いが独立して使うことができる。
2.メソッドを理解することが重要!例えば
Author(著者)テーブルとBook(本)テーブルがあったとし、その二つを関連づけたい時。class Author < ApplicationRecord
has_many :books, dependent: :destroy
endclass Book < ApplicationRecord
belongs_to :author
end上記の記述で下記のような連携ができるらしいのだが、いまいちどのような仕組みなのかわからなかった、、
@book = @author.books.create(published_at: Time.now)
こう言う時は一つづつ分解して考えるといい
authorはオブジェクト
booksはメソッド
createもメソッドcreateがメソッドなのはなんとなくわかるのだが、booksは??
もしhas_many: booksがなければメソッドにはならない。has_many: booksを追加したことによりbooksメソッドができた。ここでは表記されていないが実はhas_manyはbooks()だけでなく他のメソッドの使用も可能にしている。
そもそもメソッドってなんだっけ??
Rubyでよく見かけたこんな感じのやつです。引数を入れるとreturnで値を返す。def index @products = Product.all endbooksはこのようなメソッドであり、returnで返ってきた値が反映される。
なので整理するとauthor.books.createはbooksメソッドの引数がauthorと連携して、それにcreateメソッドが対応していると言うことです。
テキストに打ち込み説明するのは難しいですが、私自身も今理解しているところなので何かご指摘などございましたらよろしくお願いします。
- 投稿日:2019-12-05T21:11:16+09:00
rails-tutorial第10章
modelだけ単数形の理由
modelは型に当たるので、それが複数形というのはちょっとということらしい。
edit updateアクションを実装しよう
まずはeditアクションをコントローラに実装
app/controllers/users_controller.rbclass UsersController < ApplicationController def show @user = User.find(params[:id]) end def new @user = User.new end def create @user = User.new(user_params) if @user.save log_in @user flash[:success] = "Welcome to the Sample App!" redirect_to @user else render 'new' end end def edit @user = User.find(params[:id]) end private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end endprivateについて
Rubyはクラスメソッドのオーバーライドなどもでき、外側からメソッドを変更される恐れがある。
そのため、privateを宣言しそれ以下に定義したメソッドに関しては外側からアクセスできないようにしている。これで勝手にadminユーザーを作られたりなどが防げる。編集フォームのviewを生成する。
app/views/users/edit.html.erb<% provide(:title, "Edit user") %> <h1>Update your profile</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(@user) do |f| %> <%= render 'shared/error_messages' %> <%= f.label :name %> <%= f.text_field :name, class: 'form-control' %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit "Save changes", class: "btn btn-primary" %> <% end %> <div class="gravatar_edit"> <%= gravatar_for @user %> <a href="http://gravatar.com/emails" target="_blank">change</a> </div> </div> </div>これはnewアクションのviewと同じなのに、なんでupdateアクションに飛ぶの??
これはnewの場合は全く新しいインスタンスなのに対して、editの場合は、すでにDBに保存されたもので中身もあるインスタンスだから。
この違いがform_forで生成されるhtmlに影響を与えているから。
$ rails console >> User.new.new_record? => true >> User.first.new_record? => falseこれは上記のようにrailsでtrueの時はpostリクエストを、falseの時はpatchリクエストを送るように判断するから。
updateアクションの実装
app/controllers/users_controller.rbclass UsersController < ApplicationController def show @user = User.find(params[:id]) end def new @user = User.new end def create @user = User.new(user_params) if @user.save log_in @user flash[:success] = "Welcome to the Sample App!" redirect_to @user else render 'new' end end def edit @user = User.find(params[:id]) end def update @user = User.find(params[:id]) if @user.update_attributes(user_params) # 更新に成功した場合を扱う。 else render 'edit' end end private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end endもう一度update_attributesメソッドを確認しよう
update_attributesとは、Ruby on Railsのモデルに備わるメソッドで、
モデルオブジェクト.update_attributes(キー: 値, キー: 値 … )
のようにHashを引数に渡してデータベースのレコードを複数同時に更新することができるメソッドです。データベースへの更新タイミングは、update_attributesメソッドの実行と同時で、validationも実行されます。
編集失敗時のテストを書こう
$ rails generate integration_test users_edit
test/integration/users_edit_test.rbrequire 'test_helper' class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "unsuccessful edit" do get edit_user_path(@user) assert_template 'users/edit' patch user_path(@user), params: { user: { name: "", email: "foo@invalid", password: "foo", password_confirmation: "bar" } } assert_template 'users/edit' end end失敗時のコードは実装済みなのでテストは成功する。
次は編集に成功した時のテストを書こう
test/integration/users_edit_test.rbrequire 'test_helper' class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end . . . test "successful edit" do get edit_user_path(@user) assert_template 'users/edit' name = "Foo Bar" email = "foo@bar.com" patch user_path(@user), params: { user: { name: name, email: email, password: "", password_confirmation: "" } } assert_not flash.empty? assert_redirected_to @user @user.reload assert_equal name, @user.name assert_equal email, @user.email end end今の時点では成功時の処理を書いていないので、テストは失敗する。
@user.reloadはユーザーの情報をDBの情報と同期するというもの。
具体的にupdateアクションを実装する
app/controllers/users_controller.rbclass UsersController < ApplicationController . . . def update @user = User.find(params[:id]) if @user.update_attributes(user_params) flash[:success] = "Profile updated" redirect_to @user else render 'edit' end end . . . endflashとredirectを実装したので、テストが通りそうに思えるが、これでもまだ失敗してしまう。
それはテストコードで書いたパスワードが空のためだ。パスワードには、空ではなく、さらに6文字以上というバリデーションを設定しているため、それに引っかかってしまったのである。
この状態だと、名前とemailを変えるだけなのに、passwordを毎回入力しなければいけなくなってしまう。
これを解決するにはallow_nil: trueを指定してあげると良い。
app/models/user.rbclass User < ApplicationRecord attr_accessor :remember_token before_save { self.email = email.downcase } validates :name, presence: true, length: { maximum: 50 } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false } has_secure_password validates :password, presence: true, length: { minimum: 6 }, allow_nil: true . . . endhas_secure_passwordでは (追加したバリデーションとは別に) オブジェクト生成時に存在性を検証するようになっているため、空のパスワード (nil) が新規ユーザー登録時に有効になることはありません。(空のパスワードを入力すると存在性のバリデーションとhas_secure_passwordによるバリデーションがそれぞれ実行され、2つの同じエラーメッセージが表示されるというバグがありましたが (7.3.3)、これで解決できました。)
つまり、allow_nilを指定してもオブジェクト生成時にはhas_secure_passwordで存在性のバリデーションをしてくれるので安心である。
これでテストが通る。
認可について
今の状態だと、url直打ちでログインしてないのにuser_pathに入れたりなどが起こってしまう。
そのため、適切なユーザーでないとそのページにアクセスできないようにしたい。
beforeフィルターを使ってユーザーログインを要求する。
app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] . . . private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end end endbefore_action :logged_in_user, only: [:edit, :update]
これは userコントローラのeditメソッドとupdateメソッドを呼び出す前にlogged_in_userメソッドを呼び出してねーって意味。
ここで、createアクションを直打ちでしたらエラーになるんじゃね?って思うでしょ?
でも、resourcesで /usersになっているため直打ちしてもindexアクション呼び出してーってなるから大丈夫。
いや、でもshowアクションなんかは直打ちでアクセスできちゃうぞ?
これどうするんだ?beforeフィルターをした時の注意点
before_フィルターをすると、今まで通っていたテストが失敗してしまう。
理由はテスト時にedit,updateアクションを呼び出すときにログインを要求するようになったからだ。これを解決するには、テスト時のユーザーに事前にログインさせる必要がある。
test/integration/users_edit_test.rbrequire 'test_helper' class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "unsuccessful edit" do log_in_as(@user) get edit_user_path(@user) . . . end test "successful edit" do log_in_as(@user) get edit_user_path(@user) . . . end endこれでテストは通るようになる。
ただ、今度はbefore_actionが万が一コメントアウトされたときにテストで検知しないといけなくなってしまった。
これを解決していこう。
これはuser_controller_testに書いていく。
test/controllers/users_controller_test.rbrequire 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end . . . test "should redirect edit when not logged in" do get edit_user_path(@user) assert_not flash.empty? assert_redirected_to login_url end test "should redirect update when not logged in" do patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } assert_not flash.empty? assert_redirected_to login_url end endこのテストはログインしてない状態でedit,updateアクションを呼び出そうとすると、フラッシュメッセージが表示され、さらにログインページにリダイレクトされてますよね?ってテスト。
これを書いておくことで、万が一before_actionが消されるとテストが失敗するようになるので安心だよねーって話。
ただ、今の状態だと、ログインしてさえいれば他人の情報を変更できるという状態。
これを解決するにはどうすれば良いか?このテストを書くにあたって、2人以上のサンプルデータがないとダメなので、fixtureにデータをたす
test/fixtures/users.ymlmichael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %>次にコントローラの単体テストを書く
test/controllers/users_controller_test.rbrequire 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other_user = users(:archer) end . . . test "should redirect edit when logged in as wrong user" do log_in_as(@other_user) get edit_user_path(@user) assert flash.empty? assert_redirected_to root_url end test "should redirect update when logged in as wrong user" do log_in_as(@other_user) patch user_path(@user), params: { user: { name: @user.name, email: @user.email } } assert flash.empty? assert_redirected_to root_url end endこの状態だとテストが落ちてしまう。
じゃあどうすれば?
before_actionを追加しよう
app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] . . . def edit end def update if @user.update_attributes(user_params) flash[:success] = "Profile updated" redirect_to @user else render 'edit' end end . . . private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless @user == current_user end endここで気をつけなければいけないのは、
before_actionには順序があるということだ。上から順番に実行されるので、ログインしている、かつ、正しいユーザーか?という順序で実行させる。
また、def logged_in_userでcurrent_userがいることは確定しているので、
無駄な処理を書かずに、params[:id]から取得したユーザーとcurrent_userを比較すれば良いこれでテストは通る。
一応リファクタリングもしておこう
unless @user == current_user
この部分を綺麗にしたい。
app/helpers/sessions_helper.rbmodule SessionsHelper # 渡されたユーザーをログイン def log_in(user) session[:user_id] = user.id end # 永続セッションとしてユーザーを記憶する def remember(user) user.remember cookies.permanent.signed[:user_id] = user.id cookies.permanent[:remember_token] = user.remember_token end # 渡されたユーザーがログイン済みユーザーであればtrueを返す def current_user?(user) user == current_user end # 記憶トークン (cookie) に対応するユーザーを返す def current_user . . . end . . . endヘルパーメソッドを定義したので、
app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] . . . def edit end def update if @user.update_attributes(user_params) flash[:success] = "Profile updated" redirect_to @user else render 'edit' end end . . . private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless current_user?(@user) end endredirect_to(root_url) unless current_user?(@user)というように書き換えることができる。
フレンドリーフォアーディング
親切なリダイレクトを作ろう、もともとアクセスしたかったページにリダイレクトしてあげるようにしようということ。
まずは統合テストを書いていこう。
test/integration/users_edit_test.rbrequire 'test_helper' class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end . . . test "successful edit with friendly forwarding" do get edit_user_path(@user) log_in_as(@user) assert_redirected_to edit_user_url(@user) name = "Foo Bar" email = "foo@bar.com" patch user_path(@user), params: { user: { name: name, email: email, password: "", password_confirmation: "" } } assert_not flash.empty? assert_redirected_to @user @user.reload assert_equal name, @user.name assert_equal email, @user.email end endこのようにuser_editが成功するストーリーに追加してあげる。
このままだと、ログイン後userページに飛んでしまうため、テストは失敗する。次は、
ユーザーがもともと行きたかったページを覚えていたらそのページにアクセスし、覚えていなかったら普通のユーザーページにアクセスできるようなヘルパーメソッドを定義する。
sessionを使うと便利。
app/helpers/sessions_helper.rbmodule SessionsHelper . . . # 記憶したURL (もしくはデフォルト値) にリダイレクト def redirect_back_or(default) redirect_to(session[:forwarding_url] || default) session.delete(:forwarding_url) end # アクセスしようとしたURLを覚えておく def store_location session[:forwarding_url] = request.original_url if request.get? end endrequest.original_urlについて見ていこう。
requestはもともとある特殊な変数である。
original_urlメソッドを渡してあげると、userがもともと行きたかったurlを参照することができる。if request.get?について見ていこう。
本来ログインせずにpatchリクエストをしてupdateアクションを呼び出すのは意味がない。
なので、if request.get?でgetリクエストだった時だけ、session[:forwarding_url]に情報を格納するよーって話。もともと行きたかったurlが発生するのは必ずログイン前のことなので、
store_locationメソッドをbefore_actionで指定したメソッドの中に記載する。app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] before_action :correct_user, only: [:edit, :update] . . . def edit end . . . private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? store_location flash[:danger] = "Please log in." redirect_to login_url end end # 正しいユーザーかどうか確認 def correct_user @user = User.find(params[:id]) redirect_to(root_url) unless current_user?(@user) end endもしログインしてなかったら、store_locationメソッドが呼び出されて、もともと行きたかったurlが参照できる。
そして、redirect_back_orメソッドをsessionコントローラのcreateメソッドの中に入れる。
app/controllers/sessions_controller.rbclass SessionsController < ApplicationController . . . def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) log_in user params[:session][:remember_me] == '1' ? remember(user) : forget(user) redirect_back_or user else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end . . . end本当はここでテスト通るはずなんだが、まさかの失敗。
NameError: NameError: undefined local variable or method ` store_location' for #<UsersController:0x0000000006837bc8> Did you mean? store_location app/controllers/users_controller.rb:51:in `logged_in_user' test/integration/users_edit_test.rb:22:in `block in <class:UsersEditTest>'このようにエラーが出た。
で、これは全角スペースがcontrollerの方に含まれていたからなんだね。
全角スペースもrubyではメソッド名に入る。
なのでこれを取り除くとテストが通った。全てのユーザーを表示する
まずはログインしてないユーザーがindexアクションをリクエストしたときにログインページに飛ぶかをテストする。
test/controllers/users_controller_test.rbrequire 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other_user = users(:archer) end test "should redirect index when not logged in" do get users_path assert_redirected_to login_url end . . . endこの状態だとテストは失敗してしまう。
テストを通るようにするのは簡単。users_controllerにindexアクションを定義して、before_actionにindexアクションを追加してあげればいいだけ。
app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update] before_action :correct_user, only: [:edit, :update] def index end def show @user = User.find(params[:id]) end . . . end次にindexアクションに対応するviewを作っていこう。
app/views/users/index.html.erb<% provide(:title, 'All users') %> <h1>All users</h1> <ul class="users"> <% @users.each do |user| %> <li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li> <% end %> </ul>ただ、これだと、gravatar_forメソッドに引数が2つ渡されている。
users_helperに定義したメソッドは引数が1つしか渡せない。これを直す必要がある。
app/helpers/users_helper.rbmodule UsersHelper # 渡されたユーザーのGravatar画像を返す def gravatar_for(user, options = { size: 80 }) gravatar_id = Digest::MD5::hexdigest(user.email.downcase) size = options[:size] gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}" image_tag(gravatar_url, alt: user.name, class: "gravatar") end endこれでエラーはなくなるが、cssが整ってないので、
app/assets/stylesheets/custom.scss/* Users index */ .users { list-style: none; margin: 0; li { overflow: auto; padding: 10px 0; border-bottom: 1px solid $gray-lighter; } }それが終わったらheaderにリンクを設定する。
サンプルユーザーの作成
ページネーションを試したいが、30人手動で作成するのはかなり面倒。
なのでコンピュータに作ってもらおう。まずはGemfileにFaker gemを追加します
source 'https://rubygems.org' gem 'rails', '5.1.6' gem 'bcrypt', '3.1.12' gem 'faker', '1.7.3'bundleをする
データベース上にサンプルユーザーを生成するRailsタスク
次にサンプルユーザーの生成するためのコードを書く。
db/seeds.rbUser.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar") 99.times do |n| name = Faker::Name.name email = "example-#{n+1}@railstutorial.org" password = "password" User.create!(name: name, email: email, password: password, password_confirmation: password) end注目すべきはUser.create!
db/seedsはべきとうせいではないので、一度実行して、2度目を実行しようとすると、再度100人サンプルユーザーが作られてしまう。
ただ、メールアドレスはユニークネスのバリデーションを設定しているので、100回間違えることになる。
これを避けるために、!を追加して、1人目の時点でバリデーションに引っかかったら例外を出すようにしている。
faker gemの機能を試してみよう
$ rails db:migrate:reset
このコマンドを実行すると、開発用のDBのデータをリセットすることができる。そして、
$ rails db:seed
このコマンドを実行すると、サンプルユーザーが100人作られる。ページネーション
サンプルユーザーが100人できたので、ページネーション機能を作っていこう
source 'https://rubygems.org' gem 'rails', '5.1.6' gem 'bcrypt', '3.1.12' gem 'faker', '1.7.3' gem 'will_paginate', '3.1.6' gem 'bootstrap-will_paginate', '1.0.0'will_pagenateというgemをインストールする。
gem 'bootstrap-will_paginate', '1.0.0'はページネーション機能とbootstrapを関連つける
pagenationを表示させる
pagenationを表示させるのは簡単で、
app/views/users/index.html.erb<% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <% @users.each do |user| %> <li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li> <% end %> </ul> <%= will_paginate %>pagenationを表示させたいところで<%= will_paginate %>と書くだけで良い。
ここで少し注意点
<%= will_paginate %>は本来<%= will_paginate @users %>となる。
今いるコンテキストの扱っているリソースを推測してくれて、pagenateする機能なのである。
これだけでは、ダメらしい。
具体的には、indexアクション内のallをpaginateメソッドに置き換えます (リスト 10.46)。ここで:pageパラメーターにはparams[:page]が使われていますが、これはwill_paginateによって自動的に生成されます。
app/controllers/users_controller.rbclassUsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update] . . . def index @users = User.paginate(page: params[:page]) end . . . end実装はこれで終わり。
pagenationテストを行う
pagenationのテストを行うにはテスト環境にユーザーが30人以上いないとダメ。
それを解決するには、
test/fixtures/users.ymlmichael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> lana: name: Lana Kane email: hands@example.gov password_digest: <%= User.digest('password') %> malory: name: Malory Archer email: boss@example.gov password_digest: <%= User.digest('password') %> <% 30.times do |n| %> user_<%= n %>: name: <%= "User #{n}" %> email: <%= "user-#{n}@example.com" %> password_digest: <%= User.digest('password') %> <% end %>というようにしてあげる。
統合テストのファイルを作成しよう
$ rails generate integration_test users_index
test/integration/users_index_test.rbrequire 'test_helper' class UsersIndexTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "index including pagination" do log_in_as(@user) get users_path assert_template 'users/index' assert_select 'div.pagination' User.paginate(page: 1).each do |user| assert_select 'a[href=?]', user_path(user), text: user.name end end end今回のテストでは、paginationクラスを持ったdivタグをチェックして、最初のページにユーザーがいることを確認します。
30人以上のユーザーがいるとpaginationクラスを持つようになるらしい。
indexアクションのviewをリファクタリングしよう
app/views/users/index.html.erb<% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <% @users.each do |user| %> <%= render user %> <% end %> </ul> <%= will_paginate %>ここでは、renderをパーシャル (ファイル名の文字列) に対してではなく、Userクラスのuser変数に対して実行している点に注目してください。この場合、Railsは自動的に_user.html.erbという名前のパーシャルを探しにいくので、このパーシャルを作成する必要があります
app/views/users/_user.html.erb<li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li>これはさらにリファクタリングができる。
app/views/users/index.html.erb<% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <%= render @users %> </ul> <%= will_paginate %><%= render @users %>をすると、railsが自動的に
<% @users.each do |user| %> <%= render user %> <% end %>のコードを展開してくれる。
なので、これを分かっていれば、
パーシャルを先に用意して、あとはrender @usersとすれば簡単にユーザーの情報を並べて表示することができる。この状態でテストも通る。
このリファクタリングはかなりショートカットできるので覚えるように!!!
ユーザーを削除する
管理権限を持っていればユーザーを削除できるようにする。
まずは、
特権を持つ管理ユーザーを識別するために、論理値をとるadmin属性をUserモデルに追加します。この後で説明しますが、こうすると自動的にadmin?メソッド (論理値を返す) も使えるようになりますので、これを使って管理ユーザーの状態をテストできます。
$ rails generate migration add_admin_to_users admin:boolean
今回は作成されたマイグレーションファイルにデフォルト値を設定する
db/migrate/[timestamp]_add_admin_to_users.rbclass AddAdminToUsers < ActiveRecord::Migration[5.0] def change add_column :users, :admin, :boolean, default: false end endbooleanなのでtrueかfalseかしか入らないと思いがちだが、どうやらnilも入るらしい。
なので、あらかじめdefaultオプションでfalseを指定しておく。adminユーザーを作ってみよう
db/seeds.rbUser.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar", admin: true) 99.times do |n| name = Faker::Name.name email = "example-#{n+1}@railstutorial.org" password = "password" User.create!(name: name, email: email, password: password, password_confirmation: password) endそして、データベースをリセットして再度サンプルデータをページネーションで表示してあげる。
$ rails db:migrate:reset
$ rails db:seeddestroyアクションを実装しよう
まずはadminユーザーだけリンクが見えるようにしよう
app/views/users/_user.html.erb<li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> <% if current_user.admin? && !current_user?(user) %> | <%= link_to "delete", user, method: :delete, data: { confirm: "You sure?" } %> <% end %> </li>userはuser_path(user)の略。
destroyアクションを作る
app/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update, :destroy] before_action :correct_user, only: [:edit, :update] . . . def destroy User.find(params[:id]).destroy flash[:success] = "User deleted" redirect_to users_url end private . . . endこれだけではダメ、
ログインしていて、かつadminユーザーじゃないとだめapp/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update, :destroy] before_action :correct_user, only: [:edit, :update] before_action :admin_user, only: :destroy . . . private . . . # 管理者かどうか確認 def admin_user redirect_to(root_url) unless current_user.admin? end endユーザーの削除のテスト
まずはテスト用のサンプルユーザの中にadminユーザーを作る。
test/fixtures/users.ymlmichael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> admin: trueテストコードを書く。
以下は、adminユーザーだけどログインしてない、そして、ログインしてるけどadminユーザーじゃない場合はどちらもdestroyアクション使えないよねーってテストtest/controllers/users_controller_test.rbrequire 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other_user = users(:archer) end . . . test "should redirect destroy when not logged in" do assert_no_difference 'User.count' do delete user_path(@user) end assert_redirected_to login_url end test "should redirect destroy when logged in as a non-admin" do log_in_as(@other_user) assert_no_difference 'User.count' do delete user_path(@user) end assert_redirected_to root_url end end統合テストも書いてみる。
test/integration/users_index_test.rbrequire 'test_helper' class UsersIndexTest < ActionDispatch::IntegrationTest def setup @admin = users(:michael) @non_admin = users(:archer) end test "index as admin including pagination and delete links" do log_in_as(@admin) get users_path assert_template 'users/index' assert_select 'div.pagination' first_page_of_users = User.paginate(page: 1) first_page_of_users.each do |user| assert_select 'a[href=?]', user_path(user), text: user.name unless user == @admin assert_select 'a[href=?]', user_path(user), text: 'delete' end end assert_difference 'User.count', -1 do delete user_path(@non_admin) end end test "index as non-admin" do log_in_as(@non_admin) get users_path assert_select 'a', text: 'delete', count: 0 end endunless user == @admin assert_select 'a[href=?]', user_path(user), text: 'delete' endこれは、@adminとuserが異なる場合、deleteのリンクが見えるよね?というテスト
逆に@admin = userの場合、deleteリンク見えませんよねーってことassert_difference 'User.count', -1 do delete user_path(@non_admin) endまた、do endでUserの数が1少なくなることを表す時は-1を指定していることにも注目。
herokuのDBを初期化する
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
このコマンドって、pushしてからじゃないと、マイグレーションファイルがそもそもないので意味がないってことなんじゃない?桜を作る
$ heroku run rails db:seed
$ heroku restart
- 投稿日:2019-12-05T21:08:19+09:00
PytorchをRubyで動かすtorch-rbをやってみた
はじめに
Rubyからpytorchを動かすtorch-rbを動かすところまでやってみたのでレポートします。
torch-rbとは
torch-rbはAndrew Kane氏が作成・公開している一連のRuby向け機械学習ライブラリの再新作です。ここ数ヶ月、Ankane氏はonnxruntimeのバインディング、Tensorflowのバインディング、LightGBMやxgboostのバインディングと、ものすごい勢いでRuby向けの機械学習ライブラリを制作・公開してきました。なかでも最新作にあたるtorch-rbに関してはREADMEから強い気合が伝わってきます。これまでのAnkaneプロダクツは、いずれもruby-ffiを利用したものですが、今回はriceを利用しているという特徴もあります。
torch-rbは絶賛開発中なので、ここに書いた情報はすぐに古くなる可能性があります。なるべく最新の情報を参照してください。
PyTorchのインストール
まずはpytorchをインストールします。Macを使っている方は、
brew
コマンドで大丈夫みたいですが、Linuxを使っているのでサイトからダウンロードします。LanguageでC++を選択するのがポイントです。
たまたま手元のPCがノートパソコンでGPUの性能は高くないので、今回はCPUオンリーのものをダウンロードしました。ダウンロードしたら解凍してできた
libtorch
ディレクトリをどこに配置したらいいのか、実はあまりよくわかっていませんが、とりあえず/usr/local/lib
あたりに配置しましてみました。つぎに、Githubからtorch-rbのリポジトリーをcloneします。
git clone https://github.com/ankane/torch-rbまずは
bundle install
しようと試みますがbundle install
最初からエラーメッセージが出てつまづきます。どうやら、
rice
のインストールでエラーが発生しているようです。riceは Ruby Interface for C++ Extensions です。Unfortunately Rice does not build against a staticly linked Ruby. You'll need to rebuild Ruby with --enable-shared to use this library. If you're on rvm: rvm reinstall [version] -- --enable-shared If you're on rbenv: CONFIGURE_OPTS="--enable-shared" rbenv install [version]エラーメッセージを見ると
--enable--shared
オプションをつけてRubyをビルドし直すように表示されますので言われたとおりにします。CONFIGURE_OPTS="--enable-shared" rbenv install 2.6.5うまくインストールされました。
Installed ruby-2.6.5 to /home/kojix2/.rbenv/versions/2.6.5
気を取り直して、
bundle install
しなおします。今度はうまくいきました。
あとは以下のように--with-torch-dir
オプションをつけてインストールします。(Macの場合はこれらの過程は不要のようですので詳しくはREADMEをご覧ください)bundle exec rake build gem install pkg/torch-rb-0.1.4.gem -- --with-torch-dir=/usr/local/lib/libtorch/mnist example を試す
さて、インストールがうまくいったら、お決まりのmnist exampleを試してみます。
cd examples/mnist wget https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz gem install npy ruby main.rbhttps://github.com/ankane/torch-rb/tree/master/examples/mnist
class Net < Torch::NN::Module def initialize super @conv1 = Torch::NN::Conv2d.new(1, 32, 3, stride: 1) @conv2 = Torch::NN::Conv2d.new(32, 64, 3, stride: 1) @dropout1 = Torch::NN::Dropout2d.new(p: 0.25) @dropout2 = Torch::NN::Dropout2d.new(p: 0.5) @fc1 = Torch::NN::Linear.new(9216, 128) @fc2 = Torch::NN::Linear.new(128, 10) end def forward(x) x = @conv1.call(x) x = Torch::NN::F.relu(x) x = @conv2.call(x) x = Torch::NN::F.max_pool2d(x, 2) x = @dropout1.call(x) x = Torch.flatten(x, start_dim: 1) x = @fc1.call(x) x = Torch::NN::F.relu(x) x = @dropout2.call(x) x = @fc2.call(x) output = Torch::NN::F.log_softmax(x) output end end左のターミナルに
htop
によるリソースの使用状況、右のターミナルに学習の進捗が表示されています。CPUを4コア使って頑張って計算してくれているのがわかりますね。しかしCPUやメモリをカツカツに使っている感じではなくて、バランスが取れていていいですね。最終結果はこんな感じでした。
Train Epoch: 14 [59904/60000 (100%)] Loss: 0.197016 Test set: Average loss: 0.0298, Accuracy: 9909/10000 (99%)素晴らしい。
推論だけでなく学習もPythonと遜色のない速度で実行できるRubyのディープラーニングライブラリは、私が知ってる範囲ではこれがはじめてです。今回はノートPCを使用して、i5-7200U CPUで計算しましたがまあまあ短時間で学習を終えることができました。real 18m39.163s user 47m20.107s sys 0m26.818s(ブラウザとか開いていたのでさらに早くなるかも)
ちょうどPFNがChainerをやめてPytorchに行くというニュースが入ってきました。今はちょうどそんな時期ですが、やっとRubyでもディープラーニングを勉強できる環境が整備されつつあるのではないでしょうか。
この記事は以上です。
- 投稿日:2019-12-05T21:08:19+09:00
PyTorchをRubyで動かすtorch-rbをやってみた
はじめに
RubyからPyTorchを動かすTorch-rbを動かすところまでやってみたのでレポートします。
Torch-rbとは
Torch-rbはAndrew Kane氏が作成・公開している一連のRuby向け機械学習ライブラリの最新作です。ここ数ヶ月、Ankane氏はonnxruntimeのバインディング、Tensorflowのバインディング、LightGBMやxgboostのバインディングと、ものすごい勢いでRuby向けの機械学習ライブラリを制作・公開してきました。なかでも最新作にあたるtorch-rbに関してはREADMEから強い気合が伝わってきます。これまでのAnkaneプロダクツは、いずれもruby-ffiを利用したものですが、今回はriceを利用しているという特徴もあります。
torch-rbは絶賛開発中なので、ここに書いた情報はすぐに古くなる可能性があります。なるべく最新の情報を参照してください。
PyTorchのインストール
まずはpytorchをインストールします。Macを使っている方は、
brew
コマンドで大丈夫みたいですが、Linuxを使っているのでサイトからダウンロードします。LanguageでC++を選択するのがポイントです。
たまたま手元のPCがノートパソコンでGPUの性能は高くないので、今回はCPUオンリーのものをダウンロードしました。ダウンロードしたら解凍してできた
libtorch
ディレクトリをどこに配置したらいいのか、実はあまりよくわかっていませんが、とりあえず/usr/local/lib
あたりに配置しましてみました。つぎに、Githubからtorch-rbのリポジトリーをcloneします。
git clone https://github.com/ankane/torch-rbまずは
bundle install
しようと試みますがbundle install
最初からエラーメッセージが出てつまづきます。どうやら、
rice
のインストールでエラーが発生しているようです。rice は Ruby Interface for C++ Extensions です。Unfortunately Rice does not build against a staticly linked Ruby. You'll need to rebuild Ruby with --enable-shared to use this library. If you're on rvm: rvm reinstall [version] -- --enable-shared If you're on rbenv: CONFIGURE_OPTS="--enable-shared" rbenv install [version]エラーメッセージを見ると
--enable--shared
オプションをつけてRubyをビルドし直すように表示されますので言われたとおりにします。CONFIGURE_OPTS="--enable-shared" rbenv install 2.6.5うまくインストールされました。
Installed ruby-2.6.5 to /home/kojix2/.rbenv/versions/2.6.5
気を取り直して、
bundle install
しなおします。今度はうまくいきました。
あとは以下のように--with-torch-dir
オプションをつけてインストールします。(Macの場合はこれらの過程は不要のようですので詳しくはREADMEをご覧ください)bundle exec rake build gem install pkg/torch-rb-0.1.4.gem -- --with-torch-dir=/usr/local/lib/libtorch/mnist example を試す
さて、インストールがうまくいったら、お決まりのmnist exampleを試してみます。
cd examples/mnist wget https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz gem install npy ruby main.rbhttps://github.com/ankane/torch-rb/tree/master/examples/mnist
class Net < Torch::NN::Module def initialize super @conv1 = Torch::NN::Conv2d.new(1, 32, 3, stride: 1) @conv2 = Torch::NN::Conv2d.new(32, 64, 3, stride: 1) @dropout1 = Torch::NN::Dropout2d.new(p: 0.25) @dropout2 = Torch::NN::Dropout2d.new(p: 0.5) @fc1 = Torch::NN::Linear.new(9216, 128) @fc2 = Torch::NN::Linear.new(128, 10) end def forward(x) x = @conv1.call(x) x = Torch::NN::F.relu(x) x = @conv2.call(x) x = Torch::NN::F.max_pool2d(x, 2) x = @dropout1.call(x) x = Torch.flatten(x, start_dim: 1) x = @fc1.call(x) x = Torch::NN::F.relu(x) x = @dropout2.call(x) x = @fc2.call(x) output = Torch::NN::F.log_softmax(x) output end end左のターミナルに
htop
によるリソースの使用状況、右のターミナルに学習の進捗が表示されています。CPUを4コア使って頑張って計算してくれているのがわかりますね。しかしCPUやメモリをカツカツに使っている感じではなくて、バランスが取れていていいですね。最終結果はこんな感じでした。
Train Epoch: 14 [59904/60000 (100%)] Loss: 0.197016 Test set: Average loss: 0.0298, Accuracy: 9909/10000 (99%)素晴らしい。
推論だけでなく学習もPythonと遜色のない速度で実行できるRubyのディープラーニングライブラリは、私が知ってる範囲ではこれがはじめてです。今回はノートPCを使用して、i5-7200U CPUで計算しましたがまあまあ短時間で学習を終えることができました。real 18m39.163s user 47m20.107s sys 0m26.818s(ブラウザとか開いていたのでさらに早くなるかも)
ちょうどPFNがChainerをやめてPytorchに行くというニュースが入ってきました。今はちょうどそんな時期ですが、やっとRubyでもディープラーニングを勉強できる環境が整備されつつあるのではないでしょうか。
この記事は以上です。
- 投稿日:2019-12-05T20:15:43+09:00
プログラミングを始めて1年
Ubiregi Advent Calendar 2019 6日目です!
なにこれ
今日は技術の話でもなく、そもそも開発のお仕事に(まだ)就いていない人が1年、プログラミング(RubyとちょっとRails)を楽しく続けることが出来ているのでそれに関する話を書きます。
自己紹介
株式会社ユビレジの原と申します。カスタマーサクセス(CS)部に所属しております。仕事はお客さんとの調整事やらなんやらで仕事では全くプログラミングしていません。
そもそもなんでプログラミングやり始めたのか
純粋に楽しそうだったからです。結構もういい歳ですが、本気でジョブチェンジをしたい。世間体のあり様で言えば、管理職にでもなって部下を指導するみたいな役割をこなさないといけないんじゃないかな〜とたまに思うこともあります。ですがそれが自分にとって本当にプラスになる生き方なのかと考えると不必要なストレスを抱えて生きるよりは、楽しく仕事できた方が良いかなーとも思います。
いろんな言語がある中でなぜRuby?
単純にユビレジがRuby on Railsで動いている & 不明点あれば弊社開発チームに質問できる & 学んでいくうちにわかったことですが、直感的でわかりやすい言語だから あたりでしょうか。
プログラミングは難しいとよく言われているけど実際どうだったか
プログラミングに限らないと思いますが、誰でも最初は難しいと感じるのではないでしょうか。
配列とかハッシュとか横文字がズラズラ出てきて、なんなんこれ?と。役立つんかいなと。
引数とか戻り値とかようわからん!と嘆いていました・・・。
わからん→ググる→理解する(言っていることがわからなければ社内で質問する)→1ヶ月ぐらいすると忘れている・・・の繰り返しでした。
噛めば噛むほど味が出るという訳ではないですが、時間を掛けて学んでいけばゆくゆくはできるようになるんじゃないかなと思います。
あと「プログラミング 勉強法」なんかでググると3ヶ月でプログラマーデビュー!みたいな広告を見ますが、私のようにまっさらな状態からのスタートだと厳しいんじゃないかなと思います。なんらかの言語を学んでいるとかならいけるのかな・・・。毎日8時間程度の学習時間を確保できるのであれば(羨望)
あとは楽しく学べているのか、習慣化出来ているのかぐらいですかね?今までやってきたこと
【2018年 12月〜2019年 1月】
・Rubyを学び始める。使った参考書はゼロからわかる Ruby 超入門を使って基礎の基礎から始めました。入門書に関しては他にも色々あると思いますが、プログラミング自体を初めて学ぶ方にはオススメ!(コードエディタの説明とか役に立ちました)
【2019年 2月〜6月】
・アプリ作成できることが最終目標なのだから、Railsチュートリアルをやろうと奮い立ちました。しかし今思うとやや時期尚早だったなと。中身は2割ぐらいしか理解できず。とりあえず写経して動くものが作れるという体験をしました。
・チュートリアルの合間にプロを目指す人のためのRuby入門も並行して取り組みました。こちらは超入門と比較するとかなりボリュームがありますが、中身は親切丁寧に書かれているので、理解しながら読んでいけば成果は出るかと。ただし1度読んで写経したからと言ってすぐに使えるものでもないと思うので、忘れた頃に読み返すと良いかなーと思います。【2019年 7月~9月】
・検索すると兎にも角にもアプリを作るのがプログラミングを理解するのに一番の近道と言われているのでアプリ作成かなとは思うものの、チュートリアルの理解が微妙だし、もう一度Railsチュートリアルをログイン機構実装まで復習と写経を行いました。今度は中身を理解できるまで熟読と写経を行う。またすこーしだけ理解が進んだ。
【2019年 10月〜11月】
・さあもういけるやろと意気込んでみたものの、やはりよくわからない・・・。
悩んだ時に見返していたこの記事を見ると「チュートリアルやったら次は現場Rails見るといいんでないかい」とあったので素直に見ることにしました。これからですが。
※ちなみにこの記事に書いてある事は結構信憑性が高いと個人的に思っています。
・そして10月からHackerRankなるものを始めました。開発チームの方から良かったらどう?みたいな感じで。最初はどうかなーと思っていましたが、問題を解く日々が続いております。Rubyの復習にもなるし、算数やアルゴリズムの勉強にもなるので一石二鳥!この一年で購入したものや読んだ本
・MacBook Air 2016(中古)
本当はProの方がいいんですけど、プログラミングが続くかどうかわからなかったので日和ってしまった & 奥さんにめちゃ懇願してどうにか許しを得ての購入だった。でも軽いし持ち運び便利だから最初はAirでもいいんじゃなかろうか。確か7諭吉ぐらい。・ゼロからわかる Ruby 超入門
・プロを目指す人のためのRuby入門
この2冊でRubyの大まかな部分を学習
・プログラマーの数学
ちょっと難しい箇所もありましたが、全体的にわかりやすく書かれていました。込み入った問題にどう取り組めば良いのかなどの考え方が参考になる
・Webを支える技術
HTTPとかRESTなどを知るため。この世界では鉄板の様です。
・ベタープログラマー ―優れたプログラマになるための38の考え方とテクニック
会社でほぼ毎日お昼の時間に15分程度の読書会を行っており、そこで読みました。こういうプログラマーになりましょう!的な内容の本です。まあまあ読んでいましたね?いや少ないか・・・。まあ読めばいいってものじゃないし!
今後の展望
1分でも早く仕事でプログラミングをやりたい!とは常々思っていますが、じゃあお前これやれやって言われてガッテン承知!とはいかないものです?手っ取り早く認めてもらうには簡単でも良いから何らかのアプリが作成できてからですかね・・・。1年も経ったわけだし何か成果物欲しいところ(自分のモチベーションを上げるにも)。
最後に
まだまだ頑張ります!
- 投稿日:2019-12-05T19:24:13+09:00
Rails 6.0の "Active Storage's ImageProcessing transformer doesn't support :combine_options" という警告に対処する方法
困っていたこと
Rails 5.2時代にActiveStorageでこんなコードを書いていました。
user.avatar.variant( combine_options: { resize: "150^", gravity: "Center", crop: "150x150+0+0", auto_orient: true } )やっていることは縦長だったり横長だったりする画像を、中心で正方形に切り抜いて150x150サイズに縮小することです。(mini_magickを使用)
このコードをRails 6.0に上げるとこんな警告が出ました。
DEPRECATION WARNING: Generating image variants will require the image_processing gem in Rails 6.1. Please add `gem 'image_processing', '~> 1.2'` to your Gemfile.なにやらimage_processing gemをインストールしろ、と言われているので、言われたとおりGemfileに追加します。
Gemfilegem 'image_processing', '~> 1.2'bundle install
すると、今度は警告の内容が次のように変わりました。
DEPRECATION WARNING: Active Storage's ImageProcessing transformer doesn't support :combine_options, as it always generates a single ImageMagick command. Passing :combine_options will not be supported in Rails 6.1.こちらの警告は
:combine_options
はRails 6.1でサポートされなくなるぜ!と言われてるものの、具体的な修正方法がわかりません。解決方法
この警告が導入されたコミットを確認した結果、どうやら
:combine_options
をなくしてこう書けば良いっぽいです。user.avatar.variant( resize: "150^", gravity: "Center", crop: "150x150+0+0", auto_orient: true )これで警告が消え、画像の切り抜きもうまくいきました。
ただ、これがベストプラクティスかどうかは確信がないので、もっと良い解決策を知ってる人がいたらコメントお待ちしています!
追記:こんなやり方があった!
同僚の @aki77 が「こんなやり方がありますよー」と、もっと便利な書き方を教えてくれました。(どうもありがとう!)
確かもうちょっと簡単に書けるようになったはずと思って調べてみました。
多分↓で良さそう。user.avatar.variant(resize_to_fill: [150, 150])
https://edgeguides.rubyonrails.org/active_storage_overview.html#transforming-imagescarrierwaveでも同じだったなーと思ったら、あっちもimage_processing使ってるんですね。
https://github.com/carrierwaveuploader/carrierwave/blob/master/carrierwave.gemspec#L27ということで、Rails 6ではこれだけでOKです!
(auto_orient: true
もデフォルトでセットされます)user.avatar.variant(resize_to_fill: [150, 150])めっちゃシンプルになりましたね!やったー?
- 投稿日:2019-12-05T15:36:47+09:00
rails cが動かねぇ!
rails c が息をしていない…
別件でrailsのバージョンアップをして、railsを入れ直した後のこと。
consoleで確認したいことがあり、rails c
を叩く。
…反応がない、ただの屍のようだ。
いや困るねん。やめてクレメンス。いらないプロセスは躊躇なく殺そうね
てことでプロセスを確認。
ps aux | grep rails…何もないな。
調べたところ、rails c
ではspringというプロセスも走るとのこと。ps aux | grep spring ~~ 8324 0.0 0.0 4352360 3228 s009 S+ 火03PM 0:00.76 spring server | ~~ | started 47 hours agoいやがった!!お前のせいか!!
kill -9 8324そしてめでたくconsoleが起動。
いやぁ良かった。
無駄なプロセスは殺そう。追記
spring stopでも可。
これだとプロセス確認せずに止められるから便利ね。
ありがとう、友よ。
- 投稿日:2019-12-05T14:55:04+09:00
【自分メモ】ProgateでRubyを学ぶ【ウルトラ初心者】
はじめに
Progateを使ってRubyを学習しています。
自分なりのメモです。
相当あたまがわるいので文章力は底辺です。変数
シングルクォーテーションで囲むと#{変数}は展開されない。
ダブルクォーテーションで囲む必要がある。
いかなるときも、ダブルで囲んでおけば問題ないですね。
むしろシングルを使う意味ってどこかにあるんでしょうか。sample.rbname = "田中" puts "こんにちは#{name}さん" #こんにちは田中さん puts 'こんにちは#{name}さん' #こんにちは#{name}さん数字と文字列を連結することはできない。これは変数でも同じ。
文字列の中に数値を埋め込む場合には #{変数} を利用する。sample2.rbage = 25 puts "私は" + age + "歳です" #エラー puts "私は#{age}歳です" #私は25歳ですハッシュ
複数の値を管理する方法のひとつ。
- 配列は要素を順番に並べて管理する
- ハッシュはキーと値でセットにして管理するhush.rbperson = {age: 25, height: 158, name: "田中"} puts "私は#{person[:age]}歳です" #私は25歳ですさいごに
Javaのように変数の型を決める必要もないし楽チンだな〜という印象。
逆に、Javaちゃんとやれば他の言語の習得簡単だろなあと。
- 投稿日:2019-12-05T14:34:44+09:00
ActiveRecordを使いつつDBのデータをメモ化して扱ってみる
はじめに
一度登録されたら、その後あまり更新されないマスター系のデータってありますよね。
普通にDBのテーブルを作成してActiveRecordを使ったり、moduleやclassにデータをハードコーディングして扱う場合もあると思います。テーブルが不要な場合は ActiveHash というgemを使うと便利で高速ですが、なんらかの理由でDBを使いたい場合もあるかもしれません。
(例えば、Railsから切り離されたレポートシステムなどから同じマスターデータを参照したい場合など)
ということで、ActiveRecord を利用しつつ普段はオンメモリで高速にデータを扱う簡単なライブラリを作成してみました。実装したもの要約
- マスター系データを扱うModelクラスで、全レコードのオブジェクトをメモ化するメソッド
- メモ化したデータを対象にfind, selectを行うメソッド
- 上記Modelに対しbelongs_toの関係にあるModelから参照する場合も、メモ化されたオブジェクトを利用するAssociation
使い方
インストール
RubyGemsに登録したのでGemfileを使ったりgemコマンドを使ってインストールできます。
https://rubygems.org/gems/ar_memoizationGemfilegem 'ar_memoization'DBスキーマの例
サンプルとして、以下のように都道府県を扱う
prefectures
と、お店情報を扱うshops
テーブルを定義しました。
shops
はprefectures
に対する外部キーを持っており、prefectures
はあまり更新されないものとします。db/create_tables.rbclass CreateAllTables < ActiveRecord::Migration[5.0] def self.up create_table(:prefectures) do |t| t.string :name end create_table(:shops) do |t| t.belongs_to :prefecture t.string :name end end endマスターデータを定義するModelクラス
全レコードのインスタンスをメモ化したいModelクラス内で
ArMemoization::PrimaryMethods
モジュールをextendします。app/models/prefecture.rb# # 都道府県を扱うModel # class Prefecture < ApplicationRecord extend ArMemoization::PrimaryMethods end追加されるメソッド
- .find_memo(id)
- メモ化したインスタンスから、引数のIDに一致するデータを返す
- IDをキーにしたHashから該当レコードのオブジェクトを返すので爆速
- .detect_memo(&block)
- メモ化したレコードのオブジェクトをブロック引数で渡し、ブロックがtrueを返したデータを1件返す
- select_memos(&block)
- メモ化したレコードのオブジェクトをブロック引数で渡し、ブロックがtrueを返したデータを配列で返す
- all_memos
- メモ化したインスタンスの配列を全て返す
- reload_memos
- メモ化したオブジェクトをDBからリロードする
使い方の例
事前準備(データ投入)[[1, "東京"], [2, "大阪"], [3, "名古屋"]].each do |ident, name| Prefecture.create!(id: ident, name: name) end# ID:2 のレコードを取得 prefecture = Prefecture.find_memo(2) # インスタンスメソッド kanto_area? がtrueを返すオブジェクトを取得 prefecture = Prefecture.detect_memo{|pref| pref.kanto_area? } prefecture = Prefecture.detect_memo(&:kanto_area?) # syntax sugar # インスタンスメソッド kanto_area? がtrueを返すオブジェクトの配列を取得 prefectures = Prefecture.select_memos(&:kanto_area?)実装の詳細はこちら
マスターデータへの外部キーを持つModelクラス
マスターデータのModelに対してbelongs_toの関係を持つModelクラスには
ArMemoization::ForeignMethods
をextendします。
また、belongs_to
の代わりにbelongs_to_memoized
を使いAssociationを定義します。app/models/shop.rbclass Shop < ApplicationRecord extend ArMemoization::ForeignMethods belongs_to_memoized :prefecture end(通常ないと思いますが)belongs_toと同時に利用することも出来ます。
app/models/shop.rbbelongs_to :prefecture belongs_to_memoized :memoized_prefecture, class_name: "Prefecture", foreign_key: "prefecture_id"
belongs_to_memoized
は内部でbelongs_to
を実行した後で、関連名でもあるreaderメソッド(上記例ではShop#prefecture
)をoverrideし、レコードをDBからロードする処理の代わりにメモ化済みのオブジェクトをAssociationとしてセットしています。Association以外で追加されるメソッド
- .where_memoized(association_name, method_name, &block)
- 関連Modelのメモ化されたオブジェクトを用い、join + where 的な絞り込みを行う
- ActiveRecord::Relation を返す
使い方の例
# Prefectureオブジェクトの中で、kanto_area?がtrueを返すオブジェクトのIDに関連しているShopのRelationを返す # 第2引数として関連Modelのインスタンスメソッド名か、ブロックを渡す Shop.where_memoized(:prefecture, :kanto_area?).limit(3) Shop.where_memoized(:prefecture){|pref| pref.kanto_area? }.limit(3)発行されるクエリSELECT `shops`.* FROM `shops` WHERE `shops`.`prefecture_id` = 1 LIMIT 3同一の条件文を実装するのに、scopeとインスタンスメソッドの両方でコードを書く必要がなくなるので、自分的にはちょっと良い感じです。
実装の詳細はこちら
今は出来ないけど出来そうなこと
- ID以外のPrimary Key に対応
- has_one, has_many からメモ化済みオブジェクトの取得
- create, saveのコールバックでメモ化したオブジェクトを差し替える
- 開発環境ではメモ化したオブジェクトを適切なタイミング(before_actionなど)で簡単にリロードする仕組み
まとめ
ほとんどの場合はActiveHashを使えば良いと思うので、このライブラリの使いどころがあまり思いつきません。とりあえず「こんなのあったらどうだろう?」な思いつきを形にしてみました。むしろ使い道を教えてください?
それでは、もっと有意義な時間を過ごすべくデザインパターンの勉強でもしてみましょう。
12日目は @nagata03 さんです!
- 投稿日:2019-12-05T14:11:16+09:00
普段使っているgemにプルリクを送ってマージされた
(この記事は、高専OBOG Advent Calendar 2019の12/5に送った記事です。)
要約
普段使っているgemに、Pull Requestを送って、マージされました。
LazyHighChartsというgemの動きが気に入らなくてモンキーパッチを当てて使っていました。
自分のアプリケーション以外でも役に立つ、gem本体に取り込まれるべき修正だと考えたので、gemのリポジトリにプルリクを送ることにしました。流れはこんな感じです。
- 不具合を見つける
- 手元でgemの修正を試す
- githubのリポジトリに修正を送る
- マージされる
PRがマージされるまで
1. 不具合を見つける
LazyHighChartsは、Highcharts JSというチャートを描画するJavaScriptライブラリを、RailsやSinatraなどのフレームワーク内で簡単に使えるようにするgemです。
詳しい紹介は本家をご覧ください。https://github.com/michelson/lazy_high_chartsRailsでJavaScriptの読み込みにdefer属性を付与した場合、チャートが描画されない、という不具合に遭遇しました。
viewに書き出されたjsを見てみると、どうやらLazyHighChartsがこしらえたjsに問題があるようでした。2. 手元でgemの修正を試す
手元でgemの修正をためします。
問題になっている箇所を書き出しているModuleを探して、モンキーパッチを当てます。
IntelliJ IDEAだとコードジャンプで該当箇所に飛べるので、すぐにModuleやClassの名前がわかって便利ですね!
モンキーパッチを当てると、問題なくチャートが描画されました。
(しばらくこの状態で本番で使っていました)次はgemのリポジトリをforkして、gemに修正を加えます。
Gemfileのlazy_high_chartsの取得先をこんな感じにforkしたリポジトリ&ブランチに書き換えて、手元で修正版のgemを試します。gem 'lazy_high_charts', git: 'git@github.com:kosappi/lazy_high_charts.git', branch: 'use_event_listener'この修正版も、モンキーパッチのときと同じように、問題なくチャートを描画してくれました。
3. githubのリポジトリに修正を送る
手元で動作確認ができたので、本家にも同じ修正を送ります。
まずはREADME.mdを読んで、どのような流れで修正を提案するのか見てみます。README.md
Contributingを読んでみると、このように書いてあります。
- Fork the project
- Do your changes and commit them to your repository
- Test your changes. We won't accept any untested contributions (except if they're not testable).
- Create an issue with a link to your commits.
まずは動作確認して、それからissueを作って欲しい、とのことでした。
動作確認は済んでいるので、issueを作ります。
英語は得意ではないので、すでにマージされたPRや、closeされたissueを参考にしつつ、時間をかけて書くことになりました。
作ったissue1日経つと、メンテナからPRを作って欲しいという旨の返事がありました。
PRを作ります。
作ったPRPRを作ってしばらくするとCIがコケて赤くなっていました。
specを少し修正するだけで解決したんですが、事前にforkしたリポジトリでもCIを回しておくと、ベターかも知れません。あとはマージされるのを待ちます。
4. マージされる
PRを出してから1日後、マージされました。
やったね!感想
RubyやRailsを仕事で触りだして数年になりますが、めちゃくちゃgemを使うわりには、contributeする機会はまったくありませんでした。
タダで使ってばっかりでなんかスマン、という気持ちがありました。
今回、はじめてcontributeすることができて、とてもうれしいです。
- 投稿日:2019-12-05T14:11:16+09:00
LazyHighChartsにプルリクを送ってマージされた
(この記事は、高専OBOG Advent Calendar 2019の12/5に送った記事です。)
要約
普段使っているgemに、Pull Requestを送って、マージされました。
LazyHighChartsというgemの動きが気に入らなくてモンキーパッチを当てて使っていました。
自分のアプリケーション以外でも役に立つ、gem本体に取り込まれるべき修正だと考えたので、gemのリポジトリにプルリクを送ることにしました。流れはこんな感じです。
- 不具合を見つける
- 手元でgemの修正を試す
- githubのリポジトリに修正を送る
- マージされる
PRがマージされるまで
1. 不具合を見つける
LazyHighChartsは、Highcharts JSというチャートを描画するJavaScriptライブラリを、RailsやSinatraなどのフレームワーク内で簡単に使えるようにするgemです。
詳しい紹介は本家をご覧ください。https://github.com/michelson/lazy_high_chartsRailsでJavaScriptの読み込みにdefer属性を付与した場合、チャートが描画されない、という不具合に遭遇しました。
viewに書き出されたjsを見てみると、どうやらLazyHighChartsがこしらえたjsに問題があるようでした。2. 手元でgemの修正を試す
手元でgemの修正をためします。
問題になっている箇所を書き出しているModuleを探して、モンキーパッチを当てます。
IntelliJ IDEAだとコードジャンプで該当箇所に飛べるので、すぐにModuleやClassの名前がわかって便利ですね!
モンキーパッチを当てると、問題なくチャートが描画されました。
(しばらくこの状態で本番で使っていました)次はgemのリポジトリをforkして、gemに修正を加えます。
Gemfileのlazy_high_chartsの取得先をこんな感じにforkしたリポジトリ&ブランチに書き換えて、手元で修正版のgemを試します。gem 'lazy_high_charts', git: 'git@github.com:kosappi/lazy_high_charts.git', branch: 'use_event_listener'この修正版も、モンキーパッチのときと同じように、問題なくチャートを描画してくれました。
3. githubのリポジトリに修正を送る
手元で動作確認ができたので、本家にも同じ修正を送ります。
まずはREADME.mdを読んで、どのような流れで修正を提案するのか見てみます。README.md
Contributingを読んでみると、このように書いてあります。
- Fork the project
- Do your changes and commit them to your repository
- Test your changes. We won't accept any untested contributions (except if they're not testable).
- Create an issue with a link to your commits.
まずは動作確認して、それからissueを作って欲しい、とのことでした。
動作確認は済んでいるので、issueを作ります。
英語は得意ではないので、すでにマージされたPRや、closeされたissueを参考にしつつ、時間をかけて書くことになりました。
作ったissue1日経つと、メンテナからPRを作って欲しいという旨の返事がありました。
PRを作ります。
作ったPRPRを作ってしばらくするとCIがコケて赤くなっていました。
specを少し修正するだけで解決したんですが、事前にforkしたリポジトリでもCIを回しておくと、ベターかも知れません。あとはマージされるのを待ちます。
4. マージされる
PRを出してから1日後、マージされました。
やったね!感想
RubyやRailsを仕事で触りだして数年になりますが、めちゃくちゃgemを使うわりには、contributeする機会はまったくありませんでした。
タダで使ってばっかりでなんかスマン、という気持ちがありました。
今回、はじめてcontributeすることができて、とてもうれしいです。
- 投稿日:2019-12-05T12:41:42+09:00
Rails6 のちょい足しな新機能を試す110(TranslationHelper#translate default オプション編)
はじめに
Rails 6 に追加された新機能を試す第110段。 今回は、
TranslationHelper#translate
default
オプション編です。
Rails 6 では、TranslationHelper#translate
のdefault
オプションで指定された値が Hash の場合に、その Hash が返されるようになりました。Ruby 2.6.5, Rails 6.0.1 で確認しました。(Rails 6.0.0 時点で修正されています。)
$ rails --version Rails 6.0.1今回は、適切な使い道を思いつけませんでした。
で、言語 (locale) 毎に数字をカンマ区切りで表示するだけの、Railsアプリケーションを作って試してみることにします。Rails プロジェクトを作成する
$ rails new rails_sandbox $ cd rails_sandboxController と View を作る
今回は、数字を表示するための NumbersController と index ページを作成します。
$ bin/rails g controller Numbers index
Helper メソッドを作成する
locale_number_with_delimiter
メソッドを作成します。
引数は、 数字と locale です。
default_number_format メソッドをコールして、locale に対するデフォルトフォーマットを決定します。
TranslationHelper#translate
メソッド (t
メソッド)を使って、number.format
を取得し、取得できなかった場合のために、default を指定します。
ここが、今回の確認ポイントになります。
number_with_delimiter
メソッドで数字を変換します。app/helpers/numbers_helper.rbmodule NumbersHelper def locale_number_with_delimiter(number, locale) default = default_number_format(locale) format = t('number.format', locale: locale, default: default) # ここがちょい足し機能 number_with_delimiter(number, format) end private def default_number_format(locale) case locale when :en, :ja { delimiter: ',', separator: '.' } when :de, :it { delimiter: '.', separator: ',' } when :sv { delimiter: ' ', separator: ',' } when :ruby { delimiter: '_', separator: '.' } else { delimiter: ',', separator: '.' } end end endController を修正する
NumbersController
のindex
メソッドで、@locales
と@number
を設定します。app/controllers/numbers_controller.rbclass NumbersController < ApplicationController def index @locales = %i[en ja de it sv ruby] @number = 10000.5 end endView を修正する
View では、 Language(Locale) と数字を表形式で表示します。
app/views/numbers/index.html.erb<h1>Numbers#index</h1> <table> <thead> <tr> <th> Language </th> <th> Number </th> </tr> </thead> <tbody> <% @locales.each do |locale| %> <tr> <td><%= locale %></td> <td><%= locale_number_with_delimiter(@number, locale) %></td> </tr> <% end %> </tbody> </table>config/application.rb を修正する
動作確認目的なので、locale のエラーが出ないように、
config/application.rb
に、I18n.enforce_available_locales = false
を追加します。module App class Application < Rails::Application ... I18n.enforce_available_locales = false end endrails server を実行してブラウザで表示する
rails server
を実行してブラウザで表示すると、それぞれの Language(locale) に合わせた数字の表示になっています。
Rails 5では
TranslationHelper#translate
が default で指定した Hash を返さないため、NoMethodError になってしまい動作しません。
試したソース
試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try110_translate_default参考情報
- 投稿日:2019-12-05T11:31:00+09:00
KibanaのAPIにrubyからアクセスする
概要
Kibanaに対してRubyからVisualizeやDashboardを作ったりしたかったので書いた
コード
class KibanaClient def self.call method, path, data endpoint = "http://localhost:5602#{path}" authorization = 'Basic ' + Base64.encode64("elastic:changeme").chomp headers = { authorization: authorization, "kbn-xsrf": "kibana", content_type: :json, accept: :json } response = nil begin response = RestClient.send(method.to_s, endpoint, data.to_json, headers) rescue => e puts e.response response = e.response end response end endTips
- kbn-xsrfが無いと弾かれる
- Basic認証はKibanaのユーザで行う
- リクエストに必要なJSONはKibanaのSaved ObjectsからInspectすると見れる
- Saved ObjectsのExportで更に詳しく見れる
サンプル
class KibanaClient def self.call method, path, data endpoint = "http://localhost:5602#{path}" authorization = 'Basic ' + Base64.encode64("elastic:changeme").chomp headers = { authorization: authorization, "kbn-xsrf": "kibana", content_type: :json, accept: :json } response = nil begin response = RestClient.send(method.to_s, endpoint, data.to_json, headers) rescue => e puts e.response response = e.response end response end def self.gen_metric id, index_pattern, title, references = nil, query = nil references = [ { "id": index_pattern, "name": "kibanaSavedObjectMeta.searchSourceJSON.index", "type": "index-pattern" } ] if references.blank? query = { "query": { "query": "", "language": "kuery" }, "filter": [], "indexRefName": "kibanaSavedObjectMeta.searchSourceJSON.index" } if query.blank? state = { "title": title, "type": "metric", "params": { "metric": { "percentageMode": false, "useRanges": false, "colorSchema": "Green to Red", "metricColorMode": "None", "colorsRange": [ { "type": "range", "from": 0, "to": 10000 } ], "labels": { "show": false }, "invertColors": false, "style": { "bgFill": "#000", "bgColor": false, "labelColor": false, "subText": "", "fontSize": 60 } }, "dimensions": { "metrics": [ { "type": "vis_dimension", "accessor": 0, "format": { "id": "number", "params": {} } } ] }, "addTooltip": true, "addLegend": false, "type": "metric" }, "aggs": [ { "id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {} } ] } json = { "attributes": { "description": "", "kibanaSavedObjectMeta": { "searchSourceJSON": query.to_json }, "title": title, "uiStateJSON": "{}", "visState": state.to_json }, "references": references } api_path = "/api/saved_objects/visualization/#{id}?overwrite=true" response = KibanaClient.call(:post, api_path, json) response end def self.gen_pie id, index_pattern, title, field, references = nil, query = nil references = [ { "id": index_pattern, "name": "kibanaSavedObjectMeta.searchSourceJSON.index", "type": "index-pattern" } ] if references.blank? query = { "query": { "query": "", "language": "kuery" }, "filter": [], "indexRefName": "kibanaSavedObjectMeta.searchSourceJSON.index" } if query.blank? state = { "title": title, "type": "pie", "params": { "type": "pie", "addTooltip": true, "addLegend": true, "legendPosition": "top", "isDonut": true, "labels": { "show": false, "values": true, "last_level": true, "truncate": 100 }, "dimensions": { "metric": { "accessor": 1, "format": { "id": "number" }, "params": {}, "aggType": "count" }, "buckets": [ { "accessor": 0, "format": { "id": "terms", "params": { "id": "string", "otherBucketLabel": "Other", "missingBucketLabel": "Missing" } }, "params": {}, "aggType": "terms" } ] } }, "aggs": [ { "id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {} }, { "id": "2", "enabled": true, "type": "terms", "schema": "segment", "params": { "field": field, "orderBy": "1", "order": "desc", "size": 5, "otherBucket": true, "otherBucketLabel": "Other", "missingBucket": false, "missingBucketLabel": "Missing" } } ] } json = { "attributes": { "description": "", "kibanaSavedObjectMeta": { "searchSourceJSON": query.to_json }, "title": title, "uiStateJSON": "{}", "visState": state.to_json }, "references": references } api_path = "/api/saved_objects/visualization/#{id}?overwrite=true" response = KibanaClient.call(:post, api_path, json) response end def self.gen_graph id, index_pattern, title, type, params, aggs, references = nil, query = nil references = [ { "id": index_pattern, "name": "kibanaSavedObjectMeta.searchSourceJSON.index", "type": "index-pattern" } ] if references.blank? query = { "query": { "query": "", "language": "kuery" }, "filter": [], "indexRefName": "kibanaSavedObjectMeta.searchSourceJSON.index" } if query.blank? state = { "title": title, "type": type, "params": params, "aggs": aggs } json = { "attributes": { "description": "", "kibanaSavedObjectMeta": { "searchSourceJSON": query.to_json }, "title": title, "uiStateJSON": "{}", "visState": state.to_json }, "references": references } api_path = "/api/saved_objects/visualization/#{id}?overwrite=true" response = KibanaClient.call(:post, api_path, json) response end def self.gen_dashboard id, title, panels, references, options = {}, query = nil query = { "query": { "query": "", "language": "kuery" }, "filter": [] } if query.blank? puts id puts title puts panels puts references puts options puts query json = { "attributes": { "description": "", "hits": 0, "kibanaSavedObjectMeta": { "searchSourceJSON": query.to_json }, "optionsJSON": options.to_json, "panelsJSON": panels.to_json, "timeRestore": false, "title": title }, "references": references } puts json api_path = "/api/saved_objects/dashboard/#{id}?overwrite=true" response = KibanaClient.call(:post, api_path, json) response end endGeneratorから
response = KibanaClient.gen_graph("test_graph", "index_name", "タイトル", "area", params, aggs, references, query)各変数はKibanaでグラフを作って、Inspectからテンプレ化すると楽
- 投稿日:2019-12-05T11:09:27+09:00
RailsとSwiftで動画アップローダーを作る?
まえがき
知らぬ間にまたアドベントカレンダーの季節ですね?
今年もノリと勢いが前のめりすぎて立候補しましたがギリギリです。
(いつになったら一日は30時間になるのか。。)
(ちなみに去年こんなの書きました✌?10分クッキング!誰でもインフルエンサーになれるinstabotの作り方?)今年のテーマは、動画アップロード機能です。
画像は馴染みがあるけど動画はやったことないなんて人、多くないでしょうか?
そんな私を含めた方達に向けて、ゆるく解説していこうと思います◎?こんなの作ります
機能は、動画選択・プレビュー・アップロード・最新の動画をフェッチして再生です。たぶんレアな記事なので是非お付き合いください?
(あといいね欲しいですね。。!もう押してもらって大丈夫です???)お勧めしたい人
- Ruby(Rails)やったことあるよ!
- HTTPリクエストなんとなくわかるよ!
- RailsのAPIモードちょっと触ってみたいよ!
- モバイルアプリやってみたいよ!
- Swiftやってみたいよ!
- Swift初心者だよ!
- 動画アップロードやってみたいよ!
- というか開いてくれた人 みんなですね!
(せっかく一年に一回のアドベントカレンダーってお祭りなので、みんな読んでね!おねがいします!)
ようこそ〜〜〜?
使用技術
サーバーサイド(API)にRuby
フロントエンド(モバイル)にSwiftの構成です。Ruby 2.6.2
Ruby on Rails 5.2.3
Swift 5.0.1
Xcode 10.2.1
(ちょっと古いので上げます?)まだまだやれるぜって人へ
実際にアプリをストアにあげるのに、Railsプロジェクトのデプロイは必須なのでHerikuデプロイとか挑戦してみてください?
【初心者向け】railsアプリをherokuを使って確実にデプロイする方法【決定版】いざ実装??
全体のザックリした流れ
- Swiftで動画アップローダーを作る
- SwiftでAPIクライアントを作る
- RailsでAPIサーバーを作る
- Swiftで動画プレイヤーを作る
~ 完成 ~
1. [Swift] 動画アップローダーの実装?
ザックリした流れ
- プロジェクトの作成
- シュミレーターにサンプル動画を追加
- 今回使用するファイルの説明
- 動画選択の機能を作成する
- 動画再生の機能を作成する
- 動画アップロードの機能を作成する
プロジェクトの作成
- Xcodeを起動する
- Create a new Xcode projectを選択する
- Single View Appを選択してNextを選択する
- Product Name(好きなアプリ名)を入力してNextを選択する
- 保存先を指定してCreateを選択する
これでSwiftプロジェクトの作成ができました!
シュミレーターにサンプル動画を追加
シュミレーターとは、書いたプログラムの動作を、mac内で確認するためのものです。
(Railsで動作確認する時、ブラウザにlocalhost:3000と入力して見る画面のようなものです。)シュミレーターには元々サンプル画像しか入っていないので、ここで動画を追加します。
まだ何もコードは書いていませんが起動(ビルド)してみましょう!
- 起動したいデバイス(自分はiPhoneXR)を指定して左上の再生ボタンを押します
![]()
- シュミレーターを起動したら真っ白な画面が出ますがメニューに戻り、写真アプリを開きます
- macのfinderから好きな動画を選択します(なければスマホなどからmacに送ってください)
- シュミレーターにドラッグアンドドロップで追加します
今回使用するファイルの説明
ここから実際に画面を作っていきますが、
先に今回使用するファイルについてザックリ説明しておきます。
Main.storyboard(自動生成・自作も可)
storyboardはUI部品(ボタンや画像表示するのに必要なパーツなど)を置いて、
直感的にレイアウトを作成するファイルです。
細かい機能やレイアウトを実装はこのファイルでは実装しきれません。あくまでレイアウトをザックリ構築するファイルです。ViewController.swift(自動生成・自作も可)
storyboardでカバーできない機能的な実装や細かいレイアウトを記述するファイルです。
例えば、ボタンが押された時の挙動やAPIから受け取ったデータをUIに受け渡す処理なんかを書きます。今回はVideoUploaderViewController.swiftを作成します。
APIClient.swift(自作)
バックエンド(APIサーバ)へのリクエストやレスポンスを受け取るのに使用します。Info.plist(自動生成・自作も可)
設定ファイルです。今回はここでフォトライブラリへのアクセス許可の設定をします。動画選択の機能を作成する
ここでやること
1. VideoUploaderViewController.swiftを作成する
2. Main.storyboardに動画選択用のボタンを追加する
3. ユーザーにカメラロールの使用許可をとる実装を追加する
4. ボタンをViewControllerに接続する
5. ボタンが押されたときにフォトライブラリを開いて動画を選択する
6. 選択された動画のサムネイルをViewControllerに表示する1. VideoUploaderViewController.swiftを作成する
基本的に、1ページを作るのにUI部品を配置するStoryboard(ViewController)と機能を実装するViewControllerがセットで必要です。
はじめに動画の選択からアップロードまでを行うページを作っていきます。
このページは
storyboardに自動生成されるMain.storyboard(ViewController)と、
ViewControllerは自動生成されるViewController.swiftをリネームして作成していきます。storyboardは自動生成なので、まずはViewController.swiftをリネームていきます。
Xcodeの左側にあるナビゲーターからプロジェクターナビゲーター
(左上のファイルアイコンを押すと出てきます。ファイルの一覧です)を選択し、
ViewControllerをクリックして名前をVideoUploaderViewController.swiftに変更してください。
次に、セットで使用するstoryboardに紐づいているViewController名を変更します。
左側のナビゲーターからMain.storyboardを選択します。ここにはViewControllerの白いパーツがあるだけかと思います。
この黄色いアイコンを選択してください。
右側のインスペクターで、Identity InspectorのCustom Classを変更します。
ここを先ほどのVideoUploaderViewController.swiftに変更すると接続が完了します。
2. Main.storyboardに動画選択用のボタンを追加する
Main.storyboardを開いているのでついでに動画選択のボタンを設置していきます。
これをドラッグアンドドロップでViewController内におきます。
青い淵の状態だとパーツが固定されていないのでUI部品に制約(縦横のサイズやトップからの距離などのルール)を追加していきます。
基本的に必要な制約は、縦横・座標(画面に対しての位置)です。width/height
widthを設定します。
Ctrを押しながらボタンを選択、ボタン内で選択をやめるとこのようなメニューが出てきます。
ここでwidthを選択してください。
赤くなっているのは無視してください。(ちゃんと制約が指定できたら青くなります!)
制約部分を選択すると右側にAttributes Inspectorが開きます。
ここでConstantを80にしてください。
同じようにして、heightのConstantを80で指定してください。
座標
次にボタンの座標を決めていきます。
画面のトップやボトムから距離を決めたり、画面の真ん中で指定したり方法は色々あります。
今回はトップからの距離と左からの距離を指定します。
先ほどと同じように、Ctrを押しながらボタンを選択、
今度はボタンの外までカーソルを引っ張り選択を解除します。
以下のメニューが出てくるのでTop Space to Safe Areaを選択してください。
これもConstantを548に変更してください。同じように画面の左からの制約を付けていきます。
メニューを開いたら、今度はLeading Space to Safe Areaを選択してください。
ここはConstantを71に指定してください。最後にbuttonという文字をクリックして、Selectに変更してください。
これでボタンの追加は終了です。(好きに色とか付けてもらったり、位置も自由で大丈夫です。)
(指定に中途半端な値を指定しているのは、デモで作ったアプリを
AutoLayoutで実装しているのですが、そのレイアウトに近づけるためです。(起動に使う端末はXRです。)
AutoLayoutとは、UI部品同士を相対的に配置して、どのサイズの端末でも同じような配置でレイアウトを実現できる機能です。
後でソースコードを載せるので参考にしてみてください??♀️)
3. ユーザーにカメラロールの使用許可をとる実装を追加する
カメラアプリをインストールした時にこのようなモーダルに遭遇することがあると思います。
ユーザーから、フォトライブラリにアクセスする許可を取らないと、
動画を選択することができないのでこれを実装していきます。まず、Info.plistでフォトライブラリを使用する利用目的を設定します。
アプリ名のディレクトリ以下のInfo.plistを開いてください。(テスト用のディレクトリにもあるので注意してください)
Information Property Listの横にある+ボタンを選択して、Privacy - Photo Library Usage Descriptionを入力してください。
行が追加されたら、Valueの部分には利用目的を記述します。
ここまででInfo.plistの設定は終わりです。
次に、アラートを出す実装をします。
iOS11以降.plistに設定してもアクセス権限の確認が自動的に行われなくなったので
意図的にタイミングを指定してアラート表示する必要があります。
最初の画面が表示されたタイミングでアラートを表示するようにしていきましょう。最初に開かれるページになる、VideoUploaderViewController.swiftを開いてください。
今回必要なフォトライブラリへアクセスするのでPhotosというフレームワークを使用します。
Appleが用意してくれているものなのでimport Photosを記述することで使用できるようになります。
以下のように追加してください。VideoUploaderViewController.swiftimport UIKit import Photos自動生成されるviewDidLoadの下に、confirmPhotoLibraryAuthentication()という関数を作成します。
ここで、フォトライブラリの使用許可の確認を行います。VideoUploaderViewController.swiftprivate func confirmPhotoLibraryAuthentication() { }privateは、このclass内でのみ呼び出しを許可したい時に使用します。
(railsでもストロングパラメーターを記述する時に使ったことがあるかと思います。同じやつです。)この関数内で、アクセス許可をされているかの現状の確認と許可されていなかった時の挙動を記述していきます。
VideoUploaderViewController.swiftprivate func confirmPhotoLibraryAuthenticationStatus() { //権限の現状確認(許可されているかどうか) if PHPhotoLibrary.authorizationStatus() != .authorized { //許可(authorized)されていない・ここで初回のアラートが出る PHPhotoLibrary.requestAuthorization { status in switch status { //もし状態(status)が、初回(notDetermined)もしくは拒否されている(denied)の場合 case .notDetermined, .denied: //許可しなおして欲しいので、設定アプリへの導線をおく self.appearChangeStatusAlert() default: break } } } }
PHPhotoLibrary.requestAuthorization
の時に先ほどのアラートが出現します。
その結果をstatusとしてクロージャー({})内に展開します。
このアラートは基本的に一度きりなので
アプリを閉じられたりして初回(notDetermined)のままだったり
アクセスを許可していない(denied)ユーザーに対して再度設定を促すアラートを出そうと思います。appearChangeStatusAlert()という関数を作成します。
confirmPhotoLibraryAuthenticationStatus関数の下に追加してください。VideoUploaderViewController.swiftprivate func confirmPhotoLibraryAuthenticationStatus() { //略 } //ここから private func appearChangeStatusAlert() { //フォトライブラリへのアクセスを許可していないユーザーに対して設定のし直しを促す。 //タイトルとメッセージを設定しアラートモーダルを作成する let alert = UIAlertController(title: "Not authorized", message: "we need to access photo library to upload video", preferredStyle: .alert) //アラートには設定アプリを起動するアクションとキャンセルアクションを設置 let settingAction = UIAlertAction(title: "setting", style: .default, handler: { (_) in guard let settingUrl = URL(string: UIApplication.openSettingsURLString) else { return } UIApplication.shared.open(settingUrl, options: [:], completionHandler: nil) }) let closeAction = UIAlertAction(title: "cancel", style: .cancel, handler: nil) //アラートに上記の2つのアクションを追加 alert.addAction(settingAction) alert.addAction(closeAction) //アラートを表示させる self.present(alert, animated: true, completion: nil) }ここまで書けたら、画面が読み込まれた際に呼び出されるviewDidLoad()から
confirmPhotoLibraryAuthenticationStatus()を呼び出してみましょう。VideoUploaderViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() // MARK: allow access to camera roll self.confirmPhotoLibraryAuthenticationStatus() }実際に起動して、動作を確認してみましょう。
Info.plistで指定した利用目的はここに出てきます!
Don't Allowを押すと先ほどタイトルなどを設定したアラートが出現します。
settingを押すと設定アプリも開けるようになってると思います。デバッグの注意点:
今回の実装では最初のアラートは初回起動時一回しか出てこないので、
再度確認したいときは、シュミレーターからアプリを削除してビルドし直してください。4. ボタンをViewControllerに接続する
ユーザーからフォトライブラリへのアクセス許可を貰えるようになったので、
ここからはフォトライブラリから動画を選択する機能を作っていきます。まずVideoUploaderViewController.swiftと先ほどstoryboardに配置したボタンを接続していきます。
Main.storyboardを開いてください。
optionを押しながら、VideoUploaderViewController.swiftを開きます。
左側にMain.storyboard、右側にVideoUploaderViewController.swiftが開けていればOKです。
storyboardでCtrを押しながらボタンを選択して、ViewcontrollerのviewDidLoadの上まで
カーソルを引っ張ってください。
選択をやめると、このようなメニューが出てくるかと思います。
以下のようにConnectionをAction
に変更して、
NameにdidTapSelectButton
と入力してConnectを押してください。
ConnectionにAction
を指定することで
ボタンをタップされた後に呼び出される関数を自動生成することができます。
ここまでで、VideoUploaderViewController.swiftと動画選択のボタンの接続は終わりです。
5. ボタンが押されたときにフォトライブラリを開いて動画を選択する
ここでやることは、UIImagePickerControllerを使って
フォトライブラリから動画を選択できるようにします。UIImagePickerControllerとは、
メディア周り(フォトライブラリにアクセスしたりやカメラを起動したり)の機能を簡単に扱えるようにするクラスです。Photosフレームワークをインポートしてあるだけで使用できます。
(今回はフォトライブラリのアクセス許可をとる時に既にインポートしてあるのですぐに使えます。)早速実装していきます。
VideoUploaderViewControllerクラスに、UIImagePickerControllerインスタンスを生成します。VideoUploaderViewController.swiftclass VideoUploaderViewController: UIViewController{ let imagePickerController = UIImagePickerController() //略 }次に、VideoUploaderViewController.swiftにselectVideo関数を作成します。
ここにフォトライブラリを開いて動画を選択する機能を実装していきます。VideoUploaderViewController.swiftprivate func appearChangeStatusAlert() { //略 } //ここから private func selectVideo() { //選択できるメディアは動画のみを指定 self.imagePickerController.mediaTypes = ["public.movie"] //選択元はフォトライブラリ self.imagePickerController.sourceType = .photoLibrary //実際にimagePickerControllerを呼び出してフォトライブラリを開く self.present(self.imagePickerController, animated: true, completion: nil) }最後に、自動生成したdidTapSelectButtonの関数からselectVideo()を呼び出すことで
ボタンがタップされた時にフォトライブラリを開き動画を選択することができるようになります。VideoUploaderViewController.swift//選択ボタンがタップされた時に呼び出される @IBAction func didTapSelectButton(_ sender: Any) { selectVideo() }ビルドして確認してみましょう。動画を選択できるようになったと思います。
6. 選択された動画のサムネイルをViewControllerに表示する
最後に選択した動画のサムネイルをVideoUploaderViewControllerに表示します。
少しやることが多いので、流れをまとめます。以下の通りです。
- Main.storyboardにサムネイルを表示するためのUIImageViewを置く
- UIImageViewをViewControllerに接続する
- ViewControllerをUIImagePickerControllerDelegateとUINavigationControllerDelegateに批准させる
- imagePickerControllerが閉じられる時に呼び出される関数を実装する
- 取得した動画のパスを元にサムネイルを生成するMain.storyboardにサムネイルを表示するためのUIImageViewを置く
Main.storyboardを開いてください。
今回はサムネイル(画像)を表示することが目的です。画像を表示するにはUIImageViewを使用します。
UIButtonの時と同じように、LibraryからUIImageViewを検索して追加します。
高さはConstantを250ポイントに指定してください。
横幅は画面いっぱいに指定してください。
画面いっぱいにするには、Ctrを押しながらUIImageViewを選択してカーソルをUIImageViewの外側まで持っていきます。
ここで選択を解除すると以下のメニューが出てきます。
Equal Widthsを選択すると画面に対して横幅がいっぱいになります。次に横軸の座標を指定します。今回は画面に対して中央にします。
同様にメニューを出してCenter Horizontally in Safe Areaを指定してください。
このように真ん中にUIImageViewが移動すると思います。
最後に縦軸の座標を指定します。今回は動画選択のボタンのトップよりUIImageViewのボトムが54ポイント上になるように指定します。
Ctrを押しながら、UIImageViewを選択してください。カーソルは選択ボタンまで持っていってメニューを表示させます。
ここでVertical Spaceを選択してください。
Constantは54ポイントを指定してください。UIImageViewをViewControllerに接続する
選択ボタンの時と同じようにMain.storyboardを開いて、隣にVideoUploaderViewcontroller.swiftを開いてください。
(optionを押しながらVideoUploaderViewcontroller.swiftを選択してください)
Ctrを押しながら、UIImageViewをVideoUploaderViewcontroller.swiftに繋いでください。
今度はConnectionはOutlet
のままで、Connectしてください。
以下のコードが生成されたら接続は完了です。
ViewControllerをUIImagePickerControllerDelegateとUINavigationControllerDelegateに批准させる
ここが一番わかりづらいと思います?
そもそもDelegate(デリゲート)とはSwiftでよく使われる考え方で、ここら辺の記事がわかりやすいです。
【swift】イラストで分かる!具体的なDelegateの使い方。
簡単にいうと、あるクラスが別のクラスに処理をまかせる(委譲する)ことです。
なんで批准という表現が使われるかという話は、例え話がわかりやすいです。
今回扱うデリゲートやプロトコルは、所謂条約のような決まり事です。
必ず守らなければならないルールがあったり、出来ることが増えたりします。
またそこには加盟する国々(ViewControllerなど)がいます。
この関係を国々が条約に批准するというように、
ViewControllerもデリゲートやプロトコルに批准すると考えると理解しやすいです!(個人談)今回は、デリゲートに批准することで、VideoUploaderViewControllerに
UIImagePickerControllerの関数を使って、選択した動画やサムネイルを受け取ってもらいます。実装方法は、まずクラスを定義している部分にUIViewControllerと同じように必要なDelegateを記述します。
次に、imagePickerControllerの代わりにVideoUploaderViewControllerが役割を肩代わりしますよ!って宣言をします。
VideoUploaderViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() self.imagePickerController.delegate = self //略これで批准完了です!
imagePickerControllerが閉じられる時に呼び出される関数を実装する
まず、選択された動画のurlを保持するための変数を定義します。
VideoUploaderViewController.swiftclass VideoUploaderViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { //ここを追加 private var videoUrl: NSURL?先ほどのデリゲートに批准したので、動画を選択した後imagePickerが閉じられる時に呼び出される関数を利用できるようになります。
VideoUploaderViewController.swift//imagePickerが閉じられる時に呼ばれる func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { //キーを指定して選択された動画のパスを取得する let key = UIImagePickerController.InfoKey(rawValue: "UIImagePickerControllerMediaURL") videoUrl = info[key] as? NSURL //動画の絶対パスを元にサムネイルを生成(generateThumbnailFromVideoの実装は後述します。) //先ほど接続したthumbnailImageViewのimageにサムネイルをセット thumbnailImageView.image = generateThumbnailFromVideo((videoUrl?.absoluteURL)!) //サムネイルの縦横比を変えずに長い辺を画面サイズに合わせる設定 thumbnailImageView.contentMode = .scaleAspectFit //imagePickerControllerを閉じる imagePickerController.dismiss(animated: true, completion: nil) }取得した動画のパスを元にサムネイルを生成する
先ほど後述すると書いたgenerateThumbnailFromVideoという関数を実装します。
VideoUploaderViewController.swiftprivate func generateThumbnailFromVideo(_ url: URL) -> UIImage? { //以下の3行で縦動画から画像を取り出しても横向きの画像にならないようにしてる let asset = AVAsset(url: url) let imageGenerator = AVAssetImageGenerator(asset: asset) imageGenerator.appliesPreferredTrackTransform = true //切り取るタイミングの指定 var time = asset.duration time.value = min(time.value, 2) //サムネイルの生成 do { let imageRef = try imageGenerator.copyCGImage(at: time, actualTime: nil) return UIImage(cgImage: imageRef) } catch { return nil } }この関数は
-> UIImage?
でサムネイルを返り値として指定しています。
生成に成功するとreturn UIImage(cgImage: imageRef)
で画像を返してくれます。
これが先ほど実装した関数のこの部分に返るのでサムネイルが表示されるようになります。
thumbnailImageView.image = generateThumbnailFromVideo((videoUrl?.absoluteURL)!)
ビルドして確認してみましょう!
動画選択後にサムネイルが表示されてればOKです。
動画選択の機能は以上です。
動画再生の機能を作成する
ここでやること
1. 動画再生用ボタンを設置する
2. ViewControllerに接続する
3. 動画再生の関数を実装する1. 動画再生用ボタンを設置する
選択ボタンの時と同じようにMain.storyboardに動画再生用のボタンを設置してください。
・横幅と高さは選択ボタンと一緒でconstantを80ポイント
・縦の座標は選択ボタンのcenterと同じ
・横の座標は画面に対してcenter
こんな感じです(急に色付きでスミマセン?)
2. ViewControllerに接続する
こちらも選択ボタンと同じようにVideoUploaderViewControllerに接続してください。
以下の関数ができればOKです!
3. 動画再生の関数を実装する
動画を再生するにあたって、再生プレーヤーが必要となります。
Appleが提供しているAVKitフレームワークに、
ビデオコンテンツを再生するためのインターフェイスが用意されているのでこれを使用していきます。VideoUploaderViewController.swiftimport UIKit import Photos //ここを追加 import AVKitVideoUploaderViewController.swiftprivate let imagePickerController = UIImagePickerController() //ここを追加 private let playerViewController = AVPlayerViewController()次に、動画再生を行う
playVideo(from url: URL)
を実装します。VideoUploaderViewController.swiftprivate func playVideo(from url: URL) { //プレイヤーに受けとったurlをセット let player = AVPlayer(url: url) //先ほど初期化したplayerViewControllerのプレイヤーに上記のプレイヤーをセット playerViewController.player = player //playerViewControllerの表示・再生 self.present(playerViewController, animated: true) { print("playing video") self.playerViewController.player!.play() } }最後に再生ボタンを押された後に
playVideo(from url: URL)
を呼び出します。VideoUploaderViewController.swift@IBAction func didTapPlayButton(_ sender: Any) { //選択された動画の絶対パスがオプショナル(nilの可能性がある)ので //guard(railsでいうunless)でパスがnilなら早期リターンにしてる guard let url = videoUrl?.absoluteURL else { return } playVideo(from: url) }ここまで実装できたら、ビルドして再生ボタンを押して確認してみましょう!
動画再生の機能は以上です。
動画アップロードの機能を作成する
ここでやること
1. 動画アップロード用のボタンを設置する
2. ViewControllerに接続する
3. 動画アップロードの関数を実装する1. 動画アップロード用のボタンを設置する
選択ボタンの時と同じようにMain.storyboardに動画アップロード用のボタンを設置してください。
・横幅と高さは選択ボタンと一緒でconstantを80ポイント
・縦の座標は選択ボタンのcenterと同じ
・横の座標は画面に右端対してconstantを71ポイント
こんな感じです。
2. ViewControllerに接続する
こちらも選択ボタンと同じようにVideoUploaderViewControllerに接続してください。
以下の関数ができればOKです!
3. 動画アップロードの関数を実装する
動画のアップロードを行う
uploadVideo()
を実装します。
今回アップロードするのは動画のみです。
アップロードに必要な情報は、
・選択された動画のurl
・選択された動画の名前
になります。VideoUploaderViewController.swiftprivate func uploadVideo() { //urlと名前がなければ早期リターンさせる guard let videoClipPath = videoUrl?.absoluteURL, //urlの最後がファイル名になる let videoClipName = videoUrl?.lastPathComponent else { print("not found video path or name"); return } //バックエンドにリクエストを送る //ここは次の章で実装するので(APIクライアント)エラーのままで大丈夫です。 API.postData(videoClipPath: videoClipPath, videoClipName: videoClipName) }
uploadVideo()
もアップロード用のボタンが押された時に呼び出されるようにしておきます。VideoUploaderViewController.swift@IBAction func didTapUploadButton(_ sender: Any) { uploadVideo() }この機能は、APIクライアントとバックエンドのAPIを作って完成します。
2. [Swift] APIクライアントの実装?
SwiftからRailsにデータを送信するための機能を作っていきます。が、
その前に。動画データの扱い方について
一回やったのですが動画変換はbase64だと重くてツライので、
APIのリクエストヘッダーのContent-Typeにmultipart/form-dataを指定して
動画をAppleの標準の拡張子.MOVのままバックエンドにアップロードできるように実装します。ザックリと用語の解説
base64
base64はバイナリーデータ(今回は動画)を
String(ASCIIテキスト: アスキーと読みます)に変換する方法です。
バイナリーデータをStringに変換できるのでjson形式で扱えて便利です。
有名な変換方法なので詳しい話はこちらをどうぞ!
base64ってなんぞ??理解のために実装してみた
弱点は、上でも書いた通りめちゃくちゃに重たいです。
理由はASCIIテキストに変換していく中でデータサイズが33%増加することです。
例えば、1枚66KBのサンタを変換するとこんな感じ
(ちなみに文字列はまだまだ続く。
base64は100MB未満のデータに適していると言われているのでこのサンタはちょろい方)
??base64エンコーダー
こんな感じなので動画を全部文字列で扱ったらbase64⇄動画の変換は地獄です。
(1分くらいの動画をbase64でコンソールに出力したらXcodeが固まった)ということで今回は不採用?
multipart/form-data
HTML4から導入された方法で、複合型のコンテンツをMIME Typeを指定することで形式を変えずに送信することができます。(textだったりjpegだったりいろんな形式のデータを一緒に送信することが可能ということ)
HTTPリクエストのヘッダー部分で指定するContent-typeの一種です。
(Content-typeとは「このリクエストの中身はこんな形式のデータが入ってますよ〜」って宣言。)
詳しくはこちら。[フロントエンド] multipart/form-dataを理解してみよう
今回送信するデータはvideo/quicktime(.MOVファイル)だけですが、拡張性があるのでmultipart/form-dataを指定しています。ザックリした流れ
- Alamofireのインストール
- APIClient.swiftの作成
- POSTリクエストを実装
- http通信の許可
Alamofireのインストール
APIクライアントを簡単に実装できるライブラリです。CocoaPodsでインストールします。
導入方法はこちらが詳しいので参考にしてください。(【Swift】CocoaPods導入手順)[https://qiita.com/ShinokiRyosei/items/3090290cb72434852460]
今回pod 'Alamofire', '~> 4.7.2'
を指定してください。
インストール後、Xcodeを閉じて
プロジェクトディレクトリ以下に出てくる.xcworkspaceファイルで開き直したら完了です。
APIClient.swiftの作成
Xcodeが開けたら、左側に表示されるナビゲーターからプロジェクト名のディレクトリを指定してください。
?この状態
左下の+ボタン > File... > Swift File の順で、APIClient.swiftというファイルを作ってください。
ナビゲーターのプロジェクト名のディレクトリ以下にファイルが追加されていたら完了です。POSTリクエストを実装
Alamofireを使って、APIClient.swiftにVideoのPOSTリクエストを書いていきます。
まずはAlamofireをインポートします。APIClient.swiftimport AlamofireAPIクライアントを書いていきます。
ここでやりたいのは、
- Content-type: multipart/form-dataでリクエストを作成
- 動画データの追加
- 送信(エラーハンドリング)
です。APIClient.swiftstruct API { //APIのエンドポイント。 static let baseUrl = URL(string: "http://localhost:3000/api/v1/videos")! static func postData(videoClipPath: URL, videoClipName: String){ //multipart/form-dataでデータを送信する Alamofire.upload(multipartFormData: { multipartFormData in //multipartFormDataオブジェクトに対してデータの追加を行う //withNameはrailsのActiveStorage側で保存するときのキーと同じ multipartFormData.append(videoClipPath, withName: "clip", fileName: videoClipName, mimeType: "video/quicktime") }, to: baseUrl) { encodingResult in //encodingが成功するとこのハンドラが呼ばれる switch encodingResult { case.success(let upload, _ ,_): print(upload) upload .uploadProgress(closure: { (progress) in //進捗率の取得 print("Upload Progress: \(progress.fractionCompleted)") }) case.failure(let error): print(error) } } }こんな感じでPOSTリクエストの実装は完了です。
http通信の許可
http通信を許可する
iOS9以降、意図的にドメインを許可する設定をしないとXcodeでhttp通信できなくなりました。
設定しないと以下のようなエラーを吐きます。。The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.今回、rails側のlocalhostを叩きたいのでこの設定が必要になります。
この記事がわかりやすいので設定してみてください??
【swift】XcodeでiOSアプリのhttp通信を許可する方法
[おまけ]
ここではバックエンドやフロントエンドでCORSを指定する時でいう
ワイルドカードの指定と同じようなことをやっているので(全ドメインを許可)
本番環境で必要になる場合は(herokuデプロイとか)Exception Domainで特定のドメインのみを許可してください。
iOS9でHTTP通信ができない時の解決法以上でAPIクライアントの実装は終わりです!
3. [Rails] APIサーバーの実装?
ザックリした流れ
- プロジェクトの作成
- モデルの作成
- コントローラーの作成
- ルーティングの作成
プロジェクトの作成
環境構築は割愛??
1. ターミナルを開いて$ cd /path/to/プロジェクトを作成したいディレクトリ
で移動します
2.$ rails _5.2.3_ new video_uploader_server --api --skip-test
を実行します(アプリ名はvideo_uploader_serverを好きな名前に変更してください)
オプション 内容 _5.2.3_
railsのバージョン指定(6以降だと色々面倒なので今回はこちら) --api APIモード --skip-test 今回テスト書きません。書く方は抜いてください その他optionはrails newの書き方について徹底解説!を確認してみてください。
モデルの作成
モデルを作成します。
今回必要なのは
- Videoモデル
- ActiveStrangeで使用するモデル
です。ActiveRecordのVideoモデルの作成
$ cd アプリ名
作成したプロジェクトに移動$ rails g model video
でVideoモデルを作ります(gはgenerateのgです。今回カラム使わないのでこれだけで大丈夫です。)ActiveStorageの設定を行うのでマイグレーションはまだ行わなくて大丈夫です!
続きをどうぞ!ActiveStorageの設定
今回扱うのは動画(.MOV)なのでActiveStorageを使って保存していきます。
Active Storage の概要に詳しく解説されているので参考にしてください。
1.$ rails active_storage:install
ログはこんな感じになります
2.$ rails db:migrate
でデータベースを作ります
3. お好きなエディタでプロジェクトファイルを開きます
4.video.rb
を開いて編集しますVideoモデルに紐付ける動画ファイルをclipという名前でActiveStorageから呼び出せるように指定します。
video.rbclass Video < ApplicationRecord #ここを追加 has_one_attached :clip endこれはActiveStoregeで使用するモデルとVideoモデルのリレーションを定義しています。
Videoモデルにカラムを追加しなくてもVideoモデルに動画を保存しているかのように
ActiveStorageで動画を保存することができるようになります。コントローラーの作成
$ rails g controller api/v1/videos
でコントローラーを作成します
v1はバージョン1という意味です。
このように階層を分けてAPIのバージョンを管理するプロジェクトが多いです。
今回は簡単シンプルな構成で機能追加も想定していないので無くても大丈夫です。videos_controller.rbを編集します
videos_controller.rbclass Api::V1::VideosController < ApplicationController # videoの保存 def create video = Video.save(video_params) if video.save render json: { status: 'ok' } else render json: { status: 'ng' } end end # videoの取得 def index # とりあえず最後に保存したもの一件だけを表示 video = Video.last.clip # ActiveStorageで保存したビデオのurlをjsonで返す url = url_for(video) render json: { url: url } end private def video_params params.permit(:clip) #clipはActiveStorageに保存する時のキー end endルーティングの作成
controllerの階層を考慮した上でvideos_controllerのルーティングが設定できれば問題ないです。
routes.rbRails.application.routes.draw do # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html namespace 'api' do namespace 'v1' do resources :videos end end endこんな感じ。今回使わないアクションが多いのでonlyで絞り込んでもらってもいいです。
これでルーティングの実装も終わりです。
Xcodeでアプリをビルドして、railsもサーバーを起動してアップロードの確認をしてください。[ちなみに]
swiftからのリクエストは、rails側のコンソールでこんな感じで受け取ってるのを確認できます。Parameters: {"data"=>"testVideo", "clip"=>#<ActionDispatch::Http::UploadedFile:0x00007f982a2e9c70 @tempfile=#<Tempfile:/var/folders/rk/n8_pgb7x3j18qzjq1g4_4j380000gn/T/RackMultipart20191204-82817-n4evni.MOV>, @original_filename="34153FC2-F496-4B63-B0EC-D005AA1BC8DB.MOV", @content_type="video/quicktime", @headers="Content-Disposition: form-data; name=\"clip\"; filename=\"34153FC2-F496-4B63-B0EC-D005AA1BC8DB.MOV\"\r\nContent-Type: video/quicktime\r\n">}ターミナルで
open /var/folders/rk/n8_pgb7x3j18qzjq1g4_4j380000gn/T/RackMultipart20191204-82817-n4evni.MOV
で送られてきた動画の再生ができたりします。4. [Swift] 動画プレイヤーの実装?
ザックリした流れ
- 最新の動画再生用ボタンを設置する
- ViewControllerに接続する
- 最新の動画をフェッチするリクエストを実装する
- 最新の動画を再生する
動画アップロード用のボタンを設置する
選択ボタンの時と同じようにMain.storyboardに動画アップロード用のボタンを設置してください。
・横幅は文字の長さ。高さは文字の大きさを24ポイント
・縦の座標は再生ボタンから42ポイント下
・横の座標は画面に右端対してcenter
こんな感じです。
(画像追加します!)ViewControllerに接続する
こちらも選択ボタンと同じようにVideoUploaderViewControllerに接続してください。
以下の関数ができればOKです!
(画像追加します!)最新の動画をフェッチするリクエストを実装する
APIClient.swiftに以下を追加してください。
APIClient.swift//completionを使うことで呼び出し側でリクエストと動画再生を同期的に扱えるようにしてる static func fetchLatestVideoUrl(completion: @escaping (URL) -> ()) { //レスポンスの型。今回はurlのみ struct FetchResult: Codable { let url: String } //今回パラメーターは特に必要ないので[:](空)で Alamofire.request(baseUrl, method: .get, parameters: [:]) .responseJSON { response in switch response.result { case .success: print("Success!") //レスポンスをFetchResultに変換する guard let data = response.data, let result = try? JSONDecoder().decode(FetchResult.self, from: data), //取得できたFetchResultオブジェクトのurl(String?)からURLを生成 let fetchedUrl = URL(string: result.url) else { return } //取得できたURLをcompletionに渡す completion(fetchedUrl) case .failure: print("Failure!") } } }最新の動画を再生する
最新の動画を再生するための関数
playUploadedLatestVideo()
を実装します。VideoUploaderViewController.swiftprivate func playUploadedLatestVideo() { //バックエンドからファイルのurlを返してもらう(http://localhost:3000で始まるもの) //先ほどのcompletionはここの{}(クロージャ)。この中でurlを受け取る API.fetchLatestVideoUrl() { url in //このurlを使ってプレビューと同様にplayVideo(from url: URL)でビデオを開く self.playVideo(from: url) } }最後にボタンが押されたタイミングで
playUploadedLatestVideo()
を呼び出さすように記述して完成です!VideoUploaderViewController.swift@IBAction func didTapLatestVideoButton(_ sender: Any) { playUploadedLatestVideo() }あとがき
お疲れ様でした!
今回は最低限の機能実装でした。
エラーハンドリングしてアラート出したり、一覧ページを作ったり、
削除機能つけたりお好みでいろいろ追加してみてください!あとそもそもアプリをストアにあげるとかですね!
気になったこととかもっといいやり方あるよとかご指摘は
お気軽にバシバシください??読んでくださりありがとうございました?
- 投稿日:2019-12-05T10:09:07+09:00
山手線で堂々とお気に入り風俗嬢の出勤情報を確認できるステルスサービス「Cyber Office クラウド」
1 解決すべき課題
ぼく「私はオキニ嬢が決まっている。その出勤を確認したいだけなんだ・・・・。」
でも電車では風俗サイトなんて見れない、自宅でも家族がいるし、嫁は相手にしてくれない、でも独りの時間にそんなサイトばっかり見て時間が過ぎ去るのは嫌だ!私が必要とするサービスを自分のために作ってみました。
1:見る場所問題
風俗関係のウェブサイトってどうしても見る場所を選びますよね。じっくり新人や新店舗を見たい人に関しては、専用サイトで見る必要がありますしそれでいいと思います。しかし、すでにお気に入り嬢が決まっていてその娘達の出勤だけ見たいという人には、XXXサイトを行き来するのは大変な苦労です。当然家族団欒の場や電車などの公共の場では見る事ができません。
2:時間の浪費問題
私はお気に入り嬢の出勤だけを見たいだけだったのに、気がつけばエロサイトをネットサーフィンしてしまい、大変な時間を費やしたことがあります。サクッと出勤だけ確認できれば他の事にも時間が使えると思いました。3:見た目の問題
掲載されているイメージがやはりセクシーすぎて、1の問題を引き起こします。もちろん1人で見る場合にはいいのですが、公共の場には向きません。以上の課題を解決したいと思います。
2-1 設計案
・ どこで見てても良いUI
・ 正確なお気に入り嬢の出勤情報を横断的に捕捉簡単ですがひとまずこれを目標にして制作しました。
2-2 使用技術と骨子
・正確な嬢の出勤情報は大手風俗情報サイトよるとも[Yorutomoさん]からスクレイピングして、データを解析する事にしました。
・フォームにyorutomoの嬢の個別ページを入力するだけで、自分だけのオキニ嬢出勤表が作れるようにしました。
・ 使用技術
Ruby on rails / Heroku / nokogiri / mechanize / Boot strap /devide3 使い方
・1:ビジネス管理アプリ風Uiにログインします。Deviseで管理画面を作っています。今のところ、confirmableは切っていますので、適当なメールアドレスでも登録できるでしょう。
2:上部メニュービジネスメンバーの追加からオキニ嬢のURLをよるともからコピペします。
よるとも側のページは個別の嬢の詳細ページでURLは以下のような感じのはずです。
https://www.yorutomo.net/tokyo/m2/a6/shop"店番号"/gal"女性番号"/3:いくつか自分のお気に入り嬢を登録すると店舗横断型のスケジュール表が完成します。
ユーザーインターフェイスは極力風俗関係の色を排除し、嬢の名前も初めの一文字のみの表示としています。なお迷いましたが、やはり予約はこのまましたいので電話番号は表示するようにしました。
「ペン」アイコンで編集・「ゴミ箱アイコンで」削除が可能です。
4 良かった事と課題
・ 電車でも人目を気にせずお気に入り嬢の出勤を確認できるようになった。
・ 公式サイト、風俗情報サイトをサーフィンしまくる時間が削減できた。[課題]
・ スマホにしっかり対応するUl設計。
・ 嬢の名前をはっきりと出すとステルス効果が薄れるので、現在は一文字表示にしているが良い方法はないものか。もし需要があればURL公開しますので使ってみてください。技術としては今あるものを重ね合わせた一般的なもので作っています。
- 投稿日:2019-12-05T10:09:07+09:00
どこでも堂々とお気に入り風俗嬢の出勤情報を確認できるステルスサービス「Cyber Office クラウド」
1 解決すべき課題
ぼく「私はオキニ嬢が決まっている。その出勤を確認したいだけなんだ・・・・。」
でも電車では風俗サイトなんて見れない、自宅でも家族がいるし、嫁は相手にしてくれない、でも独りの時間にそんなサイトばっかり見て時間が過ぎ去るのは嫌だ!私が必要とするサービスを自分のために作ってみました。
1:見る場所問題
風俗関係のウェブサイトってどうしても見る場所を選びますよね。じっくり新人や新店舗を見たい人に関しては、専用サイトで見る必要がありますしそれでいいと思います。しかし、すでにお気に入り嬢が決まっていてその娘達の出勤だけ見たいという人には、XXXサイトを行き来するのは大変な苦労です。当然家族団欒の場や電車などの公共の場では見る事ができません。
2:時間の浪費問題
私はお気に入り嬢の出勤だけを見たいだけだったのに、気がつけばエロサイトをネットサーフィンしてしまい、大変な時間を費やしたことがあります。サクッと出勤だけ確認できれば他の事にも時間が使えると思いました。3:見た目の問題
掲載されているイメージがやはりセクシーすぎて、1の問題を引き起こします。もちろん1人で見る場合にはいいのですが、公共の場には向きません。以上の課題を解決したいと思います。
2-1 設計案
・ どこで見てても良いUI
・ 正確なお気に入り嬢の出勤情報を横断的に捕捉簡単ですがひとまずこれを目標にして制作しました。
2-2 使用技術と骨子
・正確な嬢の出勤情報は大手風俗情報サイトよるとも[Yorutomoさん]からスクレイピングして、データを解析する事にしました。
・フォームにyorutomoの嬢の個別ページを入力するだけで、自分だけのオキニ嬢出勤表が作れるようにしました。
・ 使用技術
Ruby on rails / Heroku / nokogiri / mechanize / Boot strap /devide3 使い方
・1:ビジネス管理アプリ風Uiにログインします。Deviseで管理画面を作っています。今のところ、confirmableは切っていますので、適当なメールアドレスでも登録できるでしょう。
2:上部メニュービジネスメンバーの追加からオキニ嬢のURLをよるともからコピペします。
よるとも側のページは個別の嬢の詳細ページでURLは以下のような感じのはずです。
https://www.yorutomo.net/tokyo/m2/a6/shop"店番号"/gal"女性番号"/3:いくつか自分のお気に入り嬢を登録すると店舗横断型のスケジュール表が完成します。
ユーザーインターフェイスは極力風俗関係の色を排除し、嬢の名前も初めの一文字のみの表示としています。なお迷いましたが、やはり予約はこのまましたいので電話番号は表示するようにしました。
「ペン」アイコンで編集・「ゴミ箱アイコンで」削除が可能です。
4 良かった事と課題
・ 電車でも人目を気にせずお気に入り嬢の出勤を確認できるようになった。
・ 公式サイト、風俗情報サイトをサーフィンしまくる時間が削減できた。[課題]
・ スマホにしっかり対応するUl設計。
・ 嬢の名前をはっきりと出すとステルス効果が薄れるので、現在は一文字表示にしているが良い方法はないものか。もし需要があればURL公開しますので使ってみてください。技術としては今あるものを重ね合わせた一般的なもので作っています。
- 投稿日:2019-12-05T10:09:07+09:00
堂々とお気に入り風俗嬢の出勤情報を確認できるステルスサービス「Cyber Office クラウド」
1 解決すべき課題
ぼく「私はオキニ嬢が決まっている。その出勤を確認したいだけなんだ・・・・。」
でも電車では風俗サイトなんて見れない、自宅でも家族がいるし、嫁は相手にしてくれない、でも独りの時間にそんなサイトばっかり見て時間が過ぎ去るのは嫌だ!私が必要とするサービスを自分のために作ってみました。
1:見る場所問題
風俗関係のウェブサイトってどうしても見る場所を選びますよね。じっくり新人や新店舗を見たい人に関しては、専用サイトで見る必要がありますしそれでいいと思います。しかし、すでにお気に入り嬢が決まっていてその娘達の出勤だけ見たいという人には、XXXサイトを行き来するのは大変な苦労です。当然家族団欒の場や電車などの公共の場では見る事ができません。
2:時間の浪費問題
私はお気に入り嬢の出勤だけを見たいだけだったのに、気がつけばエロサイトをネットサーフィンしてしまい、大変な時間を費やしたことがあります。サクッと出勤だけ確認できれば他の事にも時間が使えると思いました。3:見た目の問題
掲載されているイメージがやはりセクシーすぎて、1の問題を引き起こします。もちろん1人で見る場合にはいいのですが、公共の場には向きません。以上の課題を解決したいと思います。
2-1 設計案
・ どこで見てても良いUI
・ 正確なお気に入り嬢の出勤情報を横断的に捕捉簡単ですがひとまずこれを目標にして制作しました。
2-2 使用技術と骨子
・正確な嬢の出勤情報は大手風俗情報サイトよるとも[Yorutomoさん]からスクレイピングして、データを解析する事にしました。
・フォームにyorutomoの嬢の個別ページを入力するだけで、自分だけのオキニ嬢出勤表が作れるようにしました。
・ 使用技術
Ruby on rails / Heroku / nokogiri / mechanize / Boot strap /devide3 使い方
・1:ビジネス管理アプリ風Uiにログインします。Deviseで管理画面を作っています。今のところ、confirmableは切っていますので、適当なメールアドレスでも登録できるでしょう。
2:上部メニュービジネスメンバーの追加からオキニ嬢のURLをよるともからコピペします。
よるとも側のページは個別の嬢の詳細ページでURLは以下のような感じのはずです。
https://www.yorutomo.net/tokyo/m2/a6/shop"店番号"/gal"女性番号"/3:いくつか自分のお気に入り嬢を登録すると店舗横断型のスケジュール表が完成します。
ユーザーインターフェイスは極力風俗関係の色を排除し、嬢の名前も初めの一文字のみの表示としています。なお迷いましたが、やはり予約はこのまましたいので電話番号は表示するようにしました。
「ペン」アイコンで編集・「ゴミ箱アイコンで」削除が可能です。
4 良かった事と課題
・ 電車でも人目を気にせずお気に入り嬢の出勤を確認できるようになった。
・ 公式サイト、風俗情報サイトをサーフィンしまくる時間が削減できた。[課題]
・ スマホにしっかり対応するUl設計。
・ 嬢の名前をはっきりと出すとステルス効果が薄れるので、現在は一文字表示にしているが良い方法はないものか。もし需要があればURL公開しますので使ってみてください。技術としては今あるものを重ね合わせた一般的なもので作っています。
- 投稿日:2019-12-05T08:40:48+09:00
SlackBotにGIFを検索して流す機能を実装したら治安が崩壊した話
Qiitaご利用の諸君はおそらく社内でのコミュニケーションにSlackを利用していることだろう。
- 特に開発しなくても便利に使える便利なアプリが山程ある
- GitHubやGitLabの更新をメッセージとして流したりIssueなどをその場で発行できる
- Emoji無限(?)に増やせるたのしい!!!
これだけの利点があるのだから今更強烈な事情がない限りChatOps基盤にSlackを導入するのはほぼ必然と言い切ってもいいだろう。
そしてSlackではもっとコアな機能を持ったボットなどを開発する人たち向けにAPIを用意している。
今回はSlackのAPIの一つ、RealTimeMessageAPIを用いて開発していたSlackのBotにとある機能を実装したこと、リリースをした話をしようと思う。
新機能の概要
Botがinviteされているチャンネルで
特定の書式(今回は
put_gif
)+検索文字列と飛ばすとBotがそれを識別して適当なAPIからGIF画像のリンクを引っ張り、それを投げるというような機能を実装しようという話がチャンネル内で上がった。
丁度卒業研究が終わったところで暇だったので片手間で実装、そのままリリースすることにした。
実装の概略
GIF検索に利用するAPI
今回はtenor APIの提供するAPIを使う。
似たようなAPIにGIPHYがあるが、使ってみるとあまり検索結果がよくなかった(設定が悪かった?)ので移行した。利用する言語など
アプリケーション全体の実装をRuby(v2.6.5)で行った。
また、RTM APIとの相互通信のためにWebsocketを使うため、faye/websocket
も利用させてもらっている。デプロイ
個人で持っているEC2インスタンスの中にdocker及びdocker-composeを使って置いた。
GIFを拾ってくる部分の関数に関して
機能のコア部分だけ切り抜いてきた。
これをメソッドとして入れてやればGIFのリンクを返す関数ができるgif_get.rb# frozen-string-literal: true def gif_get(search_query) require 'http' # 環境変数は`docker-compose.yml`経由で`.env`を参照している tenor_api_key = ENV['TENOR_API_KEY'] response = JSON.parse(HTTP.get('https://api.tenor.com/v1/search', params: { key: tenor_api_key, q: search_query, limit: 50 })) # 検索結果が0の場合は'SearchCount: 0`とだけ返して、 # それ意外の場合は `Array#sample` でランダム抽出してURLを返す if response['results'].empty? 'SearchCount: 0' else response['results'].sample['media'][0]['gif']['url'] end end実際の成果物
ローカルでコンテナを展開しているので少し応答が遅れてしまっているが、本番のEC2上においているコンテナは1秒以内で応答を返してくれるので常用可能な機能になっている。
ここからが本題
で、弊部のワークスペースに入れた結果
なんだよこれ
TAGは関係ねえぞ
THE 世紀末と言わんばかりに遊ばれてしまった。事実今日だけで300くらいはメッセージの送受信があったように思えるこの機能を一般的なグループが入れて良いものか
結論から言うと進捗阻害にしかならないのでやめておいたほうがいい(あたりまえ)
今回は非公認のサークル組織であるためにこんなもんがまかり通っているが、汎用性はともかくとして実はこれ以外に 色々モラル的にアレなやつまで出てきてしまった ことがあった。
他の方々などはやるのであればAPIアクセス時のリクエストパラメータに
contentfilter
というキー項目があったのでそちらをmeddium
なりhigh
なりに指定してフィルタリングをかけたほうが良い。逆にSlackにセンシティブなもんが出したい!みたいな人は
off
でもいいんじゃないだろうか。覗き見られたら死亡だけど。最後に
開発しているSlackBotはソースコードをGitHubにて公開しているのでSlackを面白い感じにしたいサークルなどがあれば触っていただければ、と思います
nkc-ug/NKCUG_Slack_BotPRや新機能導入のIssueなどは弊部部員以外の方も歓迎します。
ただし導入して進捗が落ちたとかの文句は受けないぞ。
- 投稿日:2019-12-05T08:29:16+09:00
RailsエンジニアがReactNativeエンジニアになるにあたって苦しんだところワースト3
はじめに
こんにちは、株式会社OKANで「おかんPay」というプロダクトのPdMをしながらも、自らバリバリReact Nativeで実装している かなすぎ と申します。
元々は、RailsでOKANの社内システムを構築していたサーバーサイドエンジニアだったのですが、志願してReact Nativeを使うアプリエンジニアになってから約半年が立ちました。
そこで、RailsエンジニアがReact Nativeを扱うアプリエンジニアになる時に苦しんだ箇所をまとめることで、自分と同じようにReact Nativeを新たに勉強する人の助けになればと思いながら記事を書きました。想定読者
- Railsエンジニアだが、これからReactNativeアプリもつくってみたいなって思っている人
- React.js, Vue.jsなどフロントエンドフレームワークをがっつり実装したことないけど、React Nativeでアプリ作ってみたい人
RailsエンジニアがReactNativeエンジニアになるにあたって苦しんだところワースト3
RailsではJavascriptも使うことはあったのですが、MVCでいうViewのDOM操作をjQueryで軽く行うくらいの経験値であったので、ES6でがっつりロジックを書くというのも覚えることが多くて大変でした。また、Vue.jsやReact.jsなどで用いられる状態管理や、React.js特有のライフサイクルメソッドやJSX。そして、ウェブ開発では存在しないアプリ特有のDeepLinkや画面遷移の方法なども大きく異なっているので、キャッチアップに苦労しました。
ワースト1位:ES6(ES2015)
React NativeはES6で書きます。Javascriptは、ES6以降とそれより前では使える機能は大きく変わりました。現在流行りのフロントエンドのフレームワークであるAngular.js、React.js、Vue.jsなどでも、基本的にはES6以降の書き方で書くことが多いです。JavascriptはViewの見た目を動的に変える軽量な言語という認識でしたが、ES6を勉強してからは、認識が大きく変わりました。
イマドキのJavaScriptの書き方2018 - Qiita
ES2015(ES6) 入門 - Qiitaアロー関数
ES6以前では、関数を定義する時は、functionを使用していましたが、ES6以降では、アロー関数を用います。記号ばっかりで最初は慣れないかと思いますが、慣れれば、短い記述で関数を定義できるので非常に便利です。()を省略できたり、様々な記述パターンがあるので、引数の数など場合によって使いわけましょう。
// 何もしない関数 () => {} // 引数が一つだったら「()」不要 value => {} // ブロックないが1行しかないときは、「{}」不要 (a, b) => a + b // オブジェクトは「()」が必要 (a, b) => ({ a: b, b: a }) // 複数行はいつも通り (a, b) => { const c = a * 100; return { c, d: b / 100, }; }アロー関数
JavaScript アロー関数を説明するよ - Qiita
アロー関数を使って効率的にJavaScriptを記述するimport / export
モジュール毎に機能を管理する時に使います。モジュールで機能を管理することによって、他コードとの依存性を少なくして保守性を高めたり、変数名の競合を防いだり、汎用性の高いモジュールをつくって同じコードを繰り返すのを防いだりできます。
import React from 'react'; import { View, Text, StyleSheet, FlatList, Image, TouchableHighlight, Alert, Linking, } from 'react-native';export default ItemList; export const calories; export function pay(){...} export class Payment {...}import
export
ES6のexportについて詳しく調べた - Qiita非同期処理 async / await
hidouki.js// 非同期のpayメソッド const pay = async (amount) => { const items = await getItems(); const totalAmount = amount + tax; try { await this._payByCreditcard(totalAmount, items); return true; catch(e) { console.warn(e); return false; } };非同期処理には、かなり苦しめられました。Rubyは同期処理できる言語なので、コードを書いたら上から順に実行されます。しかし、Javascriptは非同期であり、たとえばAPIをコールしても、その結果を待たずに、次の行のコードの処理を進めることができてしまうのです。そこで、Promiseオブジェクトを用いて非同期処理を行い、asyncで非同期関数を定義し、awaitで非同期の結果が帰ってくるまで待つことができるのです。
JavaScriptの同期、非同期、コールバック、プロミス辺りを整理してみる - Qiita
【JavaScript入門】誰でも分かるPromiseの使い方とサンプル例まとめ! | 侍エンジニア塾ブログ(Samurai Blog) - プログラミング入門者向けサイト
async/await 入門(JavaScript) - Qiitaワースト2位:React
React NativeではReactを使ってAndroidアプリとiOSアプリを作ることができます。したがって、React自体の状態管理であったり、JSXなどReact独特の考え方・概念をキャッチアップする必要があります。フロントでReactを使用したことがあれば問題ないのですが、RailsをAPIモードで使用していた経験もなく、SlimでViewを書いていたので、Reactの概念を理解するのも大変でした。
React Native · A framework for building native apps using React
React - ユーザインターフェース構築のための JavaScript ライブラリComponent
ReactはComponent志向で作られます。Railsエンジニアだと、Componentとは?というところのキャッチアップから始める必要があります。Componetをざっくり説明すると、UIを独立させて再利用できるようにして、propsという任意の値を親要素から受け取り画面上に表示させるものです。この文章だけ読んでもわけが分からないと思うので、サンプルコードなどを見ながら理解すると、理解が深まると思います。
// ■ Reactのコンポーネントたち // 【Component】 // Stateを持っていて、再レンダリングの条件を自分でチューニングしたいときに使う。 class FooComponent extends React.Component { render() { return <View />; } } // 【PureComponent】 // Stateを持っていて、再レンダリングの条件をチューニングしないときに使う。 class BarComponent extends React.PureComponent { render() { return <View />; } } // 【Functional Component】 // Stateを持たなくて良い場合に使う。 const FuncComponent = props => { return <View />; };フロントエンドのコンポーネント設計に立ち向かう - Qiita
状態管理(state)
stateとは、Componentの状態を管理するデータです。たとえば、isPayableというフラグがたっていたら支払いのロジックを動かすということをDBでフラグを管理しなくても、フロント側で管理することができるのです。Reactでは、eduxという状態管理のライブラリを使用することが多いのですが、弊社のアプリは軽量なアプリなのでunstatedというライブラリを用いて状態管理をしています。個人的には、Reduxは冗長になってしまうので、unstatedの方が直感的でわかりやすくオススメです。
[React Native] 基本を学ぶ - Stateでコンポーネントの状態を管理する | Developers.IO
コンポーネントの state - React
jamiebuilds/unstated
次の状態管理はReduxをやめてunstatedにする理由 - Qiitaprops
Componentは、propsという任意の引数(正確には引数ではありません)を受け取って、値を当てはめたReact要素を返します。初めてReactを勉強した時は、stateとpropsをごっちゃにしてしまい、それぞれの役割を正確に理解できませんでしたで苦労ポイントです。
[React Native] 基本を学ぶ - Propsでコンポーネントのパラメータを設定する | Developers.IO
コンポーネントと props - ReactJSX
JSXはReactのComponent内でのXML風の構文です。React.jsでは、HTMLでお馴染みの
<h1>
タグなどが利用できるのですが、React Nativeでは、<View>
や<Text>
といった見慣れないタグを扱います。さらに、アプリエンジニアの方は理解しやすいそうなのですが、とかとかアプリ独特のComponentを使用する必要があるので、実際に動かしてみれば、「あーこれか。アプリで使ったことある」と思うのですが、Componentの名前などを覚えるのには一苦労しました。ActivityIndicator · React Native
class Payment extends React.Component { // ここがstateで状態をComponent内で管理している state = { isPayable: true, items: {}, totalAmount: 0, }; _onPressPayment = () => { { isPayable } = this.state; if (isPayable) { const result = await pay(amount); if (result) { this.setState({ isPaylable: false}); } } } render() { return ( // <Text>タグや<TouchableHighLight>がJSX // onPress={this._onPressPayment}がprops <TouchableHighlight onPress={this._onPressPayment}> <Text>支払いをする</Text> </TouchableHighlight> ) }; }ライフサイクルメソッド
ここが個人的には一番ReactNativeエンジニアになって辛かったです。未だに、あれこれってComponentDidMountであってるよね?って確認したくなります。Reactでは複数回renderメソッドが走るので、どのタイミングではどんなデータを用意してなければならないといったことも考慮しなくてはならないので、奥が深いです。さらにReactHooksでの書き換えも「おかんPay」では行っているので、useEffectなどの使い方は悪戦苦闘しています。
lifecycle.js// 色々なライフサイクルメソッドがあります。 componentDidMount() componentDidUpdate(prevProps, prevState, snapshot) componentWillUnmount()【React Native】React Native ライフサイクルメソッドについて|Fresopiya
React component ライフサイクル図 - Qiitaワースト3位:ReactNativeアプリやアプリ開発特有の概念
Web開発では存在しない概念であったり、Web開発の場合と概念が異なることがあり、Railsエンジニアの方にとっては、新しく勉強することが多いです。そこで、自分がReactNativeエンジニアになるにあたって特に苦労したことをまとめました。
React Navigationの画面遷移
画面遷移については、Railsでもある概念です。Routes.rbでルーティングを設定して、redirectやrenderを用いて画面遷移を実現します。しかし、ReactNativeなどアプリの開発では、画面を重ねていく、つまりStackしていくといったイメージの方がしっくりくると思います。RailsでWeb開発だけをしていた自分にとっては、なかなか理解するのに時間がかかるところでした。ライブラリによっても仕様は異なるのですが、弊社ではReact Navigationを使用しています。
React Navigation · Routing and navigation for your React Native apps
React Navigation 3.x チートシートつくってみた - QiitaDeepLinkとかLinking
アプリ同士の連携をするのには、DeepLinkやLinkingというモジュールをReact Native開発では使います。アプリエンジニアにとってはお馴染みの概念なようなのですが、Railsエンジニアの自分にとっては、未知の世界でした。urlを叩いたりするとアプリを開いたりする機能はこうやって実現するのかと素直に感心した領域です。日本語のドキュメントが少ないので、調べるのも英語だったので一苦労でした。今回紹介するのは、React NavigationのDeepLinkになります。
Deep linking · React Navigation
Linking · React Native
ディープリンクをめぐる歴史とReact NativeにFirebase Dynamic Linksを導入する手順 - KitchHike Tech Blogアプリのリリース
iOSとAndroid共にリリースを何度も経験をしているのですが、慣れれば作業なのですが、一番初めのリリースはこんなにも時間がかかるものなのかと驚愕しました。また、弊社プロダクトの「おかんPay」のReactNative版を初リリースしようとしたタイミングでAndroidの審査も始まったらしいというタイミングだったので、てんやわんやでした。さらに、Google Play ConsoleのUIや仕様はかなり頻繁に更新されてしまうので、Qiitaの記事なども最新記事を追わないないと、「そのボタンどこにあるの?」みたいなことは多発しました。初めてのアプリリリースには、余裕をもったスケジュールにすることをオススメします。
iPhoneアプリ申請やAppleの審査に関するメモ - Qiita
Androidアプリにも審査がされるようになった件 - Qiita
React NativeでiOS, Androidのストア公開のTips - QiitaiOSとAndroidのレイアウト差分
iOSとAndroidでは、PickerやCalendarで日付を選択する時など、多くの場合で、レイアウトが異なります。ReactNativeたった1つの言語でiOSとAndroidの両方のアプリを作れることは非常に大きなメリットではあるのですが、両者を全く同じに扱って言い訳ではなく、レイアウトをPlatformによって変更する必要があります。
また、エミュレータと実機の差分もあったり、Androidでは、HuaweiやSonyといった機種依存でレイアウトが変わったり、Androidのバージョンによってもレイアウトが崩れたりします。したがって、Androidすべての機種で実機テストをすることはできないので、レンタルサービスなどを用いてテストしたりしています。
import {Platform, StyleSheet} from 'react-native'; const styles = StyleSheet.create({ width: Platform.OS === 'ios' ? 200 : 100, });【React-Native】iOS, Android両対応する。 - Qiita
配備機種一覧 - 実機検証用スマホ・モバイルデバイスレンタル|テスト受託サービスのケータイラボ最後に
いかがだったでしょうか?Railsエンジニアには馴染みのない概念が多かったと思います。
この記事だけでは、もちろん十分ではないものの、自分がここ半年で苦労した箇所を中心にまとめてあるので、何かの助けになれば幸いです。次は、SocialDogのアプリ開発している@t0m0120さんです。
私もSocialDogは、Twitterを活用するのに利用させてもらっています。どんな記事が読めるのか楽しみです!
- 投稿日:2019-12-05T03:56:17+09:00
【Rails】bundle installをしたら大量のファイルを作る問題
初めに
bundle installをした際にgithubDesktopに新しいファイル生成10000ファイルのようにたくさんのファイルを生成したことはあるでしょうか。この問題に時間を使ってしまったので書き残します。
結論
bundle installをした際に大量にファイルが生成されるのは問題ではないです。正常な挙動です。本来、railsのgemのシステムは大量のファイルの上で成り立っておりそのファイルの数を普段見ることはないですがgithubDesktopでその大量の変更を見ることができるのが問題と言えるでしょう。
原因
私は普段はgem fileにgemを追加した後に
bundle install
とターミナルに打つことでinstallをしているのですが、qiitaの記事を真似していてbundle install --path vendor/bundle
と入力しました。この後に続く表記はbundle installしたファイルをどこに保存するのかということを指定しているもので本来は毎回このように打つことが正しいようです。ただしこのように打ったことでgithubの管理下に置かれてしまったらしく、省略してbundle install
をしても大量のファイルがgithubDesktop上に表示されるようになりました。解決
.bundle
とvendor/
を一度ディレクトリから消すことで解決できます。
もう一度bundle install
をしたら元の挙動に戻ると思います。
- 投稿日:2019-12-05T00:54:45+09:00
あ!こんな引数とれたんだっていう Ruby のメソッド 8選
ENECHANGE で勤務している cuzic です。
これは Ruby Advent Calendar 2019 の 5日目の記事です。
今日は Ruby のメソッドの中で、「あ、こんな引数とることができたんだ」と
個人的に感じたメソッドをいくつか紹介します。
self.class::CONSTANT
実は
::
演算子の左辺には式を指定できます。下記のようなコードがあったとします。
class A NUMBER = 1 def initialize @number = A::NUMBER end endこれは下記のように書くことができます。
class A NUMBER = 1 def initialize @number = self.class::NUMBER end endこのように
::
演算子の左辺として self.class のように式を指定することができます。上記の例であれば
a = A.new p a.class::NUMBER #=> 1のように
self.class
以外も書くことができます。array.values_at(range1, range2)
引数で指定されたインデックスの要素を配列として返すメソッド Array#values_at ですが、
Range を複数個、引数としてとることができます。array = ('a'..'z').to_a array.values_at(0..2, 10..12) => ["a", "b", "c", "k", "l", "m"]ENECHANGE ではさまざまな電気料金プランの料金計算を行っているのですが、
中には季節によって電気代が違うものの、春と秋は同じ単価という電気料金プランがあります。
春と秋の電気使用量を取り出すとき、このように range を複数引数として与えて
values_at メソッドを使うと簡潔になります。string[regex, index]
文字列の [] メソッドは第1引数に正規表現、第2引数に index を指定することで、
正規表現の capture を簡単に取り出すことができます。str = "Hello, World!" str[/(.)\1/] #=> "ll" str[/(.)\1/, 0] #=> "ll" str[/(.)\1/, 1] #=> l region = "ap-northeast-1a" region[/^(.+).$/, 1] #=> "ap-northeast-1"time.to_s(:db)
Ruby on Rails に含まれる Active Supportの中では
Time#to_s が拡張されており、さまざまな引数を
とることができます。例を紹介します。
time = Time.current time.to_s(:db) => "2019-12-03 13:17:17" time.to_s => "2019-12-03 22:17:17 +0900" time.to_s(:time) => "22:17" time.to_s(:number) => "20191203221717" time.to_s(:short) => "03 Dec 22:17" time.to_s(:long) => "December 03, 2019 22:17" time.to_s(:long_ordinal) => "December 3rd, 2019 22:17" time.to_s(:rfc822) => "Tue, 03 Dec 2019 22:17:17 +0900" time.to_s(:iso8601) => "2019-12-03T22:17:17+09:00" time.to_s(:nsec) => "20191203221717054977424" time.to_s(:usec) => "20191203221717054977"特に
:db
が便利です。他には、
nsec
はわりと使いどころがあるわりに、あまり知られていない気がします。Array#uniq{|item| ... }
Array#uniq はブロックを引数としてとることができます。
そのブロックを評価した値が重複しないような配列を取得することができます。
[*2..5, *"1".."4"].uniq(&:to_i) => [2, 3, 4, 5, "1"]enumerable.first(n), enumerable.last(n)
Enumerable モジュールの first や last メソッドは引数として
取り出す個数をとることができます。range = "a".."z" range.first(3) #=> ["a", "b", "c"] range.last(3) #=> ["x", "y", "z"] postcode = "1300012" postcode_1 = postcode.slice(0, 3) #=> "130" postcode_1 = postcode[0, 3] #=> "130" postcode_1 = postcode[0..2] #=> "130" postcode_1 = postcode[0...3] #=> "130" postcode_1 = postcode.first(3) #=> "130"enumerable.to_h{|item| [item["key"], item["value"]}
Enumerable モジュールの to_h メソッドは引数としてブロックを
とることができます。(Ruby 2.6 以降)例えば、下記のようなコードを実行できます。
(1..9).to_h{|i| ["%02d" % i, i]} => {"01"=>1, "02"=>2, "03"=>3, "04"=>4, "05"=>5, "06"=>6, "07"=>7, "08"=>8, "09"=>9}これまで、
(1..9).map{|i| ["%02d" % i, i]}.to_hとすることで、同様の効果がありましたが、より簡潔かつ効率よくハッシュを生成することかできます。
Array.new(n){|index| ... }
Array.new は配列の数を引数にとれるだけでなく、ブロックを引数としてとることができます。
例えば、下記は ENECHANGE で実際に使われていた month_from から month_to までで指定される
季節に該当するかどうかを判定するメソッドです。def in_season?(month_from, month_to, month) month_from = ( month_from || 1 ) - 1 month_to = ( month_to || 12 ) - 1 width = (month_to + 12 - month_from) % 12 + 1 months = Array.new(width) do |i| (month_from + i) % 12 + 1 end months.include?(month) endその季節に含まれる月の配列(months)を生成する処理のところで、 Array.new にブロックを引数として
与えています。おわりに
今回は Ruby のメソッドの中で比較的、そんな引数をとることができることが知られていないようなものを
紹介しました。Rubyリファレンスマニュアルを読んでいると、いろいろと今まで分かっていたつもりで分かっていなかった
様々な発見があります。一度は通読してみること、オススメです。
- 投稿日:2019-12-05T00:54:45+09:00
あ!こんな引数とれたんだっていう Ruby のメソッド 8選いた
ENECHANGE で勤務している cuzic です。
これは Ruby Advent Calendar 2019 の 5日目の記事です。
今日は Ruby のメソッドの中で、「あ、こんな引数とることができたんだ」と
個人的に感じたメソッドをいくつか紹介します。
self.class::CONSTANT
実は
::
演算子の左辺には式を指定できます。下記のようなコードがあったとします。
class A NUMBER = 1 def initialize @number = A::NUMBER end endこれは下記のように書くことができます。
class A NUMBER = 1 def initialize @number = self.class::NUMBER end endこのように
::
演算子の左辺として self.class のように式を指定することができます。上記の例であれば
a = A.new p a.class::NUMBER #=> 1のように
self.class
以外も書くことができます。array.values_at(range1, range2)
引数で指定されたインデックスの要素を配列として返すメソッド Array#values_at ですが、
Range を複数個、引数としてとることができます。array = ('a'..'z').to_a array.values_at(0..2, 10..12) => ["a", "b", "c", "k", "l", "m"]ENECHANGE ではさまざまな電気料金プランの料金計算を行っているのですが、
中には季節によって電気代が違うものの、春と秋は同じ単価という電気料金プランがあります。
春と秋の電気使用量を取り出すとき、このように range を複数引数として与えて
values_at メソッドを使うと簡潔になります。string[regex, index]
文字列の [] メソッドは第1引数に正規表現、第2引数に index を指定することで、
正規表現の capture を簡単に取り出すことができます。str = "Hello, World!" str[/(.)\1/] #=> "ll" str[/(.)\1/, 0] #=> "ll" str[/(.)\1/, 1] #=> l region = "ap-northeast-1a" region[/^(.+).$/, 1] #=> "ap-northeast-1"time.to_s(:db)
Ruby on Rails に含まれる Active Supportの中では
Time#to_s が拡張されており、さまざまな引数を
とることができます。例を紹介します。
time = Time.current time.to_s(:db) => "2019-12-03 13:17:17" time.to_s => "2019-12-03 22:17:17 +0900" time.to_s(:time) => "22:17" time.to_s(:number) => "20191203221717" time.to_s(:short) => "03 Dec 22:17" time.to_s(:long) => "December 03, 2019 22:17" time.to_s(:long_ordinal) => "December 3rd, 2019 22:17" time.to_s(:rfc822) => "Tue, 03 Dec 2019 22:17:17 +0900" time.to_s(:iso8601) => "2019-12-03T22:17:17+09:00" time.to_s(:nsec) => "20191203221717054977424" time.to_s(:usec) => "20191203221717054977"特に
:db
が便利です。他には、
nsec
はわりと使いどころがあるわりに、あまり知られていない気がします。Array#uniq{|item| ... }
Array#uniq はブロックを引数としてとることができます。
そのブロックを評価した値が重複しないような配列を取得することができます。
[*2..5, *"1".."4"].uniq(&:to_i) => [2, 3, 4, 5, "1"]enumerable.first(n), enumerable.last(n)
Enumerable モジュールの first や last メソッドは引数として
取り出す個数をとることができます。range = "a".."z" range.first(3) #=> ["a", "b", "c"] range.last(3) #=> ["x", "y", "z"] postcode = "1300012" postcode_1 = postcode.slice(0, 3) #=> "130" postcode_1 = postcode[0, 3] #=> "130" postcode_1 = postcode[0..2] #=> "130" postcode_1 = postcode[0...3] #=> "130" postcode_1 = postcode.first(3) #=> "130"enumerable.to_h{|item| [item["key"], item["value"]}
Enumerable モジュールの to_h メソッドは引数としてブロックを
とることができます。(Ruby 2.6 以降)例えば、下記のようなコードを実行できます。
(1..9).to_h{|i| ["%02d" % i, i]} => {"01"=>1, "02"=>2, "03"=>3, "04"=>4, "05"=>5, "06"=>6, "07"=>7, "08"=>8, "09"=>9}これまで、
(1..9).map{|i| ["%02d" % i, i]}.to_hとすることで、同様の効果がありましたが、より簡潔かつ効率よくハッシュを生成することかできます。
Array.new(n){|index| ... }
Array.new は配列の数を引数にとれるだけでなく、ブロックを引数としてとることができます。
例えば、下記は ENECHANGE で実際に使われていた month_from から month_to までで指定される
季節に該当するかどうかを判定するメソッドです。def in_season?(month_from, month_to, month) month_from = ( month_from || 1 ) - 1 month_to = ( month_to || 12 ) - 1 width = (month_to + 12 - month_from) % 12 + 1 months = Array.new(width) do |i| (month_from + i) % 12 + 1 end months.include?(month) end12月から4月までというのが 12、1、2、3、4 を返すことができるように上記のコードは少し工夫しています。
おわりに
今回は Ruby のメソッドの中で比較的、そんな引数をとることができることが知られていないようなものを
紹介しました。Rubyリファレンスマニュアルを読んでいると、いろいろと今まで分かっていたつもりで分かっていなかった
様々な発見があります。一度は通読してみること、オススメです。