- 投稿日:2019-11-30T23:19:56+09:00
【Rails】belongs_toで関連付けしたけどデータが保存出来ない →単数形にしてる?
はじめに
Railsアプリ作成中、
belongs_to
で関連付けたモデルのデータ保存がうまくいかず、はじかれてしまったので解決した方法です。しょうもないミスでした
この記事が役に立つ方
belongs_to
で関連付けはしたが、うまく保存が出来ない方この記事のメリット
- 無事にデータが保存出来るようになる。
環境
- macOS Catalina 10.15.1
- zsh: 5.7.1
- Ruby: 2.6.5
- Rails: 5.2.3
エラー内容
1対多の関係にある、UserモデルとRecordモデルがあるとします。
Recordテーブルのデータを新規作成し、
save!
でデータ保存しようとしたところ、以下のようなエラーが発生。ActiveRecord::RecordInvalid: Validation failed: Users must exist保存するデータには外部キー
user_id
があり、そこにはちゃんと値が入っていて、「あれ?なんで?」と困る。このときのモデルは以下のように定義していました。
app/models/record.rbclass Record < ApplicationRecord belongs_to :users endここから応急処置(間違った解決法)とちゃんとした解決法を記載します。
1.応急処置(間違った解決法)
以下のように変更。
【Before】
app/models/record.rbclass Record < ApplicationRecord belongs_to :users end↓
【After】app/models/record.rbclass Record < ApplicationRecord belongs_to :users, optional: true end
optional: true
で関連モデルなしでOK!と放し飼い状態にし、一旦。バリデーションを回避。しかし、全く意味のない矛盾した設定になるのであくまでも応急処置。
割とググるとこの解決法が多かったですが、あまり良くないと思います。
2.解決(ただの設定ミス)
【Before】
app/models/record.rbclass Record < ApplicationRecord belongs_to :users, optional: true end↓
【After】app/models/record.rbclass Record < ApplicationRecord belongs_to :user end複数形
単数形belongs_to関連付けで指定するモデル名は必ず「単数形」にしなければなりません。
Rails ガイドにもちゃんと書いてました!
英語で考えたら当たり前、お恥ずかしいです
optional: true
も不要なので削除。これで問題なし!
おわりに
最後まで読んで頂きありがとうございました
しょうもない設定ミスで時間をロスしてしまったので、同じようなエラーで困っている方がいれば参考にして頂ければと思います
参考にさせて頂いたサイト(いつもありがとうございます)
- 投稿日:2019-11-30T21:18:09+09:00
rails-tutorial第4章
helperメソッドとは
便利そうなメソッドをhelperに定義しておいてそれを呼び出す。
app/helpers/application_helper.rbmodule ApplicationHelper # ページごとの完全なタイトルを返します。 def full_title(page_title = '') base_title = "Ruby on Rails Tutorial Sample App" if page_title.empty? base_title else page_title + " | " + base_title end end endこれにより、page_titleが空の場合は、縦棒をいれずにbase_titleだけをタイトルタグに入れることができる。
app/views/layouts/application.html.erb<!DOCTYPE html> <html> <head> <title><%= full_title(yield(:title)) %></title> <%= csrf_meta_tags %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> </head> <body> <%= yield %> </body> </html>titleタグを先ほどhelperに定義したdef full_title(page_title = '')を使って実装している。
yield(:title)によりprovide(:title, "About")のように定義したタイトルを参照できる。
provideとyieldはセットで考える。pry
irbを拡張したものとしてpryという機能がある。
これはgemをインストールすることで使うことができる。$ gem intall pry $ pryこのコマンドにより、pryを使える。
putsメソッドについて
[3] pry(main)> puts 'foo' foo => nil上記のnilは計算の結果を返している。そのため、putsはfooを出力するがメソッド自体は何も返さない。
?の付くメソッド
?の付くメソッドは基本的にtrue か falseを返すようになっている。
自分でメソッドを定義するときもそれを気をつけながら定義する。nilと''の違い
nilをto_sメソッドで文字列にしたものが''
つまり、''は0文字の文字列である。Ruby(オブジェクト指向スクリプト言語)について
Rubyはメソッドを呼び出すという言い回しをしているが、実は少し違う。
オブジェクト指向スクリプト言語は、オブジェクトとのメッセージのやりとりだと考える。[18] pry(main)> 'foobar'.length => 6上記は、foobarの文字列は何文字?
俺は6文字だよー
という会話で成り立っている。ブロックの考え方
>> (1..5).each { |i| puts 2 * i } 2 4 6 8 10 => 1..5ブロック付きメソッドはメソッドの引数にRubyの処理を渡していると考えるとわかりやすい。
メソッドチェーンについて
[20] pry(main)> %w[foo bar baz].map { |i| i.upcase }.join => "FOOBARBAZ"このようにメソッドで評価された値にさらに . でメッセージを与えることをメソッドチェーンという。
ハッシュでなんでシンボルを使うのか?
[30] pry(main)> 'name'.object_id => 23088500 [31] pry(main)> :name.object_id => 88028 [32] pry(main)> 'name'.object_id => 24142460 [33] pry(main)> :name.object_id => 88028文字列オブジェクトはオブジェクトidが毎回違う。整数はオブジェクトidが毎回同じ。ただ、シンボルにすると、object_idが毎回同じになっている。
labelとしてシンボルオブジェクトを使うと毎回同じなので毎回計算する必要がない。シンボルは人間にもわかりやすい表現ができ、整数のように計算するコスト削減ができパソコンにも優しいということ。ハッシュの省略記法
[34] pry(main)> h1 = {:first => 'yokota', :second => 'daiki' } => {:first=>"yokota", :second=>"daiki"} [35] pry(main)> h1 => {:first=>"yokota", :second=>"daiki"} [36] pry(main)> h2 = { first: 'yokota', second: 'daiki' } => {:first=>"yokota", :second=>"daiki"} [37] pry(main)> h2 => {:first=>"yokota", :second=>"daiki"}シンボルを右側に書き、=>を省略した形で書くことができる。
classもオブジェクトの1つ
Rubyではあらゆるものがオブジェクトである。それはclassも例外ではない。
Stringにclassをするとclassクラスということがわかる。
Classクラスにsuperclassをするとmoduleクラスということがわかる。また自分でクラスを定義するとき、親クラスに何も指定しないと、デフォルトでObjectクラスが継承される。
- 投稿日:2019-11-30T20:36:36+09:00
Rails10年選手がGoの勉強会に参戦してきた
はじめに
静岡でRailsのWebエンジニアをしているkazuomatzです。2019年も残り1ヶ月を切りましたが、どうやら最近は業界的にRailsはオワコンらしいです。
僕は、2007年くらいからRailsを使ってきたので、かれこれ10年以上Railsを触ってきたことになるわけですが、オワコンなんて言われると、このままRailsを使い続けるか、それとも新しい言語に乗り換えるかの岐路に立たされているような気にもなってきます。
Railsが世の中に登場したのが2004年、2019年現在、Railsのバージョンは6.0です。2007年の12月にRailsのバージョン2が公開されていますから、かれこれ、5世代のRailsを使ってきたことになります。
Web2.0とRailsとの出会い
ちょっと昔話になりますが、2000年半ばに「Web」はバージョン1からバージョン2にバージョンバップされました。「Web 2.0」です。
これまでのWeb(=Web 1.0)は、ユーザーが情報を取得するだけのものでした。それがWeb 2.0になると、ユーザーがWebを通じて情報を発信することができるようになりました。そして、Webをプラットフォームとした様々なサービスやビジネスが生み出される時代に向かっていきました。
今となっては、Web 2.0とは結局何だったのかという議論もありますが、ともかく2007年11月に「Web 2.0 Expo」が盛大に開催されました。Web 2.0を提唱したティム・オライリーの講演をはじめ、当時の先進的なWeb関連のセッションが盛りだくさんでした。
当時、僕はJavaプログラマでマッシュアップ関連のWebサービスをいろいろ作っていましたが、そのWeb 2.0 Expoの会場で、Sun Microsystems(当時)のティム・ブレイの衝撃的な一言を聞くことになります。
「Ruby on Railsに触れたJavaプログラマーは、もうJavaには戻ってこないだろう」
Javaの開発元であるSun Microsystemsの中の人がこれ言っちゃったわけですから、大変なことです。少なくともWebが大きく変わるぞって時に、この発言によってRailsは脚光を浴びることになります。恐らく、これきっかけでRails始めた人はかなりいたはずです。
僕もその一人です。あの時からこの10年あまり、Railsに魅了され、RailsでいろいろなWebサービスを開発してきました。
Go勉強会へ
前置きが長くなりました。
ともあれ、Railsオワコン説を聞く中で、10年Railsを触ってきた身として、確かにいろいろ辛いことが多くなってきたなぁとも思います。そんな中見つけたのが、Shizuoka.goの勉強会でした。
勉強会の運営者も以前の仕事仲間(@secondarykey)だったので、Rails10年選手がGoを模索しているというネタで何かしゃべらせてとお願いして、参加することにしました。
ここからは、発表した内容の抜粋です。
Railsのここがよかった
- 設定より規約 / DRY ( Don’t Repeat Yourself )の哲学
- フルスタックなWebフレームワーク MVC / ORM / Asset
- 便利なライブラリ(Ruby Gems)が豊富!!
- お手軽な動的型付言語 Ruby
- 開発効率が半端ない ような気がしてた・・・
とにかくRailsに魅了されていた過去の自分。
Railsがオワコンの原因について
- フロントエンドのフレームワークがいろいろ出てきた
- Webpackなどのモジュールバンドラーが便利
- Railsでイケてたところのほころびが出てきた
僕もVue.jsは結構使うんですが、フロントエンドのフレームワークとRailsとがうまく調和しなくなってきてますね。Webpackについては、Rails向けのWebpackerがありますが、これもいまいち、むしろWebpackをそのまま使う人も多しね。Sprocketsもnode対応したり、ES6対応したりでこれでいけないこともないし・・・。
むしろ、僕が困ったと思っているのは、イケてたところのほころびが出てきたと書いたところ。
RailsってRubyGemsにものすごく多くの便利なライブラリーたちがあって、これをサクッと導入することで、いろいろなことが簡単にできます。もちろん、Ruby/Rails以外の言語やフレームワークでもそのような仕組みはあるわけだけど、Railsはちょっと探すと大抵のやりたいことは、Gem入れれば実現できちゃった。それが、当時のRailsって開発効率いいねという神話を作っていたのかもしれません。
こんなの見ちゃうと、Railsのレールに乗っているイケてる感に拍車かかりますしね・・・。
ところが、10年もいろいろWebサービスを作ってくると、サービス中止になるものもあれば(まぁこちらの方が多いわけですが)、セキュリティ対応やRailsのバージョンアップをしながら保守し続けるサービスもあります。この後者のサービスの延命が辛いんです。
Railsのバージョンアップは痛みを伴います。むしろ作り替えてしまった方が早い場合もあります。いや、作り替えられればまだいいかもしれません。データベースに貯まった様々なデータが特定のライブラリー依存の形で保存されているとかなり辛いです。
例えば、PaperClipという便利なGemがあります。画像ファイルをアップロードすると、Amazon S3などのクラウドストレージにアップロードしてくれる便利なライブラリです。これ本当によく使いました。でも、このPaperClipは、Rails 5.2以降では使えません。開発者の方が開発中止を宣言して、Rails5.2以降では、Railsに標準装備されたActiveStorageを使ってねということになっています。
これから新規に開発するサービスであれば、Rails 6でActiveStorageを使えばよいですが、すでにPaperClipを使ってデータも保存されているサービスをRails 6に移行することは簡単にはできません。
これは、Railsが悪いわけでも、開発を止めてしまったデベロッパーが悪いわけではありません。むしろ便利なGemを使うことでイケてる感を出してきたツケが回ってきたのかなと思います。
今のRuby / Railsの状況
- 2〜3年前に開発したWebサービスがサポート外のRailsで動いている
- とはいえ、Railsのバージョンアップする時間・費用がない
- 慣れ親しんだライブラリーが更新されず、最新のRails環境で使えない
こんな状況をGoは救ってくれるのか
ここからやっとGoの話。こんな状況をGoは救ってくれるのかを検証してみた。
これからもWebで食べていく僕がやっていくことは、こんなこと。
- Webサービスの開発(バックエンドもフロントエンドもやる)
- アプリ開発におけるバックエンドの開発(iOS / Androidアプリも作る)
- AWSを上手に使ってマイクロサービス指向で行く(Azure、GCPにも手を出す)
願うこと
- Web開発に必要なよいフレームワーク
- シンプルに開発ができること
- リファクタリングしやすく保守性が高いこと
発表しながら気づいた。第一にフレームワークを求めちゃうのは、フルスタックな全部入りのRailsにずいぶん甘やかされてきたんだなぁと。Rubyの世界ではWebフレームワークと言ったらほぼRails一択だけど、Goの場合は、軽量級なものからフルスタックなものまで、いろいろなフレームワークが存在する。Routerはこれで、ORMはこれをといった選択することも可能。Railsやってるとこの感覚は確かにない。
デモアプリをGoで作ったよ
僕は、この発表をするにあたり、簡単なWebサイトのモックをGoで作ってみた。
静岡市が運営している市民協働のポータルサイト「ここからネット」。このサイトには市民活動団体の活動情報や開催する講座やイベント情報、ボランティア情報などが掲載されている。そしてこれらデータはオープンデータとして公開されており、Web APIでデータを取得できる。APIのリファレンスもGithubで公開されている。
ここからネットに登録されているデータの中から静岡市の保育施設の一覧を取得して表示するだけのシンプルなものを作ってみた。
できあがりはこんな感じ。
APIからの戻りはこんな感じのJSON。
search.json{ "status": 200, "page": 1, "per_page": 2, "all_page_num": 133, "count": 2, "all_count": 266, "pois": [ { "id": 3450, "name": "静岡大学教育学部附属幼稚園", "prefecture_name": "静岡県", "city_name": "静岡市葵区", "address1": "大岩町1-10", "address2": "", "tel": "054-245-1191", "url": "http://fzk.ed.shizuoka.ac.jp/youchien/", "option_items": [ { "display_name": "分類", "attribute_name": "分類", "value": "従来どおりの幼稚園" }, { "display_name": "設立", "attribute_name": "設立", "value": "国立" }, { "display_name": "受入", "attribute_name": "受入", "value": "3歳~就学前" }, { "display_name": "情報登録日", "attribute_name": "情報登録日", "value": "H29.9.1" } ], "updated_at": "2018/05/09 22:01:39 +0900", "lat": 34.9913406, "lng": 138.3794399 } ] }GoでAPIをリクエストしてJSON Parseしてクライアントに返すプログラムはこんな感じ。
動的言語のRubyと違って静的型付言語のGoにおいては、APIで受け取るJSONの構造体をキチンと書く。/controllers/kindergarten.go// PoiData 受信データ type PoiData struct { Status int `json:"status"` Page int `json:"page"` PerPage int `json:"per_page"` AllPageNum int `json:"all_page_num"` Count int `json:"count"` AllCount int `json:"all_count"` Pois []struct { ID int `json:"id"` Name string `json:"name"` Kana string `json:"kana"` Description string `json:"description"` ZipCode string `json:"zip_code"` PrefectureName string `json:"prefecture_name"` CityName string `json:"city_name"` Address1 string `json:"address1"` Address2 string `json:"address2"` Tel string `json:"tel"` URL string `json:"url"` ImageURL string `json:"image_url"` OptionItems []struct { DisplayName string `json:"display_name"` AttributeName string `json:"attribute_name"` Value string `json:"value"` } `json:"option_items"` StartAt string `json:"start_at"` EndAt string `json:"end_at"` LocationID string `json:"location_id"` ActivityID string `json:"activity_id"` PostPhotoID string `json:"post_photo_id"` InformationID string `json:"information_id"` UpdatedAt string `json:"updated_at"` OrganizationID string `json:"organization_id"` Lat float64 `json:"lat"` Lng float64 `json:"lng"` Marker2X string `json:"marker2x"` Marker string `json:"marker"` Type string `json:"type"` } `json:"pois"` } // GetKinderGarten 保育園情報を取得する func (controller *KinderGartenController) GetKinderGarten() { url := beego.AppConfig.String("endPointURL") + "/map/search.json" page := controller.GetString("page") url += "?page=" + page dataSet := controller.GetString("type") if len(dataSet) == 0 { dataSet = "1,2,3,4,5" } url += "&data_set=" + dataSet response, err := http.Get(url) if err != nil { controller.CustomAbort(500, "Internal server error") return } defer response.Body.Close() body, err := ioutil.ReadAll(response.Body) if err != nil { controller.CustomAbort(500, "Internal server error") return } jsonBytes := ([]byte)(body) data := new(PoiData) if err := json.Unmarshal(jsonBytes, data); err != nil { controller.CustomAbort(500, "Internal server error") return } controller.Data["json"] = data controller.ServeJSON() }フロントエンドはVue.jsを使っている。モジュールバンドルはWebpackだ。ここはGoとは直接関係ない世界。
選択したWebフレームワーク
そして、肝心のWebフレームワークはいろいろ探した結果、最終的にBeego Frameworkを選んだ。
BeegoはRubyのWebフレームワークSinatraを意識して開発されたとのこと。SinatraはRubyのWebフレームワークではRailsの次に使われているもので、フルスタックなRailsよりも軽量なもの。
Beegoは、ORM、Routerも使えて、MVCモデルもいける。CLIもあってコマンド叩いていろいろできる。
実は、自分の発表の前に、元仕事仲間(@secondarykey)がGoのWebフレームワークをいろいろ紹介してくれていた(資料)。彼の所感では、Beegoは「老舗っぽくフルスッタックで色々機能がありそう。重そう。」とのこと。やっぱり、Railsのフルスタック脳がBeegoを選んだ結果は納得できる。
まとめ
Go言語とRubyを比較すると、静的と動的の議論も確かに出てきます。やっぱり静的の方が安全だよねという風潮もありますし、動的の柔軟さが開発効率をあげているのも事実です。
このあたりはトレードオフなので、要件に合わせてどちらが最適かを見極めていくことも必要かなと思います(今まで動的万歳だった自戒も込めて)。
今、自分がサクッとWebサービスを作るとしたら、やはりRailsを選ぶと思います。Rails 6のActiveStorageはよくできていると思いますし、まだまだ使えるGemも多い。自前のライブラリーやTipsも豊富にあったりするので、ユーザビリティや品質の高いWebサービスはまだまだRailsで作れます。
ただ、今回、Shizuoka.goに参加して、Goの魅力にも気づけたことも事実です。
自分が今後やっていきたいことの中で、「AWSを上手に使ってマイクロサービス指向で行く」をあげていますが、こちらはRubyよりもGoが向いているんだろうと思います。AWSでなくて、GCPなのかもしれませんが・・・。ということで、今までやってきたアーキテクチャーとは違うものにチャレンジする際に、Goを選択することをここに誓います。
今回のShizuoka.goではWebフレームワークの話のほかに、@hogedigoさんのGoでのテスト手法についての話(資料)、@cupperさんのAWSとGoの話(資料)、@hrs_sano645
さんのGo製便利ツールの紹介(資料)など、興味深い話がたくさん聞けました。最後に、Shizuoka.goのみなさんありがとうございました。また参加します。Shizuoka.go最高!
- 投稿日:2019-11-30T19:53:59+09:00
Rubyで生年月日から年齢を求める(ydayメソッド使用)
Dateインスタンスで作成した生年月日から現在の年齢を算出したい時。
誕生日の前か後かも考慮したい場合は
ydayメソッド
で比較的わかりやすく記述できるかと思います。ydayメソッドは、1月1日を1日目として、オブジェクトの日付が何日目かを整数で返してくれるメソッドです。閏年もカバーしています。
たとえば、2019年2月10日(2019-2-10)なら、返ってくる数値は41となります。
y.dayメソッドを使った年齢算出コード
require "date" birthday = Date.new(1999, 12, 10) today = Date.today #=> 2019-11-30と仮定 age = today.year - birthday.year if today.yday < birthday.yday age -= 1 end puts age #=> 19上記のコードでは、今日までの日数(today.yday)と誕生日までの日数(birthday.yday)を比較し、前者の方が小さければ誕生日はまだ来ていないのでageからマイナス1しています。
他にも色々な算出方法があるかと思いますが、方法の一つとして参考にしていただければ幸いです。
「このコードだとこんな場合は間違った結果になるよ!」ということがあれば、ご教示いただけるとありがたいです。
参考1:Ruby リファレンス
https://docs.ruby-lang.org/ja/latest/method/Time/i/yday.html
参考2:生年月日から年齢を求める
http://rubytips86.hatenablog.com/entry/2014/03/27/101209
- 投稿日:2019-11-30T19:14:09+09:00
【初心者向】配列とハッシュ取り出し方、活用の仕方
Railsを何人かに教えていて、配列の取り出し方が怪しいのではないかと思えたので
勉強会資料として書きます。配列というのはデータを複数入れることができるものです。
例えば@basic1 = [1, 2, 4, 8, 16, 32]
この時、複数入った中身を取り出すには
number = @basic1[0]
のように書く。
[0]
というのがインデックス番号になって、0番目から始まります
今回の場合なら
0番目が1
、1番目が2
、2番目が4
、3番目が8
、4番目が16
、5番目が32
となります。ビューに表示するときは例えば
<h1>基本1</h1> <div> <h2>basic1</h2> <% @basic1.each do |b| %> <%= b %><br> <% end %> </div>/sample-blog/app/controllers/users_controller.rbdef basic1 @basic1 = [1, 2, 4, 8, 16, 32] end同じパターンで
/sample-blog/app/controllers/users_controller.rbdef basic1 @basic2 = ["aaa", "bbb", "ccc", "ddd"] end<h2>basic2</h2> <div> <% @basic2.each do |b| %> <%= b %><br> <% end %> </div>配列の中に配列
少し複雑な場合
def basic1 @basic3 = [[1, 2, 3],[3, 4, 5]] end<h2>basic3</h2> <div> <% @basic3.each do |basic| %> <p><%= basic %></p> <% end %> </div>繰り返し1回目はindexが1繰り返し2回目はインデックスが1
indexが0
のところは[1, 2, 3]
1のところは[3, 4, 5]
今度は全部表示させたい(取り出したい)場合
<h2>basic3-2</h2> <div> <% @basic3.each do |basic| %> <% basic.each do |b| %> <%= b %><br> <% end %> <% end %> </div>eachの繰り返しの中にeachがあります。
1回目のループでbasic =[1, 2, 3]
が取り出されるのでこれをさらにループにかけると1
2
3
2回目のループでbasic =[3, 4, 5]
が取り出されるのでこれをさらにループにかけると3
4
5
と中身を取り出すことができます。配列の中にハッシュ
@basic5 = [ {name: "佐藤", mail: "satho@gmail.com"}, {name: "田中", mail: "tanaka@yahoo.co.jp"} ]一つ目の要素が
{name: "佐藤", mail: "satho@gmail.com"}
二つ目の要素が{name: "田中", mail: "tanaka@yahoo.co.jp"}
以下のように取り出すことができます。
<h2>basic5-1</h2> <div> <% @basic5.each do |b| %> <p><%= b[:name] %></p> <p><%= b[:mail] %></p> <% end %> </div> <h2>basic5-2</h2> <div> <p><%= @basic5[0][:name] %></p> <p><%= @basic5[1][:mail] %></p> </div>こんな感じで取り出すことができます。
- 投稿日:2019-11-30T17:31:03+09:00
モデル データ取得
テーブルからデータを取得します
モデルから取得したデータはコントローラーに送られます。
その為にはActiveRecordクラスが必要です
メソッド 用途 all テーブルの全てのデータを取得する find テーブルのレコードの内、ある1つのデータを取得する new クラスのレコードを生成する save クラスのレコードを保存する post_controller.rbdef index @post = Post.all #大文字Pからスタート @post_2 = Post.find(1) #1番のレコードを取得以上でモデル内からデータを取得し、コントロール内で定義しているインスタンス変数に代入した
- 投稿日:2019-11-30T17:16:41+09:00
Rails Tutorial Memo #2
- 投稿日:2019-11-30T16:56:35+09:00
モデル作成
モデルの作成手順
ターミナル
rails g model モデル名マイグレーションファイルが作成される
マイグレーションファイルとは.....
モデル内のテーブルを設計することができるファイル
例)モデル内のカラム名の変更def made create_table :posts do |t| #Postテーブルを作成する t.text :content #t.型の名前 :カラム名 t.timestamps #Railsが独自に用意している特別なもの end endマイグレーションファイル記載後、データベースに記載内容を反映させる
rails db:migrateテーブルに記載事項が追加さてます。
- 投稿日:2019-11-30T16:24:00+09:00
インスタンス変数
- 投稿日:2019-11-30T16:09:26+09:00
【Rails】websocket_railsで開発していたチャットをActionCableにリプレース
はじめに
こんばんは。弊社の直近の仕事で行なっていたwebsocket_railsで開発していたユーザーチャットの機能をActionCableにリプレースする話でも執筆しようと思います。
なぜActionCableにリプレースするのか
単純に、もうそろそろ限界かなと思った次第です。
WebSocketとは
通常、クライアントがWebサーバと通信を行う際にはHTTPの手順に基づいて通信を行いますが、HTTPはステートレス(ステート: 状態という事をさす事が多い?)なプロトコルなため、どれだけリクエストを送っても過去のステートを保持しないため、サーバから同じレスポンスが返ってくる仕様です。勿論、現在のWebアプリケーションでは過去のステートを保持しなければならない事情の事の方が多いため、Cookie情報を付加したりして、過去のステートをサーバに渡す事もしています。
また、一度HTTP通信が完了すると、その通信は閉じられるため、基本的には片方向の通信しかできません(TCPのコネクションとは別の模様、HTTP1.1からそれまでTCPのコネクション閉じていたものが、閉じないようになった)。
そのため、XMLHTTPRequestやHTTPロングポーリングなどの複数のHTTP通信を介して双方向通信をしているように見せかける方法はあるのですが、如何せん実装は複雑になる傾向があります。そこで、HTTPの通信を拡張させてTCPのようにハンドシェイクを行い、HTTP上で仮想的な通信経路(コネクション)を実現し、その中で双方向通信ができるようにしようと提案されたのが
WebSocket
です。Railsは5.0以降において
ActionCable
と呼ばれるJSによるクライアントサイドとRailsによるサーバサイド両方のWebSocketフレームワークを提供できる機能が標準化されており、Rails5.0にアップグレードして以降利用したいツールでした(なお、Rails5.0以前はwebsocket-railsが主流だった?)。今回は、websocket-railsで開発していたチャット機能をActionCableにリプレースします。
続いて、ActionCableの概要を書きます。ActionCableの概要
参考文献があるのですが、ActionCableはどうも用語が多すぎて複雑です。パッと読んだ理解と下手なポンチ絵なので、間違いがあれば指摘をお願いします。
このポンチ絵は、
https://railsguides.jp/action_cable_overview.html#%E7%94%A8%E8%AA%9E%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6
とhttps://railsguides.jp/action_cable_overview.html#pub-sub%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6を参考に書きました。要は、コネクションを確立させるためにチャネル(データを伝送する経路に)クライアントが加入すると、クライアントは
サブスクライバ
という名前になり、パブリッシャ(情報を伝送する者)から受け取ったデータをブロードキャストでサブスクライバに伝達させます。
この時、特定の個人宛にデータを送信するわけではないので、ブロードキャストを行うことにより、パブリッシャも後にサブスクライバとなり、データを受信することができます。中々、用語の整理が複雑ですが、基本的な所を抑えてしまえば、後はJSとRailsのコードでWebSocketの通信を行える事ができるので、次に具体的なコードにいってみましょう。
ActionCableをRailsサーバと一緒に実行する
routes.rbに以下のように
/websocket
でWebSocketリクエストがListenできるようにします。config/routes.rbmount ActionCable.server => "/websocket"クライアントがWebSocketリクエストを送る
まずはActionCableサーバにWebSocketリクエストを送ります。
createConsumer
メソッドにより、WebSocketのリクエストを送ります。createConsumerの引数には直接サーバホスト名を書く事もできますが、今回はパス名のみを指定します。consumer.js//= require action_cable //= require_self (function() { this.App || (this.App = {}); App.cable = ActionCable.createConsumer("/websocket"); }).call(this);ActionCableのサーバサイドの概要
ユーザーチャット用に利用するチャネルを作成します。チャネルの初回作成時には、親チャネルとなる
app/channels/application_cable/channel.rb
やapplication_cable/channel.rb
などなどが作成されます。% bundle exec rails g channel chat
初めに、サーバサイド側のコードを見てみましょう。
app/channels/chat_channel.rbclass ChatChannel < ApplicationCable::Channel def subscribed stream_for room end def speak(data) message_datas = data["message"] message = message_datas["message"] account_class_name = message_datas["account_class_name"] account_id = message_datas["account_id"] chat = Chat.create!(chat_room_id: room.id, sender_type: account_class_name, sender_id: account_id, message: message) ChatChannel.broadcast_to(room, chat: chat) end def remove(data) chat = Chat.find_by(id: data["message_id"]) chat.destroy! ChatChannel.broadcast_to(room, delete_id: data["message_id"]) end def update(data) message_datas = data["message"] chat = Chat.find_by(id: message_datas["message_id"]) chat.update(message: message_datas["message"]) ChatChannel.broadcast_to(room, edit_id: chat.id, edit_message: chat.message) end private def room (中略) end def my_account (中略) end endこの内の
def subscribed
のコードを見てみましょう。
subscribedはコンシューマがChatChannelにサブスクライブされたときに呼び出されるコードになります。
stream_for
により、今後ユーザーチャットでブロードキャストをする際に何をトリガーとしてブロードキャストをするのが指定しています。
今回は、中身が1対1のチャットということもあり、ChatRoomのIDを入れて検索をかけたオブジェクトをルーティングのトリガーとしています。次に、speakメソッドはクライアントがメッセージを送信した時に呼び出されるメソッドとしてここでは定義しています。受け取ったパラメーターからDBにChatデータを残しつつ、他のクライアントへブロードキャストします。この時、特定の個人を指定していないのがポイントです。
同じように、update, removeメソッドはクライアントがメッセージを更新・削除したときに呼び出されるメソッドとして定義しています。
続いて、クライアントコードを見てみましょう。
ActionCableのクライアントコードの概要
app/assets/channels/chat_room.js//= require ../channels/chat_channel.js document.addEventListener("DOMContentLoaded", () => { if ($(".chat_rooms").length) { let chat_json = JSON.parse(*****); let account_class_name = ***** let account_id = ***** const reply_button = *****; reply_button.addEventListener("click", () => { let message = {}; message["message"] = document.getElementById("chat_message").value; message["account_class_name"] = account_class_name; message["account_id"]= account_id; App.room.speak(message); }); App.room = App.cable.subscriptions.create({ channel: "ChatChannel", room_id: *****, room_type: *****, account_class_name: account_class_name, account_id: account_id}, { connected: function() { this.appendChats(chat_json); }, appendChats(data) { const html = this.createChats(data); const element = document.getElementById("chat-area"); element.insertAdjacentHTML("beforeend", html); }, updateChat(data) { let message = *****; message.innerText = data["edit_message"]; }, removeChat(message_id) { let remove_chat = *****; remove_chat.remove(); }, createChat(data) { (中略) return chat_line; }, createChats(data) { let chat_area = []; let chats = [] chats.push(`<div class="chat-message-container" style="max-height: 651px;">`); chats.push(`<div id="chat-list">`) (中略) return chats.join(""); }, speak: function(message) { return this.perform("speak", { message: message }); }, update: function(message) { return this.perform("update", { message: message }); }, remove: function(message_id) { return this.perform("remove", { message_id: message_id }); }, received: function(chat) { if(chat["chat"] != undefined) { this.appendChat(chat["chat"]); } else if(chat["edit_id"] != undefined) { this.updateChat(chat); } else if(chat["delete_id"] != undefined) { this.removeChat(chat["delete_id"]); } } }); } });大分、マスキングしましたが、基本的には各々が書いたHTMLの操作に合わせてJSのメソッドを用意するような書き方で良いと思います。
App.room = App.cable.subscriptions.create({ channel: "ChatChannel", room_id: result["room_id"], room_type: result["room_type"], account_class_name: account_class_name, account_id: account_id}, {指定のチャネルにサブスクリプションを作成することで、コンシューマがサブスクライバとして振る舞うことができます。
これ以降にも違う形でActionCableを使うのであれば、子チャネル用のサブスクライブメソッドを作ればいいわけです。
また、サブスクライバとなったクライアントからはサーバのパブリックメソッドが見えるため、公開されたパブリックメソッドをperform
メソッドで呼びだすことができます。speak: function(message) { return this.perform("speak", { message: message }); }, update: function(message) { return this.perform("update", { message: message }); }, remove: function(message_id) { return this.perform("remove", { message_id: message_id });各々のメソッドに合わせた、パラメータを付加してサーバのパブリックメソッドを呼び出します。
唯一、receivedはブロードキャストの通信を受け取った後に実行されるコールバックです。received: function(chat) { if(chat["chat"] != undefined) { this.appendChat(chat["chat"]); } else if(chat["edit_id"] != undefined) { this.updateChat(chat); } else if(chat["delete_id"] != undefined) { this.removeChat(chat["delete_id"]); } }情報の伝達方法がブロードキャストのみとなっているので、サーバから受け取るブロードキャストの中身を各メソッド用にカスタマイズすることとしました。
これによって、ブロードキャストによって送られてきたデータの中身を確認することによって、クライアントの振る舞いを変えることができます。後は各Webアプリケーションの実装に合わせて、HTMLを動的に追加・更新・削除させて見栄えを変えてみてください。
NginxとProductionの設定
後は、Webサーバとproduction.rbの設定を行います。
production.rbconfig.action_cable.allowed_request_origins = ['https://bitstar.tokyo']ActionCableは、指定されていない送信元からのリクエストを受け付けていないため、配列の形で許可する送信元ホスト名を渡します。
/etc/nginx/conf.d/virtual.confserver { listen 80; server_name bitstar.tokyo; root /var/www/bitstar/current/public; (中略) location /websocket { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_pass http://puma/websocket; }最後にNginxの設定です。NginxはロードバランサーからのアクセスをRailsサーバにわたすリバースプロキシの設定を行っているため、
/websocket
のパスに対して、HTTPのプロトコルをWebSocketにスイッチングできるように設定をします。これにより、RailsとActionCableの機能を一緒のPumaプロセスで動かすことができました。
まとめ
以上で、チャット機能をwebsocket_railsからActionCableにリプレースすることができました。
ActionCableに登場する人物の用語に関しては本当になんとかならなかったのかとちょっと疑問に思っています。
また、本当はwebsocket_railsからどうコードを移植したのか見せたかったのですが、実は修正前はHTMLの中で直接、<script>
タグを埋め込んでその中でフォームを構成しており、非常に直しにくい印象を受けてしまい、「これは全捨ての方が良くないか?」という事でコード全捨てしてしまいました。
今だと、websocket_railsでWebSocketプロトコルによる機能を作るメリットもないと考えているので、リプレースを考えていらっしゃる人は一旦コード全捨てのアプローチを取った方がいいかもしれません。それでは、また。
参考文献
- 投稿日:2019-11-30T16:07:41+09:00
Rails 流れ 其の壱
Rails 流れ
①
ターミナル rails new アプリケーション名 -オプション名
アプリケーションの作成ターミナル rails db:create
データベースの作成ターミナル rails g controller コントローラー名アクション名
コントローラーの作成②ブラウザからURLが送信される
③ルーティングにて受け取ったURLに対してどのコントローラーのどのアクション(コントローラー内のメソッド)かを識別。
[アクション]
.index :詳細表示
.new :新規保存
.create :保存
.edit :編集表示
.show :一覧表示
.update :更新
.delete :削除ルーティングファイル内 HTTPメソッド "URL" => "コントローラー名#アクション名
. GET :ページを表示する操作のみを行う時
. POST :データを登録する操作をする時
. PUT :データを変更する操作をする時
. DELETE:データを削除する操作を行う時
- 投稿日:2019-11-30T15:38:59+09:00
Rails SQliteからmysqlへ変更(途中から)
アプリ作成後にmysqlに変更する方法
MySQLを起動して接続の確認
$ mysql.server start起動後、接続できるか確認
mysql -u root -pRailsで利用するユーザーを作成します。
create user ユーザー名@'localhost' identified by 'パスワード'; 例 (create user railsuser@'localhost' identified by 'railspass';)ユーザーが作成されたことが確認
select User,Host from mysql.user;権限の付与を行います
grant all on *.* to ユーザー名@'localhost';設定ファイルの変更
config/database.ymldefault: &default adapter: mysql2 encoding: utf8 pool: 5 username: ユーザー名 password: パスワード host: localhost development: <<: *default database: sample_ec #データベース名Gemfileに記述
gem 'mysql2'$ bundlemysqlにDBの追加とテーブル作成
$ bundle exec rake db:create $ bundle exec rake db:migrateこれで完了
mysqlユーザーの削除方法
mysql> drop user ユーザー名@'localhost';最初からmysqlを使用する場合
rails new アプリケーション名 -d mysql
- 投稿日:2019-11-30T15:21:51+09:00
サーバエンジニアから始める、Swiftロードマップ
こんにちは
taskey サーバエンジニアの田代です。ふとSwiftを学ぶことになり、その時の軌跡をまとめておこうと思いnoteに残しました。
対象者: サーバエンジニアだけど、Clientのことをもっとわかってあげたい方、こまい修正位やっちゃいたい方
社内マガジンに載せる都合上noteにまとめました。
内容はこちらをご閲覧下さい。
- 投稿日:2019-11-30T14:04:55+09:00
Ruby on Rails チュートリアル1周目
Railsチュートリアルを一周してみましたので簡単な感想とこれから学習する人向けにポイントなどをまとめようと思います。
まず、Railsチュートリアルをいきなり始めるのではなくもっと基礎的な学習を前にやっておいたほうがスムーズに理解できます。
学習カリキュラムについてはこちらの方が細かく書いてくれてます。
https://qiita.com/saboyutaka/items/1a8c40e105e93ac6856aフロントエンド
私自身はWebデザイナーということもあり飛ばしていたのですが、
HTML/CSS(Sass)/Javascript
Jquery
Bootstrap
についてはすでに理解していました。IT基礎知識
どうということはないのですがサーティファイの情報処理技術者能力検定試験もとっているので基礎知識も一応あるつもりです。
https://www.sikaku.gr.jp/js/jss/Git, Command Line, SQL
Git, Command Line, SQLについては
Progate
ドットインストールで学習Ruby, Ruby on Railsの基礎
さらに
Ruby
Ruby on Railsもこちらで一周しておきました。書籍ではこちらをやってみました。
Railsの教科書
たった1日で基本が身に付く! Ruby on Rails 超入門
ゼロからわかる Ruby 超入門Rails チュートリアル前に
下記サービスの登録をしておくとスムーズにすすめることができます。
Github、もしくはBitbucketの登録
Gitの操作を行うにあたってリポジトリーを登録する必要性があるのでやっとくといいでしょう。
Herokuの登録
フリープランでいいのでクレジット登録しておくと後のsendgridからのメールで認証するときに必要になります。
AWSの登録
Cloud9ではじめているならアカウントできていると思いますが、
画像を保存するときはS3を使うので先に登録しとくとよいでしょう。Rails チュートリアル後
1周終えるだけボリュームがすごいのでかなりの学習時間がかかります。
その間に挫折する人も多いと思うのでおすすめは動画か質面ができるサービスと一緒にやるといいと思います。1周を終えたあとは2周めやるもよし、サービスを作るとよしなのですが
何を作ったらいいのかわからない、もうちょっと勉強したいという方にはTechpit
クローンサイトを作れるカリキュラムがあります。
Rails チュートリアルに比べるとかなり簡単につくれますが機能が簡単すぎて物足らないとおもいますので
Rails チュートリアルと組み合わせながらやるのがいいかもしれません。e-Navigator
作ったサービスのコードレビューをしてくれるサービスです。
現役エンジニアがレビューを行ってくれるので実践経験がない方にはちょうどいいかもしれません。私はフロントエンド(Vue.js)のほうも勉強しているのでBackendの部分はもう一度Railsチュートリアルを進め、フロントの部分をうまいこと組み合わせて見ようと思います。
以上ですが、Railsチュートリアル1周目のまとめと感想でした。
- 投稿日:2019-11-30T13:00:25+09:00
人工知能系・機械学習系・AI系のオススメAPIまとめ
Watson API
提供:IBM
リンク: https://www.ibm.com/watson/jp-ja/developercloud/services-catalog.html
機能: 検索、テキスト分析、画像認識、音声認識、音声読み上げ、翻訳、心理分析、感情分析、データ分析プラットホームVision AI
提供:Google
リンク: https://cloud.google.com/vision/
機能: クラウドやエッジにある画像を分析Cloud Speech-to-Text API
提供:Google
リンク: https://cloud.google.com/speech-to-text/
機能: 機械学習によって音声をテキストに変換、短時間音声にも長時間音声にも対応Cloud AutoML
提供: Google
リンク: https://cloud.google.com/automl/
機能: カスタム ML モデルを迅速かつ簡単にトレーニングdialogflow
提供: Google
リンク:https://dialogflow.com/
機能: 顧客からの予約・オーダーの受付や問い合わせに自然言語プロセッサ(チャットボット)で対応をするAPIWit
提供: Wit.ai
リンク: https://wit.ai/
機能: 自然言語処理AWS 機械学習
提供: AWS
リンク: https://aws.amazon.com/jp/machine-learning/
機能: 音声,高度なテキスト分析,ドキュメント分析,翻訳,文字起こし,対話型エージェント参考
- 投稿日:2019-11-30T12:36:06+09:00
rails-tutorial第3章
branchについて
$ git branch
これにより今いるブランチを確認することができる。基本的にmasterブランチは常に安定しなければいけないので、新規機能を作るときは必ずブランチを切る。$ git checkout -b static-pages
-bはブランチを切るということ。このコマンドにより、masterブランチから離れる。コントローラー
$ rails generate controller StaticPages home help
上記はStaticPagesコントローラーを作りhomeアクションとhelpアクションを追加する。さらにそれに対応するviewやテストを用意するという意味。また、rails g controllerコマンドは必ずコントローラー名を複数形にして書くこと。テストについて
require 'test_helper' class StaticPagesControllerTest < ActionDispatch::IntegrationTest test "should get home" do get static_pages_home_url assert_response :success end test "should get help" do get static_pages_help_url assert_response :success end end上記はrails g controllerコマンドを使うと自動生成されるテストコード。
スクリプト言語とテストは切っても切り離せない。test "should get home" do get static_pages_home_url assert_response :success end例えば上記は
get static_pages_home_url:static_pagesコントローラーのhomeアクションにリクエストを送った時に、
assert_response :success :必ずなんらかのレスポンスが返ってくるはずだという意味。ちなみにテスト駆動開発は、まずあるべき姿をテストコードで書く。テスト失敗する。テストが成功するようにコードを足していく、そしてリファクタリングを行うというスタイル。
staticpagesコントローラのタイトルをテストしてみよう
require 'test_helper' class StaticPagesControllerTest < ActionDispatch::IntegrationTest test "should get home" do get static_pages_home_url assert_response :success assert_select "title", "Home | Ruby on Rails Tutorial Sample App" end test "should get help" do get static_pages_help_url assert_response :success assert_select "title", "Help | Ruby on Rails Tutorial Sample App" end test "should get about" do get static_pages_about_url assert_response :success assert_select "title", "About | Ruby on Rails Tutorial Sample App" end end書くテスト3行目に足されたのは、
assert_selectはそのページのセレクトタグの中は以下のようになってますか?というもの3) Failure: StaticPagesControllerTest#test_should_get_home [/home/ec2-user/environment/sample_app/test/controllers/static_pages_controller_test.rb:8]: <Home | Ruby on Rails Tutorial Sample App> expected but was <SampleApp>.. Expected 0 to be >= 1. 3 runs, 6 assertions, 3 failures, 0 errors, 0 skipsちなみにテストを実行した結果。
最後の行は3つのテストが走って、6つのアサーションがある中で、3つが失敗したよーって意味。初めてのリファクタリング
<% provide(:title, "Home") %> <!DOCTYPE html> <html> <head> <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title> </head> <body> <h1>Sample App</h1> <p> This is the home page for the <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a> sample application. </p> </body> </html><% provide(:title, "Home") %>この部分は:titleに"Home"を代入している。
<%= yield(:title) %>はtitleをyieldメソッドで引っ張ってきてるという意味。
これにより、provideメソッドを除く、ドクタイプ宣言からheadタグまでが共通化できた、というかbodyタグの内側以外は全部同じなので外側にまとめようってこと。
<%=yield%>の中に各テンプレートが代入されるので、各テンプレートはbodyタグの内側だけ書いていれば良い。その結果application.html.erbに共通のコードをまとめる。
<!DOCTYPE html> <html> <head> <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title> <%= csrf_meta_tags %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> </head> <body> <%= yield %> </body> </html>yield(:title)のtitleは各テンプレートから引っ張ってくるよーってこと。
branch のマージ
トピックブランチで作業したら最後にmasterブランチにマージしてあげる必要がある。
トピックブランチ内で
$ git add -A
$ git commit -m "Finish static pages"$ git checkout master
ここでマスターブランチに移動。この時点でgit logをしてもトピックブランチで設定したコミットメッセージは表示されない。
$ git merge static-pages
これでトピックブランチの内容をmasterブランチに反映できる。この状態でgit push heroku master を実行すればデプロイできる。
minitest reporters
railstestでgreenやredを表示するときの設定
test/test_helper.rbENV['RAILS_ENV'] ||= 'test' require File.expand_path('../../config/environment', __FILE__) require 'rails/test_help' require "minitest/reporters" Minitest::Reporters.use! class ActiveSupport::TestCase # Setup all fixtures in test/fixtures/*.yml for all tests # in alphabetical order. fixtures :all # Add more helper methods to be used by all tests here... endrequire 'minitest/reporters'
Minitest::Reporters.user!
この2行を追加する。Guardによるテストの自動化
guardは何かファイルが更新されたら〜〜の処理を実行するというもの。
これをminitestと合わせると、ファイルを変えた時に勝手にテストを実行してくれる。1.$ bundle exec guard init
を実行して初期化する。2.$ sudo yum install -y tmux
cloud9の場合のコマンド。これによりguardの通知を受け取るのに必要なtmuxを有効にする。3.生成されたguardfileの先頭の行に
guard :minitest, spring: "bin/rails test", all_on_start: false do
を加える。4.gitignoreの最後の行に
# Ignore Spring files. /spring/*.pidを加える。
5.bundle exec guardを実行する。
- 投稿日:2019-11-30T12:18:51+09:00
deviseでログイン状態からsign_up(登録)画面に遷移した際のリダイレクト先
2種類のユーザ権限があるサービスで、ログイン状態かsign_up(登録)画面にした際に、想定してなかった画面にいリダイレクトされてこまったという話
構成
hoge_user
,fuga_user
という2種類のユーザがあります。それぞれ個別の認証が必要です。
また、それぞれのnamespaceでroot設定しています。# routes.rb namespace :hoge_users do root to: 'tops#show' ~略~ end namespace :fuga_user do root to: 'tops#show' ~略~ endさらに、親のルートでは
hoge_user
のルートに遷移するように設定しています。問題
fuga_user
でログイン済状態でfuga_user
のsign_up(登録)画面に遷移しようとするとhoge_user
トップにリダイレクトされてしまう。
ログを見ると以下のように遷移していました。
fuga_user/new
(sign_up)/
hoge_user/top
ログイン済の場合親のルートにリダイレクトされたため、そこから更に
hoge_user
ルートにリダイレクトされてました。
想定としては各ユーザのルート設定したとこにリダイレクトされると思ってたので困った。調査
deviseのコードを確認しました。
- newでは
require_no_authentication
が呼ばれログイン状態を確認する- ログイン済なら
after_sign_in_path_for
を呼ばれるなるほど。
after_sign_in_path_for
をoverrideしてないので親ルートにリダイレクトされると。
しかもこのアプリ構成上application_controller
ではなく個別のcontrollerでoverrideする必要がある。
→ 2種類のそれぞれのリダイレクト先を設定したいので
→session_controller
ではすでにやってた。今回はregistration_conroller
対応
全然DRYじゃないんだけど、それぞれのユーザ配下の
session_controller
,registration_controller
でafter_sign_in_path_for
をoverrideした。
これ以上増えるようだったらmixinするなり共通化します。
- 投稿日:2019-11-30T09:40:04+09:00
【Rails】Remember meチェックボックスと永続ログインのテスト【Rails Tutorial 9章まとめ】
Remember meチェックボックス
永続ログインするか否かを選択できるチェックボックスを、ログイン画面に実装する。
チェックボックス用フォーム
チェックボックスはユーザー情報入力用のフォームと同じ要領で作成できる。
app/views/sessions/new.html.erb<%= form_for(:session, url: login_path) do |f| %> . . . <%= f.label :remember_me, class: "checkbox inline" do %> <%= f.check_box :remember_me %> <span>Remember me on this computer</span> <% end %> <%= f.submit "Log in", class: "btn btn-primary" %> <% end %>チェックボックスから送信される値はparams[:session][:remember_me]に入っている。
チェックすれば1、チェックを外せば0である。
1であればremember(user)メソッドを呼び出して永続ログインし、0であればforget(user)メソッドを呼び出して一時ログインするように、Sessionsコントローラのcreateアクションを変更する。app/controllers/sessions_controller.rbdef 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_to user else flash.now[:danger] = 'Invalid email/password combination' render 'new' end endここで使われている三項演算子は、if文と同じ意味である。
条件式 ? 処理A : 処理B # if-else文と同じ if 条件式 処理A else 処理B end永続ログイン機能のテスト
永続ログインがちゃんと機能するか、テストを書く。
log_in_asテストヘルパー
Sessionsコントローラのlog_inヘルパーと同じ機能を持つ、テスト用のlog_in_asヘルパーメソッドを定義する。
ただし、統合テストではsessionメソッドが使えないようなので、統合テスト用に同じ名前のメソッドを作り、共にtest/test_helper.rbに置いておく。
こうすると、単体テストか統合テストかに関わらずlog_in_asメソッドでログインをテストできる。test/test_helper.rbclass ActiveSupport::TestCase fixtures :all # テストユーザーがログイン中の場合にtrueを返す def is_logged_in? !session[:user_id].nil? end # テストユーザーとしてログインする def log_in_as(user) session[:user_id] = user.id end end class ActionDispatch::IntegrationTest # テストユーザーとしてログインする(統合テスト用) def log_in_as(user, password: 'password', remember_me: '1') post login_path, params: { session: { email: user.email, password: password, remember_me: remember_me } } end end第二引数にpasswordキーをわざわざつけて、テスト用ユーザーに合わせてpasswordを変えれるようにしているようだが、あまり意味はないんじゃないかと思う。
remember_meキーは値を変更できるようにすることで、チェックボックスのチェック有り無しに対応できるようにしている。Remember meチェックボックスのテスト
チェックボックスのオンオフに対応する2つのテストを書く。
test/integration/users_login_test.rbtest "login with remembering" do log_in_as(@user, remember_me: '1') assert_not_empty cookies['remember_token'] end test "login without remembering" do # クッキーを保存してログイン log_in_as(@user, remember_me: '1') delete logout_path # クッキーを削除してログイン log_in_as(@user, remember_me: '0') assert_empty cookies['remember_token'] endチェック無しでのログインでは、一度チェック有りでログインした後ログアウトし、cookiesの値が0→1→0と変化していることを確認している。
なお、テスト内ではcookies[:remember_token]と書けず、cookies['remember_token']と書かないといけない謎の仕様があるらしい。assignsメソッド
テスト内ではUserモデルの仮想の属性であるremember_tokenにはアクセスできないが、assignsメソッドを使うとできるようになる。
今は割愛する。current_userのテスト
current_userメソッドでは、session[:user_id]が存在しない場合、cookies.signed[:user_id]が存在するか確認し、存在すれば該当するユーザーを探して現在のユーザーとして返していた。
また、その際にcookiesのトークンとUserオブジェクトのトークンが一致するか検証していた。
この2点をテストする。
(ここではテスト忘れを防ぐテクニックが紹介されているが、今は割愛する。)app/helpers/sessions_helper.rb# 記憶トークンcookieに対応するユーザーを返す def current_user if (user_id = session[:user_id]) @current_user ||= User.find_by(id: user_id) elsif (user_id = cookies.signed[:user_id]) user = User.find_by(id: user_id) if user && user.authenticated?(cookies[:remember_token]) log_in user @current_user = user end end endSessionsヘルパー用のテストを作成する。
$ touch test/helpers/sessions_helper_test.rbテストは以下のようになる。
test/helpers/sessions_helper_test.rbrequire 'test_helper' class SessionsHelperTest < ActionView::TestCase def setup @user = users(:michael) remember(@user) end test "current_user returns right user when session is nil" do assert_equal @user, current_user assert is_logged_in? end test "current_user returns nil when remember digest is wrong" do @user.update_attribute(:remember_digest, User.digest(User.new_token)) assert_nil current_user end endまずテスト用のユーザーにremember(user)メソッドを使い、トークンの作成とremember_digestへの保存、cookiesへのユーザーIDとトークンの保存を行なっている。
cookies[:user_id]が存在し、sessions[:user_id]が存在しない状態である。一つ目のテストは現在のユーザーが取得でき、それがテスト用ユーザーと一致するかを確認している。
current_userにはその後ログインしてsession[:user_id]にユーザーIDを入れる処理があるので、それも確認しておく。二つ目のテストは、テスト用ユーザーのトークンに異なるトークンを入れてみて、現在のユーザーが取得できるかを確認している。
- 投稿日:2019-11-30T08:19:28+09:00
【Rails】cookiesによる永続的なログイン【Rails Tutorial 9章まとめ】
永続ログインとcookies
一時的なログインの維持にはsessionメソッドを使用する。
しかし、ブラウザを閉じると自動的にログアウトしてしまう。ログイン状態を維持するためには、cookiesメソッドと、記憶トークンと呼ばれるデータを使用する。
記憶トークンはユーザーごとに固有な、パスワードのようなものである。
具体的な手順はこうなる。
①ユーザーごとに記憶トークンを作成する。
②記憶トークンをハッシュ化してUserモデルのカラム(remember_digest)に保存する。
③cookiesに記憶トークンとユーザーIDを入れる。
④ユーザーIDが入ったcookiesを受け取ったら、そのIDをもとにデータベースからユーザーを探し出し、そのremember_digestカラムに保存されているハッシュ値と、cookiesに入っているトークンをハッシュ化した値が一致するか確認する。記憶トークンの生成と保存
remember_digestカラム
マイグレーションファイルを作成して、Userモデルにremember_digestカラムを追加する。
このカラムには、記憶トークンをハッシュ化して保存する。$ rails generate migration add_remember_digest_to_users remember_digest:string
db/migrate/[timestamp]_add_remember_digest_to_users.rbclass AddRememberDigestToUsers < ActiveRecord::Migration[5.0] def change add_column :users, :remember_digest, :string end end記憶トークンの生成
記憶トークンはどんな文字列でもよいが、パスワードと違って個人が覚えておく必要がなく、かつセキュリティ上ランダムな文字列であることが望ましい。
ランダムな文字列を生成するために、ここではSecureRandomモジュールにあるurlsafe_base64メソッドを使う。$ rails console >> SecureRandom.urlsafe_base64 => "q5lt38hQDc_959PVoo6b7A"これを使って、トークンを生成するメソッドをUserモデルに定義する。
app/models/user.rb# 渡された文字列のハッシュ値を返す def User.digest(string) cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost BCrypt::Password.create(string, cost: cost) end # ランダムなトークンを返す def User.new_token SecureRandom.urlsafe_base64 endこのメソッドを個々のUserオブジェクトで使用することはないので、クラスメソッドとしている。
rememberインスタンスメソッド
User.new_tokenメソッドで生成したトークンをユーザーに結びつけ、remember_digestカラムにハッシュ化して保存するrememberメソッドを定義する。
ところで、パスワードを保存する場合は次のような手順を踏んでいた。
①passwordというUserモデルの仮想の属性に生のパスワードを入れる。
つまり、@user.password = "foobar"のようにしていた。
②それがbcryptとhas_secure_passwordによってハッシュ化されてpassword_digestカラムに保存される。これをトークンでも同じようにするのだが、トークンにはbcryptのような便利なものが無いので、今回はpassword属性に相当する仮想のremember_token属性を自分で作らねばならない。
仮想の属性を作るためには、attr_accessorを使う。app/models/user.rbattr_accessor :remember_token . . . # 渡された文字列のハッシュ値を返す def User.digest(string) cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost BCrypt::Password.create(string, cost: cost) end # ランダムなトークンを返す def User.new_token SecureRandom.urlsafe_base64 end # 永続セッションのためにユーザーをデータベースに記憶する def remember self.remember_token = User.new_token update_attribute(:remember_digest, User.digest(remember_token)) endattr_accessorで作った仮想の属性をメソッドの中で使うには、self.を頭につける。
(ところで、@remember_tokenではダメなんだろうか?)
update_attributeメソッドを使って、remember_digestカラムにハッシュ化したremember_tokenを代入する。ログイン状態の保持
cookiesとrememberヘルパーメソッド
記憶トークンを作成し、各ユーザーのデータベースに保存できるようになったので、次はcookiesメソッドに記憶トークンとユーザーIDを入れる。
session[:user_id]にユーザーIDを入れたのと同様に、cookies[:user_id]にユーザーIDを、cookies[:remember_token]にremember_tokenを入れる。cookies[:user_id] = user.id cookies[:remember_token] = user.remember_tokenここで、cookiesを永続化するために.permanentメソッドを使用する。
また、ユーザーIDはセキュリティ上の問題から.signedメソッドを使用して暗号化する。
これは署名つきcookieと呼ばれる。cookies.permanent.signed[:user_id] = user.id cookies.permanent[:remember_token] = user.remember_token署名つきcookieは自動で複合化されるので、そのままユーザー検索に使える。
User.find_by(id: cookies.signed[:user_id])これをSessionsコントローラのログイン部分で使用するために、Sessionsコントローラのヘルパーメソッドとして定義する。
app/helpers/sessions_helper.rb# 渡されたユーザーでログインする 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ここで非常にややこしいと思うのだが、このrememberヘルパーメソッドはデータベースにトークンを保存するために定義したrememberメソッドとは別物である。
前者はSessionsコントローラのアクション内で単独で使うが、後者はUserオブジェクトにつけて使うインスタンスメソッドである。このメソッドは、まず後者のrememberインスタンスメソッドでユーザーとトークンを紐付け(remember_digestカラムにトークンをハッシュ化して保存)、その後cookiesにユーザーIDとトークンを入れている。
記憶トークンの照合
cookiesに入ったトークンをハッシュ化して、Userオブジェクトのremember_digestカラムに入っているハッシュ値と比較する。
これが少し分かりにくく感じるが...
①cookiesには生のトークンを入れる。
②remember_digestにはハッシュ化されたトークンが入っている。
③cookiesの生のトークンをハッシュ化してみて、それが②と一致するか確かめる。
ということらしい。
これはログイン機能で使用したauthenticateメソッドと同じである。
authenticateメソッドは次のように定義されている。BCrypt::Password.new(password_digest)is_password?(unencrypted_password)左の()内にあるのはpassword_digestカラムに入っているハッシュ化されたパスワードである。
右の()内にあるのは生のパスワードである。これをトークンに当てはめるとこうなる。
BCrypt::Password.new(remember_digest).is_password?(remember_token)これをもとに、authenticated?メソッドをUserモデルに定義する。
app/models/user.rb# 渡されたトークンがダイジェストと一致したらtrueを返す def authenticated?(remember_token) BCrypt::Password.new(remember_digest).is_password?(remember_token) endここで使われているremember_tokenはこのauthenticated?メソッドでのみ使用できるローカル変数で、attr_accessorで作成したUserモデルの仮の属性ではない。
(これはややこしいので、適宜名前を変えた方がよいと思う。)
左の(remember_digest)はUserモデルのremember_digest属性のことで、self.remember_digestと同じである。永続的なログインの実装
必要なメソッドが揃ったので、Sessionsコントローラのcreateアクションを編集して、永続ログインを実装する。
ログインした後、rememberヘルパーメソッドを使用する。app/controllers/sessions_controller.rbdef create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) log_in user remember user redirect_to user else flash.now[:danger] = 'Invalid email/password combination' render 'new' end endcurrent_userメソッドの修正
永続ログイン機能を実装できたが、一つ問題がある。
現在ログインしているユーザーを返すcurrent_userメソッドは、session[:user_id]からユーザーを探している。
ログインフォームからログインした場合はsessionにもcookiesにもユーザーIDを入れるので問題ない。
しかし、その後ブラウザを閉じると、cookiesは残るがsessionが消えてしまうので現在のユーザーが取り出せなくなる。
つまりログインしているのに現在のユーザーを見つけられないことになる。これを、sessionが無い場合はcookiesからユーザーを探すように修正する。
app/helpers/sessions_helper.rb# 記憶トークンcookieに対応するユーザーを返す def current_user if (user_id = session[:user_id]) @current_user ||= User.find_by(id: user_id) elsif (user_id = cookies.signed[:user_id]) user = User.find_by(id: user_id) if user && user.authenticated?(cookies[:remember_token]) log_in user @current_user = user end end endcookiesからユーザーを見つけたら、(この時点ではログインできていないはずなので)ログインする。
ここは、ログイン時にemailからユーザーを見つけ、パスワードをauthenticateメソッドで照合するのと全く同じ要領である。
cookies[:user_id]からユーザーを見つけ、トークンをauthenticated?メソッドで照合する。
最後に、現在のユーザーを返す。なお、sessionとcookiesは繰り返し処理を省くために、if文の条件式でローカル変数user_idに入れてから使っている。
永続セッションからのログアウト
永続ログイン状態を解除するために、永続ログインで作った2つのrememberメソッドにそれぞれ対応し、逆の操作をする2つのforgetメソッドを定義して、ログアウト処理に組み込む。
forgetインスタンスメソッド
Userモデルのrememberメソッドでは、Userオブジェクトのremember_digest属性にトークンをハッシュ化して保存した。
forgetメソッドはその逆で、remember_digest属性を空にする。app/models/user.rb# ユーザーのログイン情報を破棄する def forget update_attribute(:remember_digest, nil) endforgetヘルパーメソッド
Sessionsヘルパーのremember(user)メソッドでは、rememberインスタンスメソッドを呼び出した後、cookiesにユーザーIDとトークンを入れた。
forget(user)メソッドはその逆である。app/helpers/sessions_helper.rb# 永続的セッションを破棄する def forget(user) user.forget cookies.delete(:user_id) cookies.delete(:remember_token) endlog_outメソッドの修正
最後に、log_outメソッドにforget(user)メソッドを追加すればログアウト処理が完成する。
app/helpers/sessions_helper.rb# 現在のユーザーをログアウトする def log_out forget current_user session.delete(:user_id) @current_user = nil end2つのバグ
完成した永続ログイン機能は、小さなバグが2つあるらしい。
…が、今のところ重要ではなさそうなので割愛する。永続ログインのテスト
テスト部分は別記事にまとめることにする。
- 投稿日:2019-11-30T08:09:37+09:00
スクレイピング
とは
Web上のHTMLからデータを取ってくる処理
【その1】gemをインストール
Gemfilegem 'mechanize'ターミナルbundle install
【その2】Mechanizeクラスを使うことを宣言
scraping.rbrequire 'mechanize'
【その3】インスタンス生成
scraping.rbrequire 'mechanize' agent = Mechanize.new
【その4】URLからそのページのHTMLを取得
get("URL")
scraping.rbrequire 'mechanize' agent = Mechanize.new page = agent.get("https://bluelog2.herokuapp.com/")
【その5】要素を取得
search('要素')
scraping.rbrequire 'mechanize' agent = Mechanize.new page = agent.get("https://bluelog2.herokuapp.com/") elements = page.search('#bluelog')タグ指定
('h1')
やクラス指定('.manbow')
もできます。複数続けて、ここの中のここ!みたいなこともできます。
('h2 a')
→h2タグの中のaタグを指定
【その6】テキストや属性値を取得
inner_text
で中のテキストscraping.rbrequire 'mechanize' agent = Mechanize.new page = agent.get("https://bluelog2.herokuapp.com/") elements = page.search('#bluelog') elements.each do |element| puts element.inner_text end
[:属性]
でその属性値scraping.rbrequire 'mechanize' agent = Mechanize.new page = agent.get("https://bluelog2.herokuapp.com/") elements = page.search('#bluelog') elements.each do |element| puts element[:href] end実際にやってみる
適当なファイルを作って、以下をコピペ
scraping.rbrequire 'mechanize' agent = Mechanize.new page = agent.get("https://bluelog2.herokuapp.com/") elements = page.search('#bluelog') elements.each do |element| puts element.inner_text end
ファイルを置いたディレクトリに移動しターミナルruby scraping.rbを実行
ターミナルBlueLogと表示されたら成功
ちなみにこのサイトは僕が作ったスキューバダイビングの記録をつけるアプリケーションです!
ではまた!
- 投稿日:2019-11-30T01:04:44+09:00
Rails Tutorial Memo #1
自分用の備忘録です.
第7章 ユーザー登録
Rails.env.development?
Railsオブジェクトには環境の論理値(boolean)を取るenvという属性がある.
Rails.env.test?
はテスト環境ではtrueを返し,それ以外の環境ではfalseを返す.
rails c
やrails s
はデフォルトではdevelopmentが使用されるが,他の環境を明示的に実行することもできる.
- コンソールをtest環境で起動
rails c test
- サーバーをproduction環境で起動
rails s --environment production
debugger
直接的にデバッグできる.
アプリケーションの中でよく分からない挙動が起きた時にdebuggerを差し込んで調べる.
トラブルが起こっていそうなコードの近くに差し込むのがコツ.helper
ある動作を処理する場合にメソッド化して扱えるようにRailsにあらかじめ組み込まれた機能.
link_to
,form_with
,label_for
,image_tag
などは全てhelperフォームHTML
<%= f.label :name %> <%= f.text_field :name %>は
<label for="user_name">Name</label> <input id="user_name" name="user[name]" type="text" />を生成する
SSL
ローカルのサーバからネットワークに流れる前に,大事な情報を暗号化する技術.