- 投稿日:2019-12-18T23:25:27+09:00
Railsで特定のURLからのリクエストを許可してiframeを表示させたい、、、
やりたいこと
Railsでiframeからのリクエストを許可して外部のサイトで表示させたい、、、
でもすべて許可するとなにかと怖いので特定のURLに絞りたい、、、ということで
Railsでは
response.headers['X-Frame-Options'] = 'SAMEORIGIN'が設定されていて自身のドメインしかiframeで表示出来ないそう
今回は特定のURLのみ許可したいので以下のように実装
class HogeController < ApplicationController after_action :allow_iframe, only: [:hoge] def hoge render "hoges/index", layout: false end private def allow_iframe url = "https://hoge.com" response.headers['X-Frame-Options'] = "ALLOW-FROM #{url}" response.headers['Content-Security-Policy'] = "frame-ancestors #{url}" end end注意点
なんと皆がよく使ってるであろうChrome、Safariなど多くのブラウザが
X-Frame-Options ALLOW-FROM
に対応していません1なので合わせて
Content-Security-Policy frame-ancestors
も記述してやりましょう2調べてみた感じIE以外対応してたので安心です3
効いてるか確認
Chrome DevToolsの
Network
パネルで確認できます
X-Frame-Options ALLOW-FROM
だけ設定してしまうと未対応ブラウザですべてのURLを許可する状態になってしまいます ↩
- 投稿日:2019-12-18T23:23:24+09:00
新規アプリケーションをGithubで管理する(Rails)
はじめに
Railsを使ってプログラミングの学習をしているのですが、新規アプリケーションを作成する際、Githubで管理する方法を忘れてしまうので、定着させる意味でも残したいと思います。
環境
OS: Mac
Rails 5.0.7.2
Ruby 2.5.1
MySQL
Github Desktop手順
1,新規アプリケーションを作成
ターミナル$ cd myapp #作成したいディレクトリへ移動 $ rails _5.0.7.2_ new my-blog -d mysql #Railsのバージョンを指定、DBはMySQLを指定する。 $ cd my-blog #作成したアプリケーションのディレクトリへ移動 $ rails db:create #データベース作成2,Github Desktopからローカルリポジトリにする
Github Desktop左上のCurrent Repository
をクリック→Add
→Add Existing Repository
を選択。Add Repositoryできない場合
ターミナルにて、作成したアプリのリポジトリ→git init
のコマンドを打てば解消されます。
左下から最初のコミットをする。
Publish repository
をクリック。
公開してもいいならKeep this code private
のチェックを外し、Publish Repository
をクリック。3,Githubのリモートリポジトリが作成される
- 投稿日:2019-12-18T22:37:32+09:00
オブジェクト指向設計を実践するためのまとめ
Zeals Advent Calendar 2019 の19日目の記事です。
サーバサイドエンジニアの小寺です。私がまだ初学者に近かった頃、「もっと早く知っておけばよかった!」と心の底から思ったオブジェクト指向設計についての知識を書き残しておこうと思います。
今回は、実務で求められる「保守性の高い読みやすいコード」を実践するために
オブジェクト指向設計実践ガイドを学習し、理解できたところを自分なりに要点をまとめ・メモし、備忘録としたものです。
(※4章までの内容が理解できれば十分実践していけると思いますので4章までしかまとめてないです)参考書籍
オブジェクト指向設計実践ガイド
本書のサンプルコードを引用しています。対象
- オブジェクト指向についてふんわり理解している人
- 責務がどうのこうのといったくだりをふんわり理解してる人
- オブジェクト指向設計を理解してステップアップしたい初学者
2章 単一責任を意識する
- なぜ単一責任が必要なのか
- 再利用したい
- 再利用できると未来の予期せぬ変更に対応できる
- 悪いコードを増殖させたくない
単一責任の見極め方
- 役割を聞く
# 「オブジェクト指向設計実践ガイド」 p41 class Gear attr_reader :chainring, :cog, :rim, :tire def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog @rim = rim @tire = tire end : def gear_inches ratio * (rim + (tire * 2)) end endgear = Gear.new(52, 11, 26, 1.5) gear.ratio # ギアに対してギア比を聞くのは良い gear.gear_inches # ギアに対してgear_inchesを聞くのはしっくりこないような気がする gear.tire # ギアに対してタイヤのサイズを聞くのはもっとおかしいインスタンス変数の隠蔽
# 「オブジェクト指向設計実践ガイド」 p46 class Gear def initialize(chainring, cog) @chainring = chainring @cog = cog end def ratio @chainring / @cog.to_f # <-- 破滅への道 end endアクセサメソッド(attr_reader)で隠す
# 「オブジェクト指向設計実践ガイド」 p46 class Gear attr_reader :chainring, :cog # <-- def initialize : def ratio chainring / cog.to_f # <-- end end実装的には以下と同じ
def cog @cog end実際に得ている恩恵としては未来に複雑な実装が来た時に変更が簡単
def cog if 条件 @cog * 何かの計算 else (@cog * 何かの計算) + 複雑な計算 end endデータ構造の隠蔽
# 「オブジェクト指向設計実践ガイド」 p48 class ObscuringReferences attr_reader :data def initialize(data) @data = data end def diameters # 0はリム, 1はタイヤ <-- 何のデータがどこにあるか知っている状態 # 配列の構造が変わった時などに怖い data.collect {|cell| cell[0] + (cell[1] * 2) } end end # 以下のデータが必要になる @data = [[622, 20],[622, 23],...]Structクラスを使って構造を包み隠す
# 「オブジェクト指向設計実践ガイド」 p50 class ObscuringReferences attr_reader :wheels def initialize(data) @wheels = wheelify(data) end def diameters # wheelが rim と tire を持ってることだけ知っていればいい wheels.collect {|wheel| wheel.rim + (wheel.tire * 2) } end # これで誰でも wheel に rim/tire を送れる Wheel = Struct.new(:rim, :tire) def wheelify(data) data.collect {|cell| cell[0] + (cell[1] * 2) } end endメソッドから余計な責任を抽出する
メソッドもクラスと同じく単一の責任を持つべき(再利用が簡単になるので)
先ほどの diameters を見てみる# 「オブジェクト指向設計実践ガイド」 p52 class ObscuringReferences : def diameters # wheelsを繰り返し処理するのと、それぞれの直径を計算する # といった2つの責任を持っている wheels.collect {|wheel| wheel.rim + (wheel.tire * 2) } end : end上記のメソッドを簡単に変更できるようにしていく
# 「オブジェクト指向設計実践ガイド」 p52 class ObscuringReferences : def diameters # 直径を計算する処理を別にした wheels.collect {|wheel| diamete(wheel) } end # diameter を呼べるようになった(他で再利用可能になった) def diameter(wheel) wheel.rim + (wheel.tire * 2) end : end上記はよくある責任が複数あるわかりやすい例。
大抵はこれほど明確ではない。先ほどのGearクラスを思い出してみる。# 「オブジェクト指向設計実践ガイド」 p53 class Gear : # 何か不確定でのちにトラブルを起こしそうな気がする # このメソッドが2つ以上の責任を持ってしまっている def gear_inches ratio * rim + (tire * 2) end : endgear_inches に隠れている直径の計算を抽出してみる
# 「オブジェクト指向設計実践ガイド」 p53 class Gear : # Gearがgear_inchesの計算するのはよし def gear_inches ratio * diameter end # しかし、車輪の直径の計算までするのはおかしい def diameter rim + (tire * 2) end : endといったように、小さなリファクタリングではあるが Gear が持つべきでない責任が明らかになりました。
このように、あらゆるものを単一責任にしていくことで
- 隠蔽された性質を明らかにする
- 上記の例だと 「Gear は diameterを持つべきではない」
- コメントをする必要がない
- メソッドがコメントの役割を果たす
- 再利用を促進する
- 他のプログラマーは複製ではなく再利用をするようになる
- 他のクラスへの移動が簡単
- 小さなメソッドは簡単に動かすことができる
といった恩恵が受けれます
クラス内の余計な責任を隔離する
一旦全てを単一責任するとクラスのスコープが明白になってきます。
GearクラスにはいくつかのWheel(車輪)のような振る舞いが隠れていました。
ここで、新しくクラスを作ってもいいのですがそうなると変更のコストが大きいかもしれないのでコストを最小限にしつつGearの単一責任を保つようにします。# 「オブジェクト指向設計実践ガイド」 p55 class Gear attr_reader :chainring, :cog, :wheel def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog @wheel = Wheel.new(rim, tire) end def ratio chainring / cog.to_f end def gear_inches ratio * wheel.diameter end # 隠れていた Wheel のような振る舞いを手に入れた # 書籍ではこの後追加要望があったタイミングで新しいクラスを定義する # 「あとで」決定できる力を取っておくことはアプリケーション開発において重要 Wheel = Struct.new(:rim, :tire) do def diameter rim + (tire * 2) end end end本書では、新しくアプリケーションに車輪に関する追加機能の要望が来たタイミングで Wheel クラスを新しく定義するよう書かれています。
アプリケーション開発において未来を予測することは困難なのでできる限り「あとで」決定できる力を残しておくのは非常に重要なことである3章 依存関係を管理する
依存関係を認識する
オブジェクトが次のものを知っている時、オブジェクト間には依存がある
- 他のクラスの名前を知っている
- GearはWheelというクラスが存在すると予想している
- self以外のメッセージの名前
- GearはWheelがdiameterに応答できると知っている
- メッセージが要求する引数
- Gearは Wheel.newするときに rim とtireが必要なことを知っている
- 引数の順番
- 最初が rim で 2番目が tire だと知っている
# 「オブジェクト指向設計実践ガイド」 p60 class Gear attr_reader :chainring, :cog, :rim, :tire def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog # Wheel.newするときに rim とtireが必要なことを知っている @rim = rim @tire = tire end def ratio chainring / cog.to_f end def gear_inches # Wheelというクラスが存在すると予想している # Wheelがdiameterに応答できると知っている # 最初が rim で 2番目が tire だと知っている ratio * Wheel.new(rim, tire).diameter end : end class Wheel attr_reader :rim, :tire def initialize(rim, tire) @rim = rim @tire = tire end def diameter rim + (tire * 2) end : end依存が発生している時、依存が強固になりすぎると2つのオブジェクトは変更することが難しくなってくるので2つのオブジェクトがあたかも1つのオブジェクトとして振る舞うようになってしまう。
疎結合なコードを書く
依存を減らすための技法を紹介する
依存オブジェクトの注入
クラス内でインスタンスを生成するのではなく diameterを知っているオブジェクトを受け取るようにしておく
# 「オブジェクト指向設計実践ガイド」 p66 class Gear attr_reader :chainring, :cog, :wheel def initialize(chainring, cog, wheel) @chainring = chainring @cog = cog @wheel = wheel end def ratio chainring / cog.to_f end def gear_inches ratio * wheel.diameter end : end # Gearはdiameterを知っているオブジェクトを要求する Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches依存を隔離する
- インスタンス変数の作成の分離
- 制約が厳しく、オブジェクトを注入できないような場合
- クラス内で分離するしか方法がないとき
# 「オブジェクト指向設計実践ガイド」 p68 class Gear attr_reader :chainring, :cog, :rim, :tire def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog # Gearはまだ知りすぎているが、gear_inchesの依存が減っている @rim = rim @tire = tire end : def gear_inches ratio * wheel.diameter end # wheel が呼ばれるまでインスタンスは作成されない def wheel @wheel || Wheel.new(rim, tire) end end
- 脆い外部メッセージを隔離する
- 外部への参照がクラスに埋め込まれていて、どれが変更されやすい時
# 「オブジェクト指向設計実践ガイド」 p70 def gear_inches # 恐ろしい計算 foo = some_intermedicate_result * wheel.diameter # 恐ろしい計算 end上記のように複雑な状況になった時にメソッドの中に依存が隠れてしまう。
そうした場合以下のように脆い部分を隔離する
# 「オブジェクト指向設計実践ガイド」 p71 def gear_inches # 恐ろしい計算 foo = some_intermedicate_result * diameter # 恐ろしい計算 end def diameter # もし、Wheelがdiameterに変更加えた場合副作用はここだけになる wheel.diameter end引数の順番への依存を取り除く
- 初期化の際にハッシュ使う
# 「オブジェクト指向設計実践ガイド」 p73 class Gear attr_reader :chainring, :cog, :wheel def initialize(args) @chainring = args[:chainring] @cog = args[:cog] @wheel = args[:wheel] end : end # 冗長にはなったが、引数の順番を知っていなくても大丈夫になった # ハッシュのkey名に依存はしているが、順番よりは健康的 Gear.new( chanring: 52, cog: 11, wheel: Wheel.new(26, 1.5) ).gear_inches安定性の高い引数をいくつか受け取って、安定性の低い引数はオプション引数で受け取るといった手法が多く用いられる
- 明示的にデフォルト値を設定する
# 「オブジェクト指向設計実践ガイド」 p74 def initialize(args) # 真偽値以外の場合はシンプルに || を使える @chainring = args[:chainring] || 40 @cog = args[:cog] || 18 @wheel = args[:wheel] end # 真偽値を使う or nilを設定する場合 def initialize(args) @chainring = args.fetch(:chainring, 40) @cog = args.fetch(:cog, 40) @wheel = args[:wheel] end
- 複数のパラメーターを用いた初期化を隔離する
- もし、引数を簡単に変更できないような状況下にあった場合ラッパークラスを定義して包み隠すようにする
# 「オブジェクト指向設計実践ガイド」 p77 # Gearがもし外部インターフェースの一部の場合(このクラスを変更できない場合) module SomeFramework class Gear attr_reader :chainring, :cog, :wheel def initialize(args) @chainring = args[:chainring] @cog = args[:cog] @wheel = args[:wheel] end : end end # 外部のインタフェースをラップし、自信を変更から守っている # 他のオブジェクトを生成することが目的のオブジェクトはファクトリーと呼ぶ module GearWrapper def self.gear(args) SomeFramework::Gear.new( args[:chainring], args[:cog], args[:wheel] ) end end GearWrapper.new( chanring: 52, cog: 11, wheel: Wheel.new(26, 1.5) ).gear_inchesこのテクニックは自分で変更がきかない外部のインターフェースに依存する場合に適している。
依存方向の管理
「自分より変更が少ないものに依存すべし」
4章 柔軟なインターフェースを作る
パブリックインターフェースとは
クラスのパブリックインターフェースを作り上げるメソッドは以下のような特性を備えている
- クラスの主要な責任を明らかにする
- 外部から実行されることが想定される
- 気まぐれに変更されない
- 他者がそこに依存しても安全
- テストで完全に文書化されている
パブリックインターフェースを見つける
「アプリケーション例: 自転車旅行会社」
ユースケースとして
「参加者は適切な難易度の、特定の日付の、自転車を借りられる旅行の一覧をみたい」見当をつけるためにシーケンス図を使う
オブジェクト間でやり取りされるメッセージを気軽に実験することができる
ここで起きる疑問
「Tripが利用可能な自転車まで調べなくてもいいんじゃないか?」シーケンス図を描くことによって
「このオブジェクトが〇〇という責任を負うべきなのだろうか?」と疑問が湧くようになるある旅行に対して自転車が利用可能かどうかTripクラスが見つけ出すべきでない場合、
Bycycleクラスがありそう。
- Tripはsuiable_tripsに責任があり
- Bycycleはsuitable_bycycle に責任を持つ
変更後は Tripから余計な責任は取り除けたものの Customer に移しただけ
変更後は Customer が何を望むのかと他のオブジェクトがどのようにそれを準備するのかまで知ってしまっている「どのように」を伝えるのではなく「何を」を頼む
新たなユースケース
「旅行が開始されるためには使われる自転車が全て整備されていることを確実にする」TripはMechanicがどのように整備するのか(
clean_bicycleして
pump_tiresして
lube_chainして
check_brakesするという手順
)を知ってしまっている。Mechanicが新たな整備手順を増やした時は、Tripも変更しなければならない
以下は対案
Tripにあった責任をほとんどMechanicに渡している
自転車の準備することに関して「どのように」はMechanicの責任になった
TripはMechanicにどんな改善があろうともprepare_bicycleから正しい振る舞いを得ることができる上記のようにMechanic と Trip の会話が 「どのように」から「何を」に変わった副作用として
Mechanicのパブリックインターフェースのサイズが小さくなったパブリックインターフェースが小さいということは他のところから依存されるメソッドがわずかしかないことを意味している
コンテキストの独立を模索
旅行の準備には「いつでも」自転車の準備が求められるためTripは「常に」prepare_bicycleメッセージを自身のMechanicへ送らなければならない
Tripが旅行が準備されることをMechanicに伝え、Mechanicは準備にbicyclesが必要なのでTripにコールバックをし、自転車の整備を行います。
こうして、整備士がどのように自転車を準備するかはMechanicクラスに隔離されました。
オブジェクトを見つけるためにメッセージを使う
Tripが知りすぎていたのを改善したが、今度はCustomerが知りすぎている。
上記のアプリケーションは要件を満たす新たなオブジェクトを必要としていることがなんとなく見えてきた。新たなTripFinderは安定したパブリックインターフェースを提供し、複雑な内部に関しては隠している
シーケンス図便利ー
まとめ
単一責任を意識しながらコードを書けるようになりましょう
- そのために常にメソッドの役割をメソッド自身に問い続ける
- 抽出できるものはできる限り抽出しておくと未来に変更が簡単になる
クラスの中で依存関係があることを認識する
- 状況に応じて疎結合にするための技法を使い分ける
オブジェクト間で交わされるメッセージを中心にアプリケーションを設計する
- 議論を進める時にはシーケンス図が有効
- どのようにではなくオブジェクトが「何を」要求するかに注目する
おわり
- 書籍の中に重要な説明がたくさんあるので本読みましょう(オブジェクト指向設計実践ガイド)
- 今回はあくまでコードベースで要点まとめただけなので
- 柔軟に進化し続けましょう
明日は20日目の @neuneu39 の番です。お楽しみに!!!
- 投稿日:2019-12-18T22:21:00+09:00
railsでSQLログをapplicationログとは別に出力する
railsでSQLログをapplicationログとは別に出力する
applicationログはinfoで出したいけど、別途SQLログも出したい時など
rubyActiveRecord::Base.logger = Logger.new('log/sql.log') ActiveRecord::Base.logger.formatter = proc do |severity, timestamp, progname, msg| { timestamp: timestamp.iso8601, LOG_LEVEL: severity, message: msg }.to_json << "\n" end動作環境
rails 6.0.2
ruby 2.6.3
- 投稿日:2019-12-18T21:47:28+09:00
Lineのやり取りをSlackに通知する
この記事は GLOBIS Advent Calendar 2019 - Qiita の18日目です。
こんにちは、グロービスでエンジニアをしている @shifumin です。
皆さん、Lineを使っていますか? 僕は通知オフで使っています。理由は通知で集中力が切れるのが嫌だからです。
しかし、通知オフ設定だと送られたメッセージに長時間気づかずに怒られイベントがたまに発生しますよね? 僕はたびたび発生しています。
皆さん、Slackを使っていますか?使っていますよね。凝視していますよね。
Slackなら常に見ているから見逃さないんだけどなあ。というわけで、多少矛盾を孕んでいますが、通知オフにしたLineで送られたメッセージをSlackに通知させるためのbotを作っていきたいと思います!
概要を3行で
- 通知させたいLineグループにLine botをjoinさせてLine Messaging APIでメッセージを取得する
- 上記で取得したメッセージをSlack botとしてSlackに通知する
- botはRailsに乗せてHerokuで動かす
はい、やっていきましょう。
Line Messaging APIを利用する準備
Lineの送られてきたメッセージを取得するのに LINE Messaging API を利用します。
LINE Developersコンソールでチャンネルの作成とbotの設定を行います。
設定方法は公式のドキュメントが詳しいので、 Messaging APIを利用するには と ボットを作成する を参考にするのが楽です。botの作成まで終えた後に各種設定を変更します。
- グループ・複数人チャットへの参加を許可する
- オン
- あいさつメッセージ(Greeting messages)
- オフ
- 応答メッセージ(Auto-reply messages)
- オフ
- Webhook
- オン
Webhook設定はURLももちろん設定する必要があるのですが、これは後でHerokuでappを作成後に設定します。
設定後は下記の2つの値が必要になるため、メモります。
発行されていなければ、それぞれIssue
,Reissue
から発行できると思います。
- Channel secret (Basic settingsの中)
- Channel access token (Messaging APi settingsの中) です。
botとフレンドになる
MessagingAPi > QR code にQRコードが表示されているのでbotをフレンド登録します。
また、複数人でのグループチャットを通知させたい場合はグループに招待します。Slackの設定
続いて、SlackのApp設定とbot追加を行います。
https://api.slack.com/apps のCreate New App
からAppを作成します
まず、Appの名前と導入するWorkspaceを入力します。その後Appの設定をいくつか行う必要があります。
Botの作成
- Botsから
Add a Bot User
をクリックしてbotを作成するPermissionsの設定
- PermissionsをクリックしOAuth & Permissions画面へ移動する
- OAuth Tokens & Redirect URLs >
Install App to Workspace
をクリックしてAppをインストールする- Scopesで下記のScopeを追加する
- chat:write:bot
- chat:write:user
- files:write:user
また、下記の値が必要になるため、メモります。
- OAuth Access Token (OAuth & Permissions画面)
Railsアプリ作成
ここからようやくBotの実装に進めていきます。
rails new
rails newまではよしなにやってください。
とりあえず最速でrails new。# Railsをインストールまで bundle init vim Gemfile # Gemfileの `# gem "rails"` 行のコメントアウトを外す bundle install
rails new
は今回は画面描画系がいらないのでAPIモードかつ必要ないものを全てスキップしています。
参考: 小さく薄くrails newする(ViewやJSが必要ない場合) - Qiitabundle exec rails new . --skip-yarn --skip-git --skip-action-mailer --skip-action-mailbox --skip-action-text --skip-active-record --skip-active-storage --skip-action-cable --skip-sprockets --skip-javascript --skip-turbolinks --skip-test --api --skip-bundleSDKのインストール
Line、Slackそれぞれ公式が出しているSDKのgemを使います。
line-bot-api と slack-ruby-client です。
この2つをGemfileに追加してbundle install
します。# Gemfile # add gem 'line-bot-api' gem 'slack-ruby-client'bundle installコード実装
各SDKの設定
Slackは
configure
で設定できたのでconfig/initializers/
以下に設定を用意し、Line::bot::Client
はできなかったので仕方なくApplicationControllerでクライアントを作成する際に設定するようにしました。# config/initializers/slack_ruby_client.rb require 'slack' Slack.configure do |config| config.token = ENV['SLACK_API_TOKEN'] end Slack::Web::Client.configure do |config| config.user_agent = 'Slack Ruby Client/1.0' end# app/controllers/application_controller.rb class ApplicationController < ActionController::Base private def line_client @line_client ||= Line::Bot::Client.new do |config| config.channel_secret = ENV["LINE_CHANNEL_SECRET"] config.channel_token = ENV["LINE_CHANNEL_TOKEN"] end end def slack_client @slack_client ||= Slack::Web::Client.new end endroutes
Webhookはpostでリクエストが来るので、
/callback
で受けて WebhookController#callbackに渡すようにしました。# config/routes.rb Rails.application.routes.draw do post '/callback' => 'webhook#callback' endBlocked Hostの設定
Rails6から導入されたBlocked hostの設定は良しなにやってください。
今回はHerokuでdevelopment
で動かすかつ何も考えずに全開放ということで
下記のようにしています。
Rails6でrails newするのは今回が初めてでWebhookが届かないなあと記事を書くにあたりここでめっちゃ嵌った。嵌りポイント1# config/environments/development.rb Rails.application.configure do # 略 config.hosts.clear endcontroller
line-bot-api と slack-ruby-client のREADMEとサンプル実装を見ながらbotの本実装である
WebhookController#callback
を書いていきます。 今回は
- メッセージが送られた場合はそのメッセージをそのままSlackに送る
- 写真が送られてきた場合はその写真を保存して(Herokuなのでそのうち消えるけど)、Slackにアップロードする
- ビデオ、スタンプの場合はそれぞれが送られてきた旨のメッセージを送る
ようにしました。
# app/controllers/webhook_controller.rb class WebhookController < ApplicationController CHANNEL = '#{botに通知させるチャンネル名: 例: #hoge}' def callback body = request.body.read signature = request.env['HTTP_X_LINE_SIGNATURE'] unless line_client.validate_signature(body, signature) error 400, 'Bad Request' end events = line_client.parse_events_from(body) events.each do |event| case event when Line::Bot::Event::Message case event.type when Line::Bot::Event::MessageType::Text post_message(event.message['text']) when Line::Bot::Event::MessageType::Image image_response = line_client.get_message_content(event.message['id']) file = File.open("/tmp/#{Time.current.strftime('%Y%m%d%H%M%S')}.jpg", 'w+b') file.write(image_response.body) slack_client.files_upload(channels: CHANNEL, file: Faraday::UploadIO.new(file.path, 'image/jpeg'), as_user: true, title: File.basename(file.path), filename: File.basename(file.path), initial_comment: '写真が送信されました') when Line::Bot::Event::MessageType::Video post_message('ビデオが送信されました。') when Line::Bot::Event::MessageType::Sticker post_message('スタンプが送信されました。') end end end head :ok end private def post_message(text) slack_client.chat_postMessage(channel: CHANNEL, text: text) end endHerokuのデプロイと設定
あとはHerokuでアプリを作成してデプロイして設定したら完了です。
アプリを作成してデプロイする方法はそこら辺に転がっているので割愛します。環境変数の設定
上の方でメモっておいた下記の環境変数を設定します。
- LINE_CHANNEL_SECRET
- Lineの
Channel secret
- LINE_CHANNEL_TOKEN
- Lineの
Channel access token
- SLACK_API_TOKEN
- Slackの
OAuth Access Token
Dynoの起動
Dynoも忘れずに起動させます。
Herokuを触るのが久しぶりで起動を忘れていて、動いていないHerokuに対してWebhookを大量に空振りさせていた。嵌りポイント2Webhook URLの設定
最後にWebhook URLを設定したら完了です。
Settngs > Domains のURLをLine DevelopersコンソールのMessaging API > Webhook settingsのURLに設定します。
エンドポイントは/callback
としているので忘れないようにします。実際の動作
グループにbotを招待してLineからメッセージを送ると下記のようにSlackに通知されます。
(左: Line, 右: Slack)また、Lineの通知をオフにしていても下記のような重要なメッセージを見逃すことが減ります
(なぜならSlackは見ているので)終わりに
今回はLineのやり取りをSlackに通知するbotを作ってみました。
公式で用意されているIntegration等でSlackとLineを直接繋ぐことはできないですが、それぞれのアプリがAPIとそのSDKを用意してくれているので、少しコードを書けばこの記事のようにそれぞれのアプリを連携させることができます。
個人開発といえばアプリ作成をまず思い浮かべるかと思いますが、APIを利用したbot開発や便利スクリプト作成も手間の割に日々の生活が便利になったりするのでオススメです!!
- 投稿日:2019-12-18T21:28:52+09:00
[Rails6]ActionTextの入力フォームが伸びて困る
はじめに
就活のポートフォリオサイトとしてAsobiというWebサイトを作成しました。(QiitaにAsobiに関しての記事を書いています。就活用ポートフォリオとしてWebサービス「Asobi」を作りました。)
このサイトの中でRails6から新しく導入されたActionText
を使っているのですが、入力フォームが行数に応じて伸びてしまいます。今回はそんなActionTextの入力フォームをいい感じにするためにやったことをご紹介します。
実行環境
- Ruby 2.6.5
- Rails 6.0.2
ActionTextとは
Rails6から実装されたリッチテキストコンテンツと編集機能を導入する機能です。
ActionTextのインストールと導入したいモデルとカラムの用意を行い、少しコードを書くだけでブログのようなリッチテキストエディタが導入できます。
Ajaxを利用した画像のアップロードも実装されており、アップロードされた画像は
ActiveStorage
を利用して保存されます。今回ActionTextの導入に関しては割愛させていただきます。
導入に関しては下記の記事が参考になるかと思います。Rails6新機能 ActionText使用方法
Rails 6 と Action text を使ってみる - もふもふ技術部伸びて困る
ActionTextで生成されるリッチテキストエディタの入力フォームは、行数に応じて高さが伸びるようになっています。
デフォルトの挙動
これをQiitaやはてなブログのような、はみ出た部分をスクロールバーで表示するように実装します。
こんな風な挙動にしたい
実装
Rails
側でこれを実装する方法を見つけられなかったため、CSS
で実装します。Trixについて
実装の前に、ActionTextに含まれる
Trix
について説明します。
Trix
とはリッチテキストエディタを実装するJavascriptのライブラリです。(ちなみに開発元はRuby on Railsと同じBasecampです)
Trixのドキュメントを見てみると、Place an empty <trix-editor></trix-editor> tag on the page. Trix will automatically insert a separate <trix-toolbar> before the editor.
Like an HTML <textarea>, <trix-editor> accepts autofocus and placeholder attributes. Unlike a <textarea>, <trix-editor> automatically expands vertically to fit its contents.とのことで、ページのTrixのエディタを入れたい場所に
trix-editor
タグを書き込むことで使えるようになります。ActionTextの場合、
erb
ファイルに<%= form.rich_text_area :body %>と記述することで
trix-editor
タグが生成されます。
そしてリッチテキストエリアへの入力はtrix-editor
タグの子要素にDOMとなって反映されていきます。なので、入力フォームのスタイルは
親要素であるtrix-editor
タグのCSSを記述することで調整できそうです。CSSの記述
入力フォームの高さを固定するために
min-height
とmax-height
を、そして入力フォームからはみ出した部分はoverflow-y: auto;
を設定することでスクロールバーで表示できるようにします。以下のCSSを記述します。(
actiontext.scss
が/app/assets
か/app/javascript
に生成されていると思うので、そこに追記することをオススメします)trix-editor { min-height: 20em; max-height: 20em; overflow-y: auto; }以上で目指す挙動の入力フォームになっていると思います。
まとめ
ActionTextは新しい機能のため中々情報が出てきません。もしかしたらActionTextの設定ファイルか何かでうまいことやれるかもしれないです。
何か直すべき点、間違った記述があればコメント等で指摘していただければと思います。
参考文献
- 投稿日:2019-12-18T21:00:16+09:00
【Rails】使用するレイアウトファイルを切り替える
デフォルトでは全てのviewファイルが
application.html.erb
をレイアウトとして利用するようになっています。
コントローラーやアクションごとに、使用するレイアウトを指定する方法を調べました。レイアウトファイル側から指定
レイアウトファイルの名前を
views/layouts/hoges.html.erb
にすることで、hoges_controller
内の全てのアクションに適用されます。コントローラー側から指定
コントローラーの内上部に
layout '<layoutファイル名の冒頭>'
と書き込むことで、そのコントローラー内のアクションで利用するレイアウトファイルを指定することができます。hoges_controller.rbclass HogesController < ApplicationController layout 'another' def index end . .(レイアウトファイル名が
another.html.erb
の場合)アクションごとに指定
アクション内に
render layout: '<'layoutファイル名の冒頭'>
と書き込むことで、そのアクションで利用するレイアウトファイルを指定することができます。hoges_controller.rbclass HogesController < ApplicationController layout 'another' def index render layout: 'another' end . .(レイアウトファイル名が
another.html.erb
の場合)参考
- 投稿日:2019-12-18T20:51:59+09:00
未経験が受託企業に入って半年経ったので、学んだスキル全部書いてみる
はじめに
今年2019年6月にエンジニアとして晴れてキャリアをスタートさせました。
それから約半年間で本当にいろいろなことを学ばせて頂いたので、私ごとですが簡単に学んだことをまとめます。もちろんマスターなんてしておらず、全てが勉強中なうえ、なんならちょっとかじっただけのものまで書いてますので悪しからず。
一概に未経験といっても、自社開発/受託企業/SES企業、大手/メガベンチャー/スタートアップ、元IT業界/全くの素人、普段からパソコン触ってた/触ってない等いろいろなキャリアの始め方があると思うので、平均的な成長度合いなのかどうかは不明ですが、ほんの一例としてご参考ください。
■簡単な経歴
・良くも悪くもない普通の大卒
パソコンスキルはitunesで音楽入れる程度
・某アパレルチェーン企業で3年ほど勤務
内2年管理職
パソコンスキルはWordで毎週報告書を書く程度
・2018/12〜 Mac購入
某オンラインスクールで勉強開始
・2019/3〜 独学開始、都内もくもく会に週1参加、Menta契約、アプリ作成等
・2019/6〜 就職
・2019/12 現在0ヶ月〜半年で学んだこと
1)プログラミング技術
Ruby/Rails
ほぼ毎日触っていました。
基本的には既存のコードや他PJのコードをマネて書くことが多いですが、必ず意味を理解しながら次へ繋げています。
ただやはりまだまだ分からないことが多く、日々痛感しています。ここでいろいろ書蹴たらいいのですがキリがなさそうなので、また別の記事で書きます。
HTML/CSS
Railsに合わせてHTMLはslimで書いていました。
特にCSSの方は奥深すぎてほんとナメてました。めちゃくちゃ難しいです。
新システム開発時にいかに大勢のユーザーに"初めから"良い印象を持ってもらえるかと考えた時に、画面作成でかなり手こずりました。SQL
本番データをよく触らせて貰える案件だったので、抽出しなければならないことが多くありました。
入社前は全く勉強できてなかったのですが、基礎的なところからサブクエリの書き方、
inner join
left outer join
concat
あたりに触れ、
加えてlimit
をつけずクエリ重すぎて本番が落ちる等も経験できたので、とても良い経験()になりました。DB
上記と同じく、よく触らせて貰えました。
案件JOIN当初、railsdbから大事なレコードをそのまま物理削除してしまい、その直後5分後くらいに先方から怒りの電話がかかってきたのは良い思い出()です。mysqlコマンドやdump、トランザクションの仕組みなど知ることができました。
Git
入社前からもちろん触っていましたが、
git merge
やgit clone
等は正直あまりよく分かっていませんでした。現在でもまだそこまで幅広く扱えてないですが、
それでもgit add
前にgit status
やgit diff
で変更内容をちゃんと確認する習慣付けや、コンフリクト発生時の解消方法、git fetch
git stash
git reset --hard HEAD^
あたりを日常的に使えるようになりました。IDE操作
cloud9で開発していました(これ言うとよく社外の方に珍しいと言われます)。
https://qiita.com/shin1kt/items/03eed49c12104002a2c7
こちらにあるような様々なショートカットを教えて頂き、いち早く見たいファイルを引っ張り出したり、直前の操作を取り消す/戻すなど、格段にスピードが速くなりました(当たり前のことかも知れませんが)。
これを通してMacのショートカットキーを覚えたりすることも多かったです。
いずれはVScodeとか使って開発したいなという思いも密かにあります。テストの書き方
Rubyでminitestを書きました。
入社前はRSpecとともに中途半端な勉強で終わっていたので、実務を通して、調べながらであればなんとか書けるようになったかなと思います。
ただ担当PJのテスト管理が少し甘そうなので、しっかりテストを書いたら実際はどうなるのかな、と気になってはいます。セキュリティ対策
新システム開発の中でセキュリティ対策をしました。
Railsガイド等を参考に、
・総当たり攻撃・辞書攻撃対策
・CSRF対策
・セッションハイジャック対策
・個人情報保護(クライアントやログ)
・SSL化
・エラー文表記修正(「入力されたユーザー名は登録されていません」のような具体的なメッセージにしない等)
・パスワード強化
・リダイレクトとファイル対策
あたりを一通りやったかなと思います(まだまだあると思いますが)。今後はAWS側でのセキュリティ対策もしたいです。
Vim
いわゆるVimmerとは程遠いのですが、
本番サーバー内でdumpファイルを探したり、落ちた原因を探るためコードを直接いじってみたりして、黒い画面に抵抗がなくなる程度にはなりました。
cd
で(地道に)いろんなディレクトリに寄る旅をしたりしたので、普段全く触れないようなディレクトリやファイルがまだまだたくさんあるんだなーというのも実感できました。これで半ば強制的にLinuxの簡単な勉強にも繋がっていきました。
AWS
社内で勉強会を開催し、基礎システムの概要から掴んでみたり、PJごとにシステム構成を見ていったりしました。
ちなみに自分の案件はEC2を使わずLightsailというパッケージを使ってサーバーが構築されていたため少し特殊だったのですが、それも含め良い知見になりました。早いうちにこちらの資格取得まで結びつけたいです。。。
AWSソリューションズアーキテクトアソシエイトステージング/本番環境へのデプロイ
今でもそうですが毎週のようにリリース作業を行なっているので、抵抗がないのが怖いくらいの感覚になりました(おそらく異常なんですよね、これ)。
上記で述べたようにいろんな場面で何度もやらかして先輩方に迷惑をかけまくっているので、
今では ローカルでの確認/ステージング(テスト)環境での確認・打鍵/コマンドのテンプレ化・誤字脱字チェック 等をしっかり行なう習慣が(嫌でも)つきました。新システム作成〜システム構成〜リリース
AWSを触るということも含め、とても良い経験になりました。
DBは既存システムのものを共用したので当初「スキーマファイルはどこ??」「カラムはどうやって追加するの??」「ユーザーのパスワードが入ったかどうかってどこで確認できるの??」などパニックになったり、
まず初めにサーバーを立てようと既存サーバーのスナップショットを取ろうとしたらEC2すら使っておらず出鼻をくじかれたり、
ドメインとサブドメインの違いが分からずしばらくIPアドレスをそのまま打ち込んで画面を開いたり、
今となっては馬鹿らしいですが当時は本気で取り組んでいました。2)顧客対応
タスク/スケジュール管理
タスク量は多くマルチタスクになりがちなので、優先順位を決めたり、話が出たら漏れのないようにすぐメモしたり、不明点はすぐ聞くようにしました。
頻繁なチャットのやりとりも含め、1つのことのなかなか集中できない難しさを痛感しています。
うまくやるコツをぜひ教えて頂きたいです。工数見積もり
このタスクならだいたいこれくらいかかるだろうな、という予想を自分なりにするようにしていました。
それでもまだまだ多く工数を見積もってしまっているらしく、上司と答えあわせをするといつも大体半分くらいに修正されます泣とても難しいです。
見積書/請求書の書き方、渡し方
受託はお客様に納品して初めてお金を頂くので、書類もしっかりしたものを書く必要がありました。
渡す時期やお会いした時の渡すタイミング等にも気をつけたり、PDFでもデータ送信も行なったりしました(どこまでやるかはお客様にもよると思います)。肝心の月単位での工数計算に関しては、、、、まだまだなので、これから勉強です。
お客様を運用に乗せる気遣い/気配り
担当していた案件のお客様が、非IT/多くの部署や担当者がいる/レガシーな問題を抱えるお客様、ということも影響し、そもそも導入を嫌がっていたり、依頼したことを予定通りやってくれなかったりしました。
そのため、システムを入れるメリットや現状との具体的な比較、システムの使い方を実際に見せるなどしてお客様の抵抗を少しでも軽減できるように努めたり、食事をご一緒して少しでも親近感を持って頂けるようにしました。協力的なお客様であれば問題なかったと思うのですが、これはお会いするたびになかなか骨の折れる業務でした。
大人数に向けたシステム説明
結構大きなリリースを控えると、60名ほどのお客様の前で自分がシステムの説明をしました。
人前で説明するのはあまり緊張しない方なので助かりましたが、丁寧に説明できなかったりうまく伝えられなかった点があり、誰でも理解できる目線での話し方と内容にしないといけないと反省しました。
ただこれが、逆にベテランエンジニアだらけだった場合にどうなってたかと考えると、、、それも恐ろしいものです。
3)その他
基本的なPC操作
そもそも自分はPCに疎かったので、現役エンジニアに囲まれながら操作することでいろいろと技を盗めました。
ファイル形式の違いやデータ管理、ショートカットあたりでしょうか。タイピングスピードに関してはまだなかなかにひどいので、今後ブラインドタッチができるように頑張ります。
スプレッドシートの使い方
お客様と共有で使うものを中心に、スプレッドシートの使い方を覚えました。
ワードこそ前職で触っていましたが、エクセルは触ったことがなく今新たに使い方を覚えるのもちょっと億劫だったので、このタイミングでスプレッドシートだけでも使えるようになれて良かったです。
基本何にでも使えるので、やはり便利です。
チーム開発の流れ
Gitを中心に、ブランチをどのように管理するか、チーム全体でのコード管理はどうするか、プルリクをレビュー頂いたらどう修正して再レビュー頂くか、OKだったらどうマージするか、本番にはどのタイミングで反映させるか、ローカルの最新化はどうやるか、など一通り掴めました。
独学では単純に「コミット→プッシュ! コミット→プッシュ!」しかやってなかったので、現場で働かないとこれはなかなか掴めないですね。
分からない点の質問の仕方
初めは詰まったらすぐ質問してしまってましたが、まずは自分でひたすらググりまくって、それでもダメなら一体何がわからないのか質問内容をしっかり文章にまとめてから質問するようにしました。
質問内容をまとめているうちに、問題を俯瞰して見ることができたりググり方を変えたりすることに繋がったり、ググり方のコツを覚えるとだんだんとググり力も上がっていったりしたので、結構効果はあったと思います。
また周辺知識の定着度合いに比例して、「調べればわかりそう」と思うことも増えていきました。半年〜1年でやりたいこと
ここからはほんのメモ書き程度に、自分が今後の半年で学んでいきたいスキルをバーっと書いていきます。
先にこれ勉強すればいいのに、とかありましたら、ご指摘頂けると嬉しいです。フロント書きたい
ベタにvue.jsかreactあたり触れたいです。
、、、、の前にJSの基礎からしっかりやりたいですね。インフラを深く知る
AWSは基礎システムしかまだ触っていないので、よく聞くけどいまいち分かってないものから知見を深めていきたいです。
とりあえずセキュリティあたりが見れるようになると嬉しい。PLでチーム管理
責任を持って開発したり、スケジュール組んだりして、案件全体の流れを把握する経験がしたいです。
振られたタスクを必死でやってく、だけでは見えないところも多々あると思うので。デザイン
新システムをリリースした時に、まざまざとデザイン力のなさを感じました。
Web制作みたいなところの勉強をしたいです。
有名どころのデザインの本から数冊探して勉強してみます。IT基礎
いわゆるコンピューターリテラシーというものを全く知らないのはエンジニアとしてマズいと思うので、基礎の基礎であるITパスポートから基本情報の午前の出題範囲くらいは頭に入れておきたいです。
ググり力
マイナーな開発を調べている時に見る、英語の記事やドキュメントにまだまだ抵抗があります。
Qiita様や先人が書かれたブログ記事には日々お世話になっていますが、そろそろ幅広く読めるようになりたいです。
- 投稿日:2019-12-18T20:20:41+09:00
rails 部分テンプレート健忘禄
- 投稿日:2019-12-18T17:57:57+09:00
Railsチュートリアル 第12章 パスワードの再設定 - 本番環境での動作に関する演習
1.production環境でユーザー登録を試してみましょう。ユーザー登録時に入力したメールアドレスにメールは届きましたか?
まずは「Sign up」画面から、ユーザー登録に必要な情報を入力し、「Create my account」ボタンをクリックします。
上記の画面が出て、入力したメールアドレスが有効ならば、実際にメールが到着しているはずです。確認してみましょう。
確かにメールが到着していました。
2. メールを受信できたら、実際にメールをクリックしてアカウントを有効化してみましょう。また、Heroku上のログを調べてみて、有効化に関するログがどうなっているのか調べてみてください。
ヒント: ターミナルからheroku logsコマンドを実行してみましょう。
まずは、受信したメールの「Activate」リンクをクリックします。
無事ユーザーが有効化されました。
ユーザー有効化処理時のサーバーログ
まずはUsersコントローラーの
new
アクションに関するログです。新規ユーザー登録情報の入力画面が表示されるところまでです。2019-12-17T03:38:26.701082+00:00 heroku[router]: at=info method=GET path="/signup" host=warm-woodland-62915.herokuapp.com request_id=9ef43fd8-9976-4b6c-9793-fde8257190ab fwd="103.5.140.188" dyno=web.1 connect=1ms service=28ms status=200 bytes=3428 protocol=https 2019-12-17T03:38:26.675341+00:00 app[web.1]: I, [2019-12-17T03:38:26.675221 #4] INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab] Started GET "/signup" for 103.5.140.188 at 2019-12-17 03:38:26 +0000 2019-12-17T03:38:26.676239+00:00 app[web.1]: I, [2019-12-17T03:38:26.676157 #4] INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab] Processing by UsersController#new as HTML 2019-12-17T03:38:26.677651+00:00 app[web.1]: I, [2019-12-17T03:38:26.677545 #4] INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab] Rendering users/new.html.erb within layouts/application 2019-12-17T03:38:26.678404+00:00 app[web.1]: I, [2019-12-17T03:38:26.678315 #4] INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab] Rendered shared/_error_messages.html.erb (0.1ms) 2019-12-17T03:38:26.686611+00:00 app[web.1]: I, [2019-12-17T03:38:26.686532 #4] INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab] Rendered users/_form.html.erb (8.5ms) 2019-12-17T03:38:26.686868+00:00 app[web.1]: I, [2019-12-17T03:38:26.686762 #4] INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab] Rendered users/new.html.erb within layouts/application (9.1ms) 2019-12-17T03:38:26.688192+00:00 app[web.1]: I, [2019-12-17T03:38:26.688122 #4] INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab] Rendered layouts/_rails_default.erb (0.9ms) 2019-12-17T03:38:26.688627+00:00 app[web.1]: I, [2019-12-17T03:38:26.688523 #4] INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab] Rendered layouts/_shim.html.erb (0.1ms) 2019-12-17T03:38:26.689527+00:00 app[web.1]: I, [2019-12-17T03:38:26.689418 #4] INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab] Rendered layouts/_header.html.erb (0.6ms) 2019-12-17T03:38:26.690255+00:00 app[web.1]: I, [2019-12-17T03:38:26.690186 #4] INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab] Rendered layouts/_footer.html.erb (0.3ms) 2019-12-17T03:38:26.690776+00:00 app[web.1]: I, [2019-12-17T03:38:26.690656 #4] INFO -- : [9ef43fd8-9976-4b6c-9793-fde8257190ab] Completed 200 OK in 14ms (Views: 13.3ms | ActiveRecord: 0.0ms)続いて、Usersコントローラーの
create
アクションが開始されました。ユーザーの操作としては、「新規ユーザー登録情報の入力画面で、「Submit」ボタンがクリックされたところ」です。2019-12-17T03:39:00.628200+00:00 app[web.1]: I, [2019-12-17T03:39:00.628068 #4] INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] Started POST "/signup" for 103.5.140.188 at 2019-12-17 03:39:00 +0000 2019-12-17T03:39:00.629006+00:00 app[web.1]: I, [2019-12-17T03:39:00.628922 #4] INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] Processing by UsersController#create as HTML 2019-12-17T03:39:00.629110+00:00 app[web.1]: I, [2019-12-17T03:39:00.629041 #4] INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] Parameters: {"utf8"=>"✓", "authenticity_token"=>"kPI+VonM8nuUu11tRyJNCgkw++6CAdlBZgdH7pgF3GwQjj4/Ah6f7x8yXoXNG4AwTYpDgE+jwtNJ13XXCZMh8w==", "user"=>{"name"=>"Hoge Hoge", "email"=>"[有効なメールアドレス]", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Create my account"} 2019-12-17T03:39:00.714970+00:00 app[web.1]: D, [2019-12-17T03:39:00.714832 #4] DEBUG -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] (1.7ms) BEGIN 2019-12-17T03:39:00.718390+00:00 app[web.1]: D, [2019-12-17T03:39:00.718293 #4] DEBUG -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] User Exists (2.1ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2 [["email", "[有効なメールアドレス]"], ["LIMIT", 1]] 2019-12-17T03:39:00.807361+00:00 app[web.1]: D, [2019-12-17T03:39:00.807224 #4] DEBUG -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] SQL (10.7ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "activation_digest") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id" [["name", "Hoge Hoge"], ["email", "[有効なメールアドレス]"], ["created_at", "2019-12-17 03:39:00.718914"], ["updated_at", "2019-12-17 03:39:00.718914"], ["password_digest", "$2a$10$tLq83mKKaJN4XGBafxp0BOMpmTDw6iKYj729bNu9dE6jrGYu7w/o6"], ["activation_digest", "$2a$10$CKnDgcIkfShDxkQLli6OU.6vszmQ3rrXdSMa7khjybONYxxNWxNf2"]] 2019-12-17T03:39:00.811145+00:00 app[web.1]: D, [2019-12-17T03:39:00.811013 #4] DEBUG -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] (3.0ms) COMMIT有効化トークンに対応するダイジェストは以下の文字列ですね。
$2a$10$CKnDgcIkfShDxkQLli6OU.6vszmQ3rrXdSMa7khjybONYxxNWxNf2続いて、メイラーに関するログが返されています。
2019-12-17T03:39:00.817822+00:00 app[web.1]: I, [2019-12-17T03:39:00.817728 #4] INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] Rendering user_mailer/account_activation.html.erb within layouts/mailer 2019-12-17T03:39:00.818892+00:00 app[web.1]: I, [2019-12-17T03:39:00.818826 #4] INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] Rendered user_mailer/account_activation.html.erb within layouts/mailer (0.9ms) 2019-12-17T03:39:00.819846+00:00 app[web.1]: I, [2019-12-17T03:39:00.819782 #4] INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] Rendering user_mailer/account_activation.text.erb within layouts/mailer 2019-12-17T03:39:00.820407+00:00 app[web.1]: I, [2019-12-17T03:39:00.820338 #4] INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] Rendered user_mailer/account_activation.text.erb within layouts/mailer (0.4ms) 2019-12-17T03:39:01.140739+00:00 app[web.1]: D, [2019-12-17T03:39:01.140608 #4] DEBUG -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] UserMailer#account_activation: processed outbound mail in 328.5ms 2019-12-17T03:39:01.383306+00:00 app[web.1]: I, [2019-12-17T03:39:01.383191 #4] INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] Sent mail to [有効なメールアドレス] (242.3ms) 2019-12-17T03:39:01.383340+00:00 app[web.1]: D, [2019-12-17T03:39:01.383285 #4] DEBUG -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] Date: Tue, 17 Dec 2019 03:39:01 +0000続いて、ユーザー有効化URL通知用メールのヘッダーが返されます。
2019-12-17T03:39:01.383343+00:00 app[web.1]: From: noreply@example.com 2019-12-17T03:39:01.383345+00:00 app[web.1]: To: [有効なメールアドレス] 2019-12-17T03:39:01.383347+00:00 app[web.1]: Message-ID: <5df84dd5239a7_42b04df0df05c5129@b042e83a-3492-4249-911e-6b41b5e09cd5.mail> 2019-12-17T03:39:01.383349+00:00 app[web.1]: Subject: Account activation 2019-12-17T03:39:01.383351+00:00 app[web.1]: Mime-Version: 1.0 2019-12-17T03:39:01.383353+00:00 app[web.1]: Content-Type: multipart/alternative; 2019-12-17T03:39:01.383355+00:00 app[web.1]: boundary="--==_mimepart_5df84dd521d3b_42b04df0df05c50d3"; 2019-12-17T03:39:01.383358+00:00 app[web.1]: charset=UTF-8 2019-12-17T03:39:01.383360+00:00 app[web.1]: Content-Transfer-Encoding: 7bit 2019-12-17T03:39:01.383362+00:00 app[web.1]: 2019-12-17T03:39:01.383364+00:00 app[web.1]: 2019-12-17T03:39:01.383366+00:00 app[web.1]: ----==_mimepart_5df84dd521d3b_42b04df0df05c50d3以下のログには、テキストメールの本文が返されています。
2019-12-17T03:39:01.383368+00:00 app[web.1]: Content-Type: text/plain; 2019-12-17T03:39:01.383370+00:00 app[web.1]: charset=UTF-8 2019-12-17T03:39:01.383372+00:00 app[web.1]: Content-Transfer-Encoding: 7bit 2019-12-17T03:39:01.383374+00:00 app[web.1]: 2019-12-17T03:39:01.383376+00:00 app[web.1]: Hi Hoge Hoge, 2019-12-17T03:39:01.383377+00:00 app[web.1]: 2019-12-17T03:39:01.383380+00:00 app[web.1]: Welcome to the Sample App! Click on the link below to activate your account: 2019-12-17T03:39:01.383382+00:00 app[web.1]: 2019-12-17T03:39:01.383384+00:00 app[web.1]: https://warm-woodland-62915.herokuapp.com/account_activations/iNFYhM1X7rDUmVORFrGMbA/edit?email=[有効なメールアドレス] 2019-12-17T03:39:01.383386+00:00 app[web.1]: 2019-12-17T03:39:01.383387+00:00 app[web.1]: 2019-12-17T03:39:01.383390+00:00 app[web.1]: ----==_mimepart_5df84dd521d3b_42b04df0df05c50d3以下のログには、HTMLメールの本文が返されています。
2019-12-17T03:39:01.383392+00:00 app[web.1]: Content-Type: text/html; 2019-12-17T03:39:01.383393+00:00 app[web.1]: charset=UTF-8 2019-12-17T03:39:01.383395+00:00 app[web.1]: Content-Transfer-Encoding: 7bit 2019-12-17T03:39:01.383397+00:00 app[web.1]: 2019-12-17T03:39:01.383402+00:00 app[web.1]: <!DOCTYPE html> 2019-12-17T03:39:01.383404+00:00 app[web.1]: <html> 2019-12-17T03:39:01.383406+00:00 app[web.1]: <head> 2019-12-17T03:39:01.383409+00:00 app[web.1]: <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 2019-12-17T03:39:01.383410+00:00 app[web.1]: <style> 2019-12-17T03:39:01.383412+00:00 app[web.1]: /* Email styles need to be inline */ 2019-12-17T03:39:01.383414+00:00 app[web.1]: </style> 2019-12-17T03:39:01.383416+00:00 app[web.1]: </head> 2019-12-17T03:39:01.383418+00:00 app[web.1]: 2019-12-17T03:39:01.383420+00:00 app[web.1]: <body> 2019-12-17T03:39:01.383422+00:00 app[web.1]: <h1>Sample App</h1> 2019-12-17T03:39:01.383424+00:00 app[web.1]: 2019-12-17T03:39:01.383426+00:00 app[web.1]: <p>Hi Hoge Hoge,</p> 2019-12-17T03:39:01.383428+00:00 app[web.1]: 2019-12-17T03:39:01.383430+00:00 app[web.1]: <p> 2019-12-17T03:39:01.383432+00:00 app[web.1]: Welcome to the Sample App! Click on the link below to activate your account: 2019-12-17T03:39:01.383434+00:00 app[web.1]: </p> 2019-12-17T03:39:01.383435+00:00 app[web.1]: 2019-12-17T03:39:01.383438+00:00 app[web.1]: <a href="https://warm-woodland-62915.herokuapp.com/account_activations/iNFYhM1X7rDUmVORFrGMbA/edit?email=[有効なメールアドレス]">Activate</a> 2019-12-17T03:39:01.383440+00:00 app[web.1]: 2019-12-17T03:39:01.383441+00:00 app[web.1]: </body> 2019-12-17T03:39:01.383443+00:00 app[web.1]: </html> 2019-12-17T03:39:01.383445+00:00 app[web.1]: 2019-12-17T03:39:01.383447+00:00 app[web.1]: ----==_mimepart_5df84dd521d3b_42b04df0df05c50d3--メール本文から、有効化トークンは以下の文字列であることがわかります。
iNFYhM1X7rDUmVORFrGMbA続いてのログにより、Usersコントローラーの
create
アクションの最後は / へのリダイレクトで完了したことがわかります。2019-12-17T03:39:01.383918+00:00 app[web.1]: I, [2019-12-17T03:39:01.383828 #4] INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] Redirected to https://warm-woodland-62915.herokuapp.com/ 2019-12-17T03:39:01.384190+00:00 app[web.1]: I, [2019-12-17T03:39:01.384101 #4] INFO -- : [b0361e09-bbea-4688-bdcd-1f9d317e14fa] Completed 302 Found in 752ms (ActiveRecord: 17.6ms)今度は、AccountActivationsコントローラーに対する
edit
が開始されたところからのログです。「メール本文中に記載されたURLにアクセスがあった」というタイミングですね。2019-12-17T03:48:03.998174+00:00 app[web.1]: I, [2019-12-17T03:48:03.998081 #4] INFO -- : [f2ca36e3-b2d1-4746-ba15-c67b852fbee5] Started GET "/account_activations/iNFYhM1X7rDUmVORFrGMbA/edit?email=[有効なメールアドレス]" for 103.5.140.188 at 2019-12-17 03:48:03 +0000 2019-12-17T03:48:03.999604+00:00 app[web.1]: I, [2019-12-17T03:48:03.999504 #4] INFO -- : [f2ca36e3-b2d1-4746-ba15-c67b852fbee5] Processing by AccountActivationsController#edit as HTML 2019-12-17T03:48:03.999666+00:00 app[web.1]: I, [2019-12-17T03:48:03.999597 #4] INFO -- : [f2ca36e3-b2d1-4746-ba15-c67b852fbee5] Parameters: {"email"=>"[有効なメールアドレス]", "id"=>"iNFYhM1X7rDUmVORFrGMbA"} 2019-12-17T03:48:04.013441+00:00 app[web.1]: D, [2019-12-17T03:48:04.013350 #4] DEBUG -- : [f2ca36e3-b2d1-4746-ba15-c67b852fbee5] User Load (4.0ms) SELECT "users".* FROM "users" WHERE "users"."email" = $1 LIMIT $2 [["email", "[有効なメールアドレス]"], ["LIMIT", 1]] 2019-12-17T03:48:04.101912+00:00 app[web.1]: D, [2019-12-17T03:48:04.101770 #4] DEBUG -- : [f2ca36e3-b2d1-4746-ba15-c67b852fbee5] SQL (12.9ms) UPDATE "users" SET "activated" = 't', "activated_at" = '2019-12-17 03:48:04.087544' WHERE "users"."id" = $1 [["id", 104]] 2019-12-17T03:48:04.102898+00:00 app[web.1]: I, [2019-12-17T03:48:04.102788 #4] INFO -- : [f2ca36e3-b2d1-4746-ba15-c67b852fbee5] Redirected to https://warm-woodland-62915.herokuapp.com/users/104 2019-12-17T03:48:04.103242+00:00 app[web.1]: I, [2019-12-17T03:48:04.103156 #4] INFO -- : [f2ca36e3-b2d1-4746-ba15-c67b852fbee5] Completed 302 Found in 103ms (ActiveRecord: 18.7ms)(再掲)以下のログのSQL文を見るに、ユーザーの有効化処理は正常に完了したようです。
2019-12-17T03:48:04.101912+00:00 app[web.1]: D, [2019-12-17T03:48:04.101770 #4] DEBUG -- : [f2ca36e3-b2d1-4746-ba15-c67b852fbee5] SQL (12.9ms) UPDATE "users" SET "activated" = 't', "activated_at" = '2019-12-17 03:48:04.087544' WHERE "users"."id" = $1 [["id", 104]]3. アカウントを有効化できたら、今度はパスワードの再設定を試してみましょう。正しくパスワードの再設定ができたでしょうか?
まず、ログインフォームから「forgot password」のリンクをクリックします。
パスワード再設定の対象となるユーザーのメールアドレスを入力します。
メールが送られてきます。ハイライトした部分は再設定用トークンです。
リンクをクリックして、パスワード再設定フォームへ進みます。
新しいパスワードを入力します。
パスワードの再設定が完了し、パスワードを再設定したユーザーのプロフィールページが表示されます。
パスワード再設定に関するサーバーログ
まずはPasswordResetsコントローラーの
new
アクションに関するログです。2019-12-17T09:27:37.630786+00:00 app[web.1]: I, [2019-12-17T09:27:37.630687 #4] INFO -- : [4904fcd9-d965-4f6a-93d7-0fa95587d243] Started GET "/password_resets/new" for 175.177.6.7 at 2019-12-17 09:27:37 +0000 2019-12-17T09:27:37.632157+00:00 app[web.1]: I, [2019-12-17T09:27:37.632064 #4] INFO -- : [4904fcd9-d965-4f6a-93d7-0fa95587d243] Processing by PasswordResetsController#new as HTML 2019-12-17T09:27:37.634071+00:00 app[web.1]: I, [2019-12-17T09:27:37.633999 #4] INFO -- : [4904fcd9-d965-4f6a-93d7-0fa95587d243] Rendering password_resets/new.html.erb within layouts/application 2019-12-17T09:27:37.635383+00:00 app[web.1]: I, [2019-12-17T09:27:37.635314 #4] INFO -- : [4904fcd9-d965-4f6a-93d7-0fa95587d243] Rendered password_resets/new.html.erb within layouts/application (1.2ms) 2019-12-17T09:27:37.635971+00:00 app[web.1]: I, [2019-12-17T09:27:37.635907 #4] INFO -- : [4904fcd9-d965-4f6a-93d7-0fa95587d243] Rendered layouts/_rails_default.erb (0.4ms) 2019-12-17T09:27:37.636126+00:00 app[web.1]: I, [2019-12-17T09:27:37.636073 #4] INFO -- : [4904fcd9-d965-4f6a-93d7-0fa95587d243] Rendered layouts/_shim.html.erb (0.0ms) 2019-12-17T09:27:37.636548+00:00 app[web.1]: I, [2019-12-17T09:27:37.636470 #4] INFO -- : [4904fcd9-d965-4f6a-93d7-0fa95587d243] Rendered layouts/_header.html.erb (0.2ms) 2019-12-17T09:27:37.636862+00:00 app[web.1]: I, [2019-12-17T09:27:37.636799 #4] INFO -- : [4904fcd9-d965-4f6a-93d7-0fa95587d243] Rendered layouts/_footer.html.erb (0.1ms) 2019-12-17T09:27:37.637075+00:00 app[web.1]: I, [2019-12-17T09:27:37.637018 #4] INFO -- : [4904fcd9-d965-4f6a-93d7-0fa95587d243] Completed 200 OK in 5ms (Views: 3.7ms | ActiveRecord: 0.0ms)ここまでのログは、「forgot password」リンクのクリックから、パスワード再設定対象のメールアドレスの入力画面がWebブラウザに表示されるところまでに対応しています。
続いて、PasswordResetsコントローラーの
create
アクションが開始されました。ユーザーの操作としては、「パスワード再設定対象のメールアドレスの入力画面で、「Submit」ボタンがクリックされたところ」です。2019-12-17T09:28:10.677268+00:00 app[web.1]: I, [2019-12-17T09:28:10.677144 #4] INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] Started POST "/password_resets" for 175.177.6.7 at 2019-12-17 09:28:10 +0000 2019-12-17T09:28:10.678356+00:00 app[web.1]: I, [2019-12-17T09:28:10.678278 #4] INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] Processing by PasswordResetsController#create as HTML 2019-12-17T09:28:10.678449+00:00 app[web.1]: I, [2019-12-17T09:28:10.678378 #4] INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] Parameters: {"utf8"=>"✓", "authenticity_token"=>"s6/JcwQzuM6JAiREt9u1PDuPaKc5we/TAWuJbaIJyxcyV3rusF2/H0ksby8hky03XDCj8W0j/jxFcmxVT61haQ==", "password_reset"=>"[FILTERED]", "commit"=>"Submit"} 2019-12-17T09:28:10.686514+00:00 app[web.1]: D, [2019-12-17T09:28:10.686418 #4] DEBUG -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] User Load (2.8ms) SELECT "users".* FROM "users" WHERE "users"."email" = $1 LIMIT $2 [["email", "[有効なメールアドレス]"], ["LIMIT", 1]] 2019-12-17T09:28:10.766666+00:00 app[web.1]: D, [2019-12-17T09:28:10.766571 #4] DEBUG -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] SQL (3.7ms) UPDATE "users" SET "reset_digest" = '$2a$10$uOXG8lKmc/1ytMzVC5QHAOiY0S/GjmMYHYMb7lVd.dNK8vIsQFYtG', "reset_sent_at" = '2019-12-17 09:28:10.762063' WHERE "users"."id" = $1 [["id", 104]](再掲)パスワード再設定トークンに対応するダイジェストをRDBに保存する処理には、以下のログが対応しています。
2019-12-17T09:28:10.766666+00:00 app[web.1]: D, [2019-12-17T09:28:10.766571 #4] DEBUG -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] SQL (3.7ms) UPDATE "users" SET "reset_digest" = '$2a$10$uOXG8lKmc/1ytMzVC5QHAOiY0S/GjmMYHYMb7lVd.dNK8vIsQFYtG', "reset_sent_at" = '2019-12-17 09:28:10.762063' WHERE "users"."id" = $1 [["id", 104]]ダイジェストは以下の文字列ですね。
$2a$10$uOXG8lKmc/1ytMzVC5QHAOiY0S/GjmMYHYMb7lVd.dNK8vIsQFYtG続いて、メイラーに関するログが返されています。
2019-12-17T09:28:10.771901+00:00 app[web.1]: I, [2019-12-17T09:28:10.771829 #4] INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] Rendering user_mailer/password_reset.html.erb within layouts/mailer 2019-12-17T09:28:10.772669+00:00 app[web.1]: I, [2019-12-17T09:28:10.772606 #4] INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] Rendered user_mailer/password_reset.html.erb within layouts/mailer (0.7ms) 2019-12-17T09:28:10.773513+00:00 app[web.1]: I, [2019-12-17T09:28:10.773447 #4] INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] Rendering user_mailer/password_reset.text.erb within layouts/mailer 2019-12-17T09:28:10.773958+00:00 app[web.1]: I, [2019-12-17T09:28:10.773897 #4] INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] Rendered user_mailer/password_reset.text.erb within layouts/mailer (0.4ms) 2019-12-17T09:28:10.987227+00:00 app[web.1]: D, [2019-12-17T09:28:10.987125 #4] DEBUG -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] UserMailer#password_reset: processed outbound mail in 219.8ms 2019-12-17T09:28:11.369313+00:00 heroku[router]: at=info method=POST path="/password_resets" host=warm-woodland-62915.herokuapp.com request_id=3885b893-52a2-4eed-bc9d-d14c153f57e1 fwd="175.177.6.7" dyno=web.1 connect=1ms service=692ms status=302 bytes=1049 protocol=https 2019-12-17T09:28:11.363917+00:00 app[web.1]: I, [2019-12-17T09:28:11.363795 #4] INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] Sent mail to [有効なメールアドレス] (376.4ms) 2019-12-17T09:28:11.364042+00:00 app[web.1]: D, [2019-12-17T09:28:11.363974 #4] DEBUG -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] Date: Tue, 17 Dec 2019 09:28:10 +0000続いて、パスワード再設定URL通知用メールのヘッダーが返されます。
2019-12-17T09:28:11.364046+00:00 app[web.1]: From: noreply@example.com 2019-12-17T09:28:11.364048+00:00 app[web.1]: To: [有効なメールアドレス] 2019-12-17T09:28:11.364051+00:00 app[web.1]: Message-ID: <5df89faaf1d68_42b033a8e4c6c36478@0a287fc0-3c85-4948-82ec-158df909485c.mail> 2019-12-17T09:28:11.364053+00:00 app[web.1]: Subject: Password reset 2019-12-17T09:28:11.364055+00:00 app[web.1]: Mime-Version: 1.0 2019-12-17T09:28:11.364057+00:00 app[web.1]: Content-Type: multipart/alternative; 2019-12-17T09:28:11.364059+00:00 app[web.1]: boundary="--==_mimepart_5df89faaf0b60_42b033a8e4c6c36371"; 2019-12-17T09:28:11.364062+00:00 app[web.1]: charset=UTF-8 2019-12-17T09:28:11.364064+00:00 app[web.1]: Content-Transfer-Encoding: 7bit以下のログには、テキストメールの本文が返されています。
2019-12-17T09:28:11.364070+00:00 app[web.1]: ----==_mimepart_5df89faaf0b60_42b033a8e4c6c36371 2019-12-17T09:28:11.364072+00:00 app[web.1]: Content-Type: text/plain; 2019-12-17T09:28:11.364074+00:00 app[web.1]: charset=UTF-8 2019-12-17T09:28:11.364077+00:00 app[web.1]: Content-Transfer-Encoding: 7bit 2019-12-17T09:28:11.364078+00:00 app[web.1]: 2019-12-17T09:28:11.364081+00:00 app[web.1]: To reset your password click the link below: 2019-12-17T09:28:11.364082+00:00 app[web.1]: 2019-12-17T09:28:11.364085+00:00 app[web.1]: https://warm-woodland-62915.herokuapp.com/password_resets/itlJPL32OY-XMHORblVHpQ/edit?email=[有効なメールアドレス] 2019-12-17T09:28:11.364087+00:00 app[web.1]: 2019-12-17T09:28:11.364089+00:00 app[web.1]: This will expire in two hours. 2019-12-17T09:28:11.364091+00:00 app[web.1]: 2019-12-17T09:28:11.364093+00:00 app[web.1]: If you did not request your password to be reset, please ignore this email and 2019-12-17T09:28:11.364095+00:00 app[web.1]: your password stay as it is. 2019-12-17T09:28:11.364097+00:00 app[web.1]: 2019-12-17T09:28:11.364099+00:00 app[web.1]: 2019-12-17T09:28:11.364101+00:00 app[web.1]: ----==_mimepart_5df89faaf0b60_42b033a8e4c6c36371以下のログには、HTMLメールの本文が返されています。
2019-12-17T09:28:11.364103+00:00 app[web.1]: Content-Type: text/html; 2019-12-17T09:28:11.364105+00:00 app[web.1]: charset=UTF-8 2019-12-17T09:28:11.364107+00:00 app[web.1]: Content-Transfer-Encoding: 7bit 2019-12-17T09:28:11.364109+00:00 app[web.1]: 2019-12-17T09:28:11.364110+00:00 app[web.1]: <!DOCTYPE html> 2019-12-17T09:28:11.364112+00:00 app[web.1]: <html> 2019-12-17T09:28:11.364115+00:00 app[web.1]: <head> 2019-12-17T09:28:11.364117+00:00 app[web.1]: <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 2019-12-17T09:28:11.364119+00:00 app[web.1]: <style> 2019-12-17T09:28:11.364121+00:00 app[web.1]: /* Email styles need to be inline */ 2019-12-17T09:28:11.364123+00:00 app[web.1]: </style> 2019-12-17T09:28:11.364125+00:00 app[web.1]: </head> 2019-12-17T09:28:11.364128+00:00 app[web.1]: 2019-12-17T09:28:11.364130+00:00 app[web.1]: <body> 2019-12-17T09:28:11.364132+00:00 app[web.1]: <h1>Password reset</h1> 2019-12-17T09:28:11.364133+00:00 app[web.1]: 2019-12-17T09:28:11.364135+00:00 app[web.1]: <p>To reset your password click the link below:</p> 2019-12-17T09:28:11.364137+00:00 app[web.1]: 2019-12-17T09:28:11.364140+00:00 app[web.1]: https://warm-woodland-62915.herokuapp.com/password_resets/itlJPL32OY-XMHORblVHpQ/edit?email=[有効なメールアドレス] 2019-12-17T09:28:11.364141+00:00 app[web.1]: 2019-12-17T09:28:11.364143+00:00 app[web.1]: <p>This will expire in two hours.</p> 2019-12-17T09:28:11.364145+00:00 app[web.1]: 2019-12-17T09:28:11.364147+00:00 app[web.1]: <p> 2019-12-17T09:28:11.364150+00:00 app[web.1]: If you did not request your password to be reset, please ignore this email and 2019-12-17T09:28:11.364152+00:00 app[web.1]: your password stay as it is. 2019-12-17T09:28:11.364153+00:00 app[web.1]: </p> 2019-12-17T09:28:11.364155+00:00 app[web.1]: 2019-12-17T09:28:11.364157+00:00 app[web.1]: </body> 2019-12-17T09:28:11.364159+00:00 app[web.1]: </html> 2019-12-17T09:28:11.364161+00:00 app[web.1]: 2019-12-17T09:28:11.364164+00:00 app[web.1]: ----==_mimepart_5df89faaf0b60_42b033a8e4c6c36371--メール本文から、パスワード再設定トークンは以下の文字列であることがわかります。
itlJPL32OY-XMHORblVHpQ続いてのログにより、PasswordResetsコントローラーの
create
アクションの最後は / へのリダイレクトで完了したことがわかります。2019-12-17T09:28:11.364629+00:00 app[web.1]: I, [2019-12-17T09:28:11.364565 #4] INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] Redirected to https://warm-woodland-62915.herokuapp.com/ 2019-12-17T09:28:11.364841+00:00 app[web.1]: I, [2019-12-17T09:28:11.364781 #4] INFO -- : [3885b893-52a2-4eed-bc9d-d14c153f57e1] Completed 302 Found in 686ms (ActiveRecord: 6.5ms)今度は、PasswordResetsコントローラーに対する
edit
が開始されたところからのログです。「メール本文中に記載されたURLにアクセスがあった」というタイミングですね。2019-12-17T09:28:40.413354+00:00 heroku[router]: at=info method=GET path="/password_resets/itlJPL32OY-XMHORblVHpQ/edit?email=[有効なメールアドレス]" host=warm-woodland-62915.herokuapp.com request_id=ed1e4c85-a8ad-4e30-90a9-2d581a582d69 fwd="175.177.6.7" dyno=web.1 connect=1ms service=98ms status=200 bytes=3413 protocol=https 2019-12-17T09:28:40.314045+00:00 app[web.1]: I, [2019-12-17T09:28:40.313937 #4] INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69] Started GET "/password_resets/itlJPL32OY-XMHORblVHpQ/edit?email=[有効なメールアドレス]" for 175.177.6.7 at 2019-12-17 09:28:40 +0000 2019-12-17T09:28:40.314979+00:00 app[web.1]: I, [2019-12-17T09:28:40.314889 #4] INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69] Processing by PasswordResetsController#edit as HTML 2019-12-17T09:28:40.315025+00:00 app[web.1]: I, [2019-12-17T09:28:40.314972 #4] INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69] Parameters: {"email"=>"[有効なメールアドレス]", "id"=>"itlJPL32OY-XMHORblVHpQ"} 2019-12-17T09:28:40.319097+00:00 app[web.1]: D, [2019-12-17T09:28:40.319022 #4] DEBUG -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69] User Load (1.7ms) SELECT "users".* FROM "users" WHERE "users"."email" = $1 LIMIT $2 [["email", "[有効なメールアドレス]"], ["LIMIT", 1]] 2019-12-17T09:28:40.398272+00:00 app[web.1]: I, [2019-12-17T09:28:40.398149 #4] INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69] Rendering password_resets/edit.html.erb within layouts/application 2019-12-17T09:28:40.403019+00:00 app[web.1]: I, [2019-12-17T09:28:40.402925 #4] INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69] Rendered shared/_error_messages.html.erb (0.7ms) 2019-12-17T09:28:40.405414+00:00 app[web.1]: I, [2019-12-17T09:28:40.405331 #4] INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69] Rendered password_resets/edit.html.erb within layouts/application (7.0ms) 2019-12-17T09:28:40.406458+00:00 app[web.1]: I, [2019-12-17T09:28:40.406378 #4] INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69] Rendered layouts/_rails_default.erb (0.7ms) 2019-12-17T09:28:40.406920+00:00 app[web.1]: I, [2019-12-17T09:28:40.406841 #4] INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69] Rendered layouts/_shim.html.erb (0.0ms) 2019-12-17T09:28:40.407613+00:00 app[web.1]: I, [2019-12-17T09:28:40.407535 #4] INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69] Rendered layouts/_header.html.erb (0.4ms) 2019-12-17T09:28:40.408136+00:00 app[web.1]: I, [2019-12-17T09:28:40.408058 #4] INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69] Rendered layouts/_footer.html.erb (0.2ms) 2019-12-17T09:28:40.408613+00:00 app[web.1]: I, [2019-12-17T09:28:40.408534 #4] INFO -- : [ed1e4c85-a8ad-4e30-90a9-2d581a582d69] Completed 200 OK in 93ms (Views: 10.7ms | ActiveRecord: 1.7ms)ここまでのログは、パスワード再設定画面がWebブラウザに表示されるところまでに対応しています。
続いて、PasswordResetsコントローラーの
update
アクションが開始されました。ユーザーの操作としては、「パスワード再設定画面で、「Submit」ボタンがクリックされたところ」です。2019-12-17T09:29:12.595676+00:00 app[web.1]: I, [2019-12-17T09:29:12.595538 #4] INFO -- : [8848c122-5082-4846-86ce-2ceb8b852cbe] Started PATCH "/password_resets/itlJPL32OY-XMHORblVHpQ" for 175.177.6.7 at 2019-12-17 09:29:12 +0000 2019-12-17T09:29:12.596922+00:00 app[web.1]: I, [2019-12-17T09:29:12.596829 #4] INFO -- : [8848c122-5082-4846-86ce-2ceb8b852cbe] Processing by PasswordResetsController#update as HTML 2019-12-17T09:29:12.597042+00:00 app[web.1]: I, [2019-12-17T09:29:12.596951 #4] INFO -- : [8848c122-5082-4846-86ce-2ceb8b852cbe] Parameters: {"utf8"=>"✓", "authenticity_token"=>"dg4e+oRJwi5xsE/Jz4Pmq+2UoA2OiDh5+ocmuTpFoVb2ch6TD5uvuvo5TCFFuiuRqS4YY0MqI+vVVxSAq9NcyQ==", "email"=>"[有効なメールアドレス]]", "user"=>{"password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Update password", "id"=>"itlJPL32OY-XMHORblVHpQ"} 2019-12-17T09:29:12.601036+00:00 app[web.1]: D, [2019-12-17T09:29:12.600952 #4] DEBUG -- : [8848c122-5082-4846-86ce-2ceb8b852cbe] User Load (1.6ms) SELECT "users".* FROM "users" WHERE "users"."email" = $1 LIMIT $2 [["email", "[有効なメールアドレス]"], ["LIMIT", 1]] 2019-12-17T09:29:12.677061+00:00 app[web.1]: D, [2019-12-17T09:29:12.676951 #4] DEBUG -- : [8848c122-5082-4846-86ce-2ceb8b852cbe] (1.4ms) BEGIN 2019-12-17T09:29:12.756980+00:00 app[web.1]: D, [2019-12-17T09:29:12.756856 #4] DEBUG -- : [8848c122-5082-4846-86ce-2ceb8b852cbe] User Exists (1.6ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) AND ("users"."id" != $2) LIMIT $3 [["email", "[有効なメールアドレス]"], ["id", 104], ["LIMIT", 1]] 2019-12-17T09:29:12.759699+00:00 app[web.1]: D, [2019-12-17T09:29:12.759627 #4] DEBUG -- : [8848c122-5082-4846-86ce-2ceb8b852cbe] SQL (1.4ms) UPDATE "users" SET "password_digest" = $1, "updated_at" = $2 WHERE "users"."id" = $3 [["password_digest", "$2a$10$H0x3ghcOyu0l4DHat9NqLOsJ0.xRYZ.DDKTXvoaO18Ot7E5p/omzq"], ["updated_at", "2019-12-17 09:29:12.757425"], ["id", 104]] 2019-12-17T09:29:12.762463+00:00 app[web.1]: D, [2019-12-17T09:29:12.762374 #4] DEBUG -- : [8848c122-5082-4846-86ce-2ceb8b852cbe] (2.4ms) COMMIT 2019-12-17T09:29:12.763828+00:00 app[web.1]: D, [2019-12-17T09:29:12.763760 #4] DEBUG -- : [8848c122-5082-4846-86ce-2ceb8b852cbe] (1.1ms) BEGIN 2019-12-17T09:29:12.765871+00:00 app[web.1]: D, [2019-12-17T09:29:12.765779 #4] DEBUG -- : [8848c122-5082-4846-86ce-2ceb8b852cbe] SQL (1.3ms) UPDATE "users" SET "reset_digest" = $1, "updated_at" = $2 WHERE "users"."id" = $3 [["reset_digest", nil], ["updated_at", "2019-12-17 09:29:12.763908"], ["id", 104]] 2019-12-17T09:29:12.768224+00:00 app[web.1]: D, [2019-12-17T09:29:12.768124 #4] DEBUG -- : [8848c122-5082-4846-86ce-2ceb8b852cbe] (2.1ms) COMMIT 2019-12-17T09:29:12.768664+00:00 app[web.1]: I, [2019-12-17T09:29:12.768603 #4] INFO -- : [8848c122-5082-4846-86ce-2ceb8b852cbe] Redirected to https://warm-woodland-62915.herokuapp.com/users/104 2019-12-17T09:29:12.768800+00:00 app[web.1]: I, [2019-12-17T09:29:12.768749 #4] INFO -- : [8848c122-5082-4846-86ce-2ceb8b852cbe] Completed 302 Found in 172ms (ActiveRecord: 15.9ms)パスワードの再設定が正常に完了していますね。最後は当該ユーザーのプロフィールページにリダイレクトされています。
- 投稿日:2019-12-18T17:41:23+09:00
slim の書き方が合ってるかどうか、すぐ試したい
問題(やりたいこと)
slim のレビューをしていて、こんな書き方ができたような…ということを言いたいが自信がない。
とりあえず一行だけ slim を書いて、その書き方が合っているか確かめるにはどうするか。解決策
ターミナルで下記のコマンドを実行する。これで標準入力の待ち状態に入るので適当な slim を書き込み Control+d を2回押す。(なぜ2回なのかはわからない)
bundle exec slimrb
例
たとえば
bundle exec slimrb div hoge
のあと Control+d を2回押すと
<div>hoge</div>が出力されて、正しい slim だったということがわかる。
- 投稿日:2019-12-18T17:41:23+09:00
slim ちょっと試したいときには slimrb を使う
問題(やりたいこと)
slim のレビューをしていて、こんな書き方ができたような…ということを言いたいが自信がない。
とりあえず一行だけ slim を書いて、その書き方が合っているか確かめるにはどうするか。解決策
ターミナルで下記のコマンドを実行する。これで標準入力の待ち状態に入るので適当な slim を書き込み Control+d を2回押す。(なぜ2回なのかはわからない)
bundle exec slimrb
例
たとえば
bundle exec slimrb div hoge
のあと Control+d を2回押すと
<div>hoge</div>が出力されて、正しい slim だったということがわかる。
- 投稿日:2019-12-18T17:19:23+09:00
xray-railsはRails6だと動かないという話
TL;DR
xray-rails
gemを使っている方がRails6にアップグレードするときは要注意。事の流れ
発端
プロジェクトをRails6に上げたところ、なぜか
image_tag
が動作しなくなりました。具体的には、app/assets/images/exist.svg
みたいなファイルがあるのに、image_tag 'exist.svg'
がエラーを吐くようになりました。調査
試しに新規にRails6アプリケーションを作成し、同じファイルをコピーして
image_tag
を使用したところ問題なく動作しました。この時点で該当アプリケーションと新規アプリケーションの差分が原因(ファイルや記法の問題ではない)ということはわかったものの、設定が問題だと思いこんでいたためどこが真の原因かを突き止めることができませんでした。ruby-jp
自力での調査を続けてもよかったのですが、正直疲れていた私はオンラインで質問することにしました。
幸い、ruby-jp
というSlackコミュニティがあり、そこにはrails
チャンネルがあるので私はそちらに質問を投下しました。undefined method 'pathname' for #<#Class:0x00007f840557dad8:0x00007f84088a6018>というエラーが出て困っています。
既存のRails 5.2のアプリをRails 6に上げたら出るようになりました。
image_tagの箇所で出ており、 app/assets/images/hoge.svgが存在するときに image_tag 'hoge.svgを呼ぶと上記エラーとなります。
アセットパイプラインがおかしいとは思うのですが、正常動作しているRails 6のアプリと比べても違いがわからずに困っています。
同じような経験のある方はいらっしゃいますでしょうか…するとなんと8分後に回答が!
xray-rails入ってます?
はっとなりました。
Gemfile
にxray-rails
のエントリがあることは知っていたため「あります」と回答するとURLが。https://github.com/brentd/xray-rails/pull/103
なるほど…
解決
xray-rails
をGemfile
から除外するとエラーは出なくなりました、めでたしめでたし。
(master
ブランチを使うという手もありましたが、もう使っていないということだったので除外する方法を採りました)まとめ
というわけで、
xray-rails
が原因であることがruby-jp
のおかげでわかりました、という話でした。回答してくださったbluerabbit
さんをはじめとした方々、ありがとうございました!
- 投稿日:2019-12-18T16:52:37+09:00
【Rails】ITインフラ全体像をまとめてみた(DNSサーバーへの問合せ、リクエストからレスポンスの流れ)
目的
RailsチュートリアルではMVCといったRailsアプリケーション特有の全体像については書かれていますが、それ以前の一般的なシステムのインフラについてはほぼ載っていないのが現状です。今回はそうした一般的なシステム全体像を含めて解説して、Railsの全体像について理解を深めていきたいと思います。
流れ
①WebブラウザでURLをアクセスする時DNSサーバーを使ってドメインIPアドレスの取得作業を行う。
②DNSサーバーはIP アドレスを返す。
③ブラウザはサーバのIPアドレスにHTTPリクエストを行う。
④Webサーバーは静的コンテンツを見せて、動的コンテンツについてはAPサーバーに要求する。
⑤APサーバーがRailsで作成されたアプリケーションを実行して、ルーター(Route.rb)がリクエストをコントローラに振り分ける。
⑥コントローラーが動的なコンテンツについてはModelに行きデータベースとの通信を行い、必要な情報をデータベースから取り出し、コントローラーに返す。コントローラーがその情報を保存し、ビューに通信を行いHTMLを生成する。
⑦コントローラは、ビューで生成されたHTMLを受け取り、ブラウザに返す。(HTTPレスポンス)
まとめ
RailsについてRailsチュートリアル等でMVCの流れなどはよく載っていますが、それ以前の流れについては詳しく載っているものは少ないと思います。他のRailsエンジニアの方の意見を聞きながらまとめたものの、自信がないので間違っている部分などございましたら、お知らせください。
- 投稿日:2019-12-18T16:07:59+09:00
ActiveRecordのメソッドとSQLの関係を基礎から理解しよう!
目標
- RailsのModelの中核を担うActiveRecordのメソッドと発行されるSQLの関係を理解すること
- includesやpluckといったメソッドを理解することで中級者レベルにステップアップすること
目次
- 用語
- 基本編
- 基本編解説
- 応用編
- 応用編解説
- まとめ
- さらに発展的な内容
用語
RDBMS
Relational Data Base Management Systemの略。その名の通りリレーショナルデータベースを管理するソフトウェアの総称で、データをカラム(列)と、レコード(行)の中にまとめ、それらを二次元のテーブル(表)の中に並べて表現するものです。データの取得や作成といった手続きにSQL言語が用いられます。代表的なものにはPostgresqlやMysqlなどがあります。
ORM
ORMとはオブジェクトリレーショナルマッピングの略で、Rubyなどのオブジェクト指向言語では直接扱うことのできないRDBMS上のテーブルやデータを、クラスなどにマッピングすることで、扱えるようにするプログラミングの技法のこと。
ActiveRecord
Railsで用いられているActiveRecordとは、MVCのMつまりModelの中核を担う階層で、主にデータベースとのやり取りを行います。これは、ORMシステムに記述されている「Active Recordパターン」を実装したライブラリであり、このパターンと同じ名前が付けられています。 Railsのモデルとして作成されたクラスにはApplicationRecordクラスがもれなく継承されていますが、これはActiveRecord::Baseを継承したものなので、Railsで生成される全てのクラスでこのActiveRecordのメソッドを使用することができるというわけです。
準備
以下に用いる例では、UserモデルとArticleモデルが出てきます。それぞれ以下のようなカラムを持っているものとします。
Userモデル
カラム 型 id integer name string age integer Articleモデル
カラム 型 id integer user_id integer title string body string コンソールやRDBMSににアクセスして直接実行して確認したい方は、以下のリンクで解説と同じ環境を用意する手順を示しているので参考にしてみてください。
https://qiita.com/ttexan/private/63c9f07cd1a33af3d18d
1.ActiveRecordのメソッドと発行されるSQL文(基礎編)
ActiveRecordのメソッドとそれに対応してデータ取得のために発行されるSQLの様子は以下のようになります。(SQL文はrails console上に出てくるものと少し異なりますが、実際のRDBMSで実行できる形で示しています。)
# ① User.all # SELECT * FROM users; # ② User.find(1) # SELECT * FROM users WHERE id = 1 LIMIT 1; # ③ User.find_by(name: "太郎") # SELECT * FROM users WHERE name = '太郎' LIMIT 1; # ④ User.create(name: 'John', age: 20) # INSERT INTO users (name, age) values ('John', 20); # ⑤ User.first.update(name: "Mathias") # SELECT * FROM users ORDER BY id ASC LIMIT 1; # UPDATE users SET name = 'Mathias' updated_at = NOW() WHERE id = 1;基礎編解説
①all
SELECT * FROM users;
コントローラーのindexでよく実行されるメソッドです。ご存知の通り、allは、全てのユーザーを取得するというActiveRecordのメソッドです。*
はワイルドーカードで、全てという意味です。つまり、このSQLは「usersテーブルからuserの全てのカラムを取得する」という意味になります。取得されたデータはUserクラス(の配列)にマッピングされ、繰り返し処理や、カラムの値を表示する際には、Rubyのメソッドとして実行できるようになるという仕組みが提供されます。②find
SELECT * FROM users WHERE id = 1 LIMIT 1;
findメソッドを実行すると途中まではallと一緒で最後にWHERE id = 1 LIMIT 1
というのがつきます。このSQLは「usersテーブルからidが1のuserの一つ目のデータの全てのカラムを取得する」という意味です。allに条件がついただけです。③find_by
SELECT * FROM users WHERE name = '太郎' LIMIT 1;
idで条件指定をしてユーザーを取得する際には、単純に1を指定すればよしなにやってくれましたが、それ以外のカラムで条件指定をしたい場合にはfind_byメソッドを用いてカラムと条件の指定を同時にします。
しかし、発行されたSQLを見るとWHERE id = 1
の部分がWHERE name = '太郎'
に変わっただけですね。ActiveRecordに限らず、このようにid検索の場合には簡略化されたメソッドを提供しているORMシステムは多く存在します。④create
INSERT INTO users (name, age) values ('John', 20);
カラムと挿入したい値を指定し、このように書きます。sqlではデータを列に挿入するという感覚なので、insertという句が用いられているのだ思います。また、''
と""
の使い分けは厳密で、文字列を使用する場合には''
でくくる必要があります。⑤update
SELECT * FROM users ORDER BY id ASC LIMIT 1;
UPDATE users SET name = 'Mathias' updated_at = NOW() WHERE id = 1;
updateとではまず、更新すべきデータの列を取得して、それを書き換えるという動作を行うため、二つのSQL文が発行されます。ここで用いられているfirstメソッドは、データベース上でidがもっとも小さいもののデータを取得するメソッドですが、SQLではその条件を「データをidの昇順に並べてその最初のデータを取ってくる」という指定の仕方をします。また、udpateのやり方は見ての通りですが、insertとは少しカラムと値の指定の仕方が異なるのです。2.ActiveRecordのメソッドと発行されるSQL(応用編)
# ① Article.pluck(:title) # SELECT title FROM articles; # ② User.find(1).articles # SELECT * FROM users WHERE id = 1 LIMIT 1; # SELECT * FROM articles WHERE user_id = 1; # ③ Article.all.includes(:user) # SELECT * FROM articles; # SELECT * FROM users WHERE id in (1,2,3...);応用編解説
①pluck
今までは、
*
で指定した列の全てのデータを取得していましたが、pluck
メソッドを使うことによってカラムを指定してデータを取得することが可能になります。取得するデータの規模が大きくなるほど実行速度は遅くなるので、適宜このようなメソッドを使用することがアプリケーションの性能改善に役立ちます。②子モデルのAssociation
なんのことはない、ユーザーを見つけてそのユーザーのidを持ったarticleを取得しているだけです。しかし、それをrubyで簡単にかけるようなインターフェースを実装してくれているので、我々は非常に便利に使うことができるというわけです。
③includes
記事を全て取得してその記事のuser_idが含まれるユーザーを取得しているだけで、アソシエーションによる関連モデルの取得とどう違うのかと思われるかもしれません。しかし、この場合はあくまで
@articles
にはarticleモデルの配列が入っています。これは例えばhtmlで<% @articles.each do |a| %> <%= a.title %> <%= a.user.name %> <% end %>のように表示をしたい場合に必要となります。このテンプレートを表示するには、コントローラで
@articles = Article.all
と指定すれば問題なく表示されるのですが、実際には3行目のa.user.name
が呼ばれるたびに、裏でSELECT * FROM users WHERE id = (a.user_id)
というSQLが毎回発行されてしまい動作が非常に重たくなってしまうのです。これは俗にN+1問題と言われ、articlesを取得(1回)+articlesの数(=N回)分userを取得するSQLが発行されるため、そのように呼ばれています。それを解決するメソッドが
includes
で、あらかじめ必要となるテーブルのデータを取得しておき、SQLの発行を2回で済ませます。まとめ
ActiveRecordやデータベース、そしてその関係について説明しました。Railsでの開発はそもそもアプリケーションとデータベースが別れているということすら意識せずに使用できるため便利な反面、ちょっとしたエラーが起きるととたんに理解の浅さが浮き彫りになってしまうことがあります。これを機会にぜひ勉強してみてください。
発展的な内容
- join, preload, eage_loadなどのさらに発展的な内容も理解したい方向け
https://qiita.com/k0kubun/items/80c5a5494f53bb88dc58
- データベースや設計を学びたい方向け
- 投稿日:2019-12-18T15:18:06+09:00
一人で新規Webサービスを作ろうとしたら、Amplifyにたどり着いた
こんにちは。
フリーランスエンジニアのtelumoです。はじめに
この記事では、
- Amplifyを使うまでの経緯
- 実際に使ってみた所感
について書きたいと思います。
具体的なAmplifyの使い方は他の記事や公式に任せたいと思います。
この記事では、私の目線でAmplifyを語っていますので、この記事を読めば体系的にAmplifyが理解できるとかは期待しないでください。。。(すみません)Amplifyがバックエンドの選択肢の一つになる、そんなきっかけになれば幸いです。
Amplifyを使うまでの経緯
前提
今年、友人の紹介である建設会社でWebサービスを開発する依頼を受けました。
そのWebサービスは求人サイトで、サービスの特徴としては、地図から求人が探せるというものです。
大島てるの求人サイト版のようなイメージです。得意なフレームワークでWebアプリを構築
はじめ私はDjangoでアプリを構築していました。
(一人プロジェクトだったので技術は自由に選べました)Djangoは、
- モデルを記述するだけでDBマイグレーションをやってくれる
- ユーザーのクラスを継承するだけで複数ロールのユーザーを作成して、それぞれに認可を与えられる
- テンプレートエンジンを利用してフロントが比較的楽にかける
といった特徴があります。
これらは、Djangoの特権でもなんでもなく、他のフルスタックフレームワークにも存在する便利機能です。私はPythonが得意で、DjangoでのWebアプリ構築経験があったので、
「まずはDjangoで」
的なノリでアプリを構築しました。プロトタイプが完成
地図を使ったアプリは作ったことがなかったので、最初の段階では実際にアプリを作りながら技術検証をしていました。
インフラ(AWSを利用)はpulumiでさくっと実装しました。余談ですが、pulumiのawsxを利用すると、AWSのベストプラクティスに則ったインフラが数行のコードで書けるのでおすすめです。
そして、最初のプロトタイプが完成しました。
DjangoでWebアプリ自体は比較的に簡単に作れたのですが、なんか遅いんです。
地図をドラッグしたあとのアイドル状態時にデータベースからデータを取得して地図に表示する仕組みなのですが、
なんか遅い。調べてみると、
- WebサーバーがDBサーバーからデータを取得するのに時間がかかっている
- ブラウザが取得するデータの通信量が大きい
というダブルパンチで遅くなっていることがわかりました。
まず1に関しては、WebサーバーとDBの最適化が必要だなと感じました。
プロトタイプなので、どちらも小さなスペックのものを利用していたので遅いのは当然なのですが、新規サービスでどれだけのスペックのものを用意すればいいのかが測りづらい、、、
当然ですが、サービスが利用されなくてもサーバー台はかかる。。。そもそも、一人プロジェクトでインフラに時間をかけたくないな。。。
みたいなことを考えていました。
スペックの問題以外にも、クエリが最適化されているかという点も自信がありませんでした。次に2に関してですが、APIのレスポンスの実装がおかしいことがわかりました。
テストデータが数百件のうちは、いいんですが、数万件になった時に、取得するデータがたくさんあると、(当たり前ですが)表示も遅くなります。
だからAPIのレスポンスのパラメーターを適切に調節してあげればいいだけです。
しかし、それと同時に
もしかして、APIの修正が入ったらいちいちAPI側とブラウザ側書き直さなきゃいけない!?
みたいなことを考えていました。WebサーバーをLambdaにする
これを解決するために、色々と試行錯誤しました。
後から考えると、この試行錯誤している時の私の失敗は、
Djangoメインで考えていたことです。
そもそも、Webアプリケーションを作るとなったらWebサーバー + RDBという構成は一般的で、
Djangoでなくても、そういう構成で利用できるフレームワークをまず考えてしまう人は多いと(勝手に)予想しています。試行錯誤のうちの一つは、WebサーバーをLambdaに置き換えるというものです。
「Webサーバーがサーバーだからスペックやコストを気にする必要があるんだ。だったらサーバーレスにすればいいじゃない。」
という単純な思考です。LambdaにDjangoを乗せるには、zappaを使えば簡単ですね。
ただ、ここで有名な問題にぶち当たります。
それは、「Lambda + RDS相性良くない問題」です。ただ、Aurora ServerlessのData APIを利用すれば、その問題を解決できると思ってました。
確かにそれでLambdaからRDSは問題なく利用できます。
しかし、それだとDjangoの良さを殺すことになります。何せ、Data APIを呼び出すためのコードを別に書く必要があるのですから。それに、「Webサーバー + RDB」構成において、RDB(RDS)が占めるコストは少なくありません。
新規サービスでリクエスト数もわからないのに常時稼働のRDBを保持するのは賢明ではないと思いました。他にも、コンテナサービスであるFargateやk8sを考えましたが、学習コストが高いのと、RDBの問題で二の足を踏んでました。
Amplifyを思い出す
そんなこんなしている時にふとAmplifyを思い出しました。
AWS Dev Day 2018の「Dev Day Challenge」に参加した際に、チャットアプリを作りAmplifyを使ったのです。Dav Dayのときは、開発時間もあっという間だったので詳しく理解することはできませんでしたが、
「そういえば、AWSソリューションアーキテクトの人がAmplifyをめちゃくちゃ推してたな」
という印象が強く残っていたので、調べてみることにしました。すると、以下の点で今回の案件にぴったりなバックエンドだと感じました。
- サーバーレスでリクエスト課金なので、リクエスト数に最適なインフラを構築する必要がない(そもそもBaaSなのでインフラを意識する必要ない)
- GraphQLがデフォルトで使え、必要なデータをフロントが選択できる(APIを実装する必要がない)
- ユーザーの種類に応じて権限を細かく設定できる(Cognitoとの連携がやりやすい)
ということで、色々ありましたがAmplifyを使ってみることにしました。
Amplifyを使うということは、Djangoはもう必要がないということです。フロントはNuxt.jsでやりました。
地図の実装が重要なアプリケーションなので、HTMLを返Djangoのようなフレームワークは合っていなかったのかなと思います。Amplifyを使ってみた所感
Amplifyたどり着くまでの話が長くなりましたが、ここからは実際に使ってみた感じたことを書きます。
結論を先に言うと、Amplify最高でした!
これからWebアプリケーションを作る際に、まず間違いなく有力な選択肢の一つになると思います。そういえば、Amplify自体の説明をしていませんね。
Amplifyは、AWSのBaaSです。フロントからバックエンドを簡単に構築することができます。
詳しいことは、公式や記事等を参考にしてください。では、所感を書いていきます。
scheme.graphql
が、データモデルのドキュメントになるAmplifyでは、バックエンドとしてGraphQLが簡単に利用できます。
データストアはDynamoDBなのですが、GraphQLを挟むことで普通のCRUDや、チャットのようなサブスクリプションが必要なアプリが楽に構築することができます。GraphQLを利用するために、
scheme.graphql
というファイルを書きます。
逆に言うと、データストア周りで書く必要があるのはscheme.graphql
だけです。これが意味するのは、
scheme.graphql
自体が実質的にデータモデルのドキュメントになるということです。
これは非常に楽です。このscheme.graphql
ファイルを見るだけで、
- どんなテーブルが存在するのか
- それぞれのテーブルにどんなカラムが存在するのか
- 各カラムの型
- テーブルへのアクセス権限まわり
が一目瞭然なのです。
これをみてください。
type Salary @model @auth(rules: [{allow: groups, groups: ["Admin"]}]) { id: ID! wage: Int currency: String }上記は、Amplifyの公式に書かれた例です。
GraphQLを触ったことがない人でも少なくとも以下のことが読み取れるのではないかと思います。
- Salaryモデルは、
ID
型のid
、Int
型のwage
、String
型のcurrency
という3つのフィールドから成り立っていること。Admin
グループにのみ何かしらの権限が与えられていること。
@model
ディレクティブで実際にDynamoDBにテーブルを作ってくれるとか、
Adminグループに与えられる権限はCRUD全ての権限であるとか、
ID
型のid
はAWSが自動生成してくれるとか、、、
そう言ったことはドキュメントを読んで一つ一つ理解していく必要がありますが、このようなモデルをScheme.graphql
に記述していくだけで、テーブルが簡単に作れます。しかも、(Cognitoと連携して)どんなユーザーにどんな権限が許可されているのかもすぐにわかります。さらにAmplifyはこの
Scheme.graphql
を読み取って自動でquery、mutation、subscriptionのJavaScript(or TypeScript)のスクリプトを自動生成してくれます!
素晴らしい。Djangoやその他のフレームワークでも同じようなことができるとは思います。
Djangoの場合は、モデルのクラスを実装すればいいんです。
しかしアプリごとに別々のファイルに書く必要がありますし、所詮スクリプトなので定義的ではありません。余談ですが、宣言的なコードを見ればドキュメントを書く必要がない(少なくとも見る必要がない)と言うのはエンジニアの理想です。
関数型言語が流行ったのもそういった背景があるからだと思います。AmplifyはRDBユーザーでも利用しやすい
AmplifyのデータストアであるDynamoDBはいわゆる「NoSQL」です。そのため、RDBを主に利用する人にとっては理解しにくいところです。
しかし、AmplifyはRDBを利用する人が理解しやすいと感じます。
RDBでは、「1対多」や「多対多」の関係を利用してテーブルを作成していきますが、同様のことがScheme.graphql
にも記述できます。
(そもそも、AWSのGraphQLサービスであるAppSyncはバックエンドにRDSも利用できるので当たり前かもしれません。)type Post @model { id: ID! title: String! comments: [Comment] @connection(keyName: "byPost", fields: ["id"]) } type Comment @model @key(name: "byPost", fields: ["postID", "content"]) { id: ID! postID: ID! content: String! post: Post @connection(fields: ["postID"]) }上記は、
@connection
ディレクティブを利用した1対多の関係を表したテーブルのサンプルコードです。
Postテーブルが複数のComment([Comment]
のように配列で表している)を持っていることが一目瞭然です。
Post側にもComment側にも@connection
を記述することでどちらからでももう一方を取得することができます。そして取得する時には、以下のようなGraphQLのクエリを発行すればいいのです。
query GetCommentWithPostAndComments { getComment( id: "a-comment-id-1" ) { id content post { id title comments { items { id content } } } } }GraphQLはフロントで取得したいカラムを選択できます。
さらにAmplifyはこのquery自体も自動で生成してくれます!
(もちろん自分でカスタマイズしたクエリを記述して利用することも可能です。)多対多のモデルは、間にテーブルを一つ挟めば実現できます。(RDBと同じですね。)
type Post @model { id: ID! title: String! editors: [PostEditor] @connection(keyName: "byPost", fields: ["id"]) } type PostEditor @model(queries: null) @key(name: "byPost", fields: ["postID", "editorID"]) @key(name: "byEditor", fields: ["editorID", "postID"]) { id: ID! postID: ID! editorID: ID! post: Post! @connection(fields: ["postID"]) editor: User! @connection(fields: ["editorID"]) } type User @model { id: ID! username: String! posts: [PostEditor] @connection(keyName: "byEditor", fields: ["id"]) }CI/CDもAmplify1つで。
複数環境をAmplify CLIで構築、デプロイできることは当たり前なのですが、Amplifyコンソールを利用すれば、GitHubなどのGitレポジトリと連携して環境毎のバックエンドを自動で選択してくれます。
環境 Gitレポジトリのブランチ バックエンド(Amplify) 本番環境 master prod 開発環境 dev dev 上記のように、本番環境と開発環境があって、Gitレポジトリのブランチがそれぞれmaster、devと分かれているとします。
そして、本番環境はバックエンドのprod環境を、開発環境はdev環境を利用したい。
よくありますよね。それ、Amplifyコンソールで環境変数を設定しておけば、Amplifyが自動でやってくれます。
つまり、masterブランチに変更(プルリクからのマージ等が行われるなど)があった場合は、prodのバックエンドが利用され、
devブランチに変更があった場合は、devバックエンドが利用されます!
(もちろんバックエンドの環境は、本番、開発だけでなく自由に追加することができます。)これは便利ですよね。
フロント側はバックエンドの指定等一切することがなく、Amplifyがやってくれるのです。また、gitのブランチの変更を感知してデプロイされるタイミングでテストも自動でやってくれます。
現在はE2EのテストフレームワークであるCypressのみ利用できます。おわりに
ざっくりとAmplifyを利用するに至った経緯と所感を私の視点ではありますが、書いてみました。
書き忘れましたが、最初の問題であった「レスポンスの遅さ」はAmplifyを利用することで解決していました。
もちろんサーバーを使っても、解決していたと思います。
しかし、課金コストの問題や一人でインフラを管理する時間的コストを考えると、Amplifyにして正解だと思います。余談ですが、BaaSといえばFirebaseも有名ですが、
- GraphQLが楽に使える
- テーブルへのアクセス権限が同じファイルに楽にかけて管理しやすい
- 環境を自由に分けられる
そんなAmplifyの方が私は好きです。
- 投稿日:2019-12-18T14:31:45+09:00
マイグレーションファイルのdef upとdef self.upの違いについて調べてみた
背景
社内のマイグレーションファイルにて、
def up
とdef self.up
が混在していたのでどちらかに統一すべく(そもそも統一すべきかどうかも含めて)、違いを調べてみた。
(正確には、調べてみたけどまだ途中です)環境
ruby 2.6.3
rails 5.2.3やり方
適当なマイグレーションファイル内に
binding.pry
を埋め込んで挙動を確認する。def up
class CreateUsers < ActiveRecord::Migration[5.2] def up binding.pry # ここに埋め込む create_table :users do |t| t.string :name t.timestamps end end end実行
$ rails db:migrate == 20191020160206 CreateUsers: migrating ====================================== 2: def self.up 3: binding.pry => 4: create_table :users do |t| 5: t.string :name 6: 7: t.timestamps 8: end 9: end pry(CreateUsers)> step # nextだとcreate_tableの実行後に移動してしまうのでstepでcreate_tableの中に入る 603: def method_missing(name, *args, &block) # :nodoc: => 604: nearest_delegate.send(name, *args, &block) 605: end pry(CreateUsers)> self => CreateUser pry(CreateUsers)> nearest_delegate # CreateUsersクラスのインスタンスが格納されている => <##<CreateUsers:0x00007fbc300761f8 @connection= #<ActiveRecord::ConnectionAdapters::SQLite3Adapter:0x00007f9f74064350 .....> @name="CreateUsers" @version=2019102016020 pry(CreateUsers)> name => :create_table pry(CreateUsers)> args => [:users] pry(CreateUsers)> block => #<Proc:0x00007fbc30176c10@/home/ec2-user/environment/sample/db/migrate/20191020160206_create_users.rb:4>
CreateUsers
クラスのインスタンス(nearest_delegate
)がcreate_table
を実行。pry(CreateUsers)> step 858: def method_missing(method, *arguments, &block) => 859: arg_list = arguments.map(&:inspect) * ", " 860: 861: say_with_time "#{method}(#{arg_list})" do 862: unless connection.respond_to? :revert 863: unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method) 864: arguments[0] = proper_table_name(arguments.first, table_name_options) 865: if [:rename_table, :add_foreign_key].include?(method) || 866: (method == :remove_foreign_key && !arguments.second.is_a?(Hash)) 867: arguments[1] = proper_table_name(arguments.second, table_name_options) 868: end 869: end 870: end 871: return super unless connection.respond_to?(method) 872: connection.send(method, *arguments, &block) 873: end 874: end pry(#<CreateUsers>)> self # nearest_delegateと同じ => #<CreateUsers:0x00007fbc300761f8 @connection= #<ActiveRecord::ConnectionAdapters::SQLite3Adapter:0x00007f9f74064350 .....> @name="CreateUsers" @version=2019102016020 pry(#<CreateUsers>)> method => :create_table pry(#<CreateUsers>)> arguments => [:users]
create_table
のレシーバはCreateUsers
であり、CreateUsers
の親クラス(ActiveRecord::Migrationなど)や読み込んでいるモジュールのクラスメソッドにもないっぽいので、クラスメソッドのmethod_missing
が呼ばれる。def up
とりあえず実行
$ rails db:migrate == 20191020160206 CreateUsers: migrating ====================================== From: /home/ec2-user/environment/sample/db/migrate/20191020160206_create_users.rb @ line 4 CreateUsers#up: 2: def up 3: binding.pry => 4: create_table :users do |t| 5: t.string :name 6: 7: t.timestamps 8: end 9: end pry(#<CreateUsers>)> self => #<CreateUsers:0x00007f8f04070c70 @connection= #<ActiveRecord::ConnectionAdapters::SQLite3Adapter:0x00007f9f74064350 .....> @name="CreateUsers", @version=20191020160206> pry(#<CreateUsers>)> step From: /home/ec2-user/environment/rails/activerecord/lib/active_record/migration.rb @ line 859 ActiveRecord::Migration#method_missing: 858: def method_missing(method, *arguments, &block) => 859: arg_list = arguments.map(&:inspect) * ", " 860: 861: say_with_time "#{method}(#{arg_list})" do 862: unless connection.respond_to? :revert 863: unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method) 864: arguments[0] = proper_table_name(arguments.first, table_name_options) 865: if [:rename_table, :add_foreign_key].include?(method) || 866: (method == :remove_foreign_key && !arguments.second.is_a?(Hash)) 867: arguments[1] = proper_table_name(arguments.second, table_name_options) 868: end 869: end 870: end 871: return super unless connection.respond_to?(method) 872: connection.send(method, *arguments, &block) 873: end 874: end
create_table
のレシーバがインスタンスになったので、インスタンスメソッドのmethod_missing
が呼ばれる。これ以降は
up
もself.up
も同じ(だと思う)。結論
self.up
の場合はクラスメソッドのmethod_missingが呼ばれ、その後にレシーバにインスタンスを指定した後、再度インスタンスメソッドのmethod_missingが呼ばれる。up
の場合はインスタンスメソッドのmethod_missingだけが呼ばれる。
いやいや
nearest_delegate
ってなんなのnearest_delegate
module ActiveRecord class Migration class << self attr_accessor :delegate # :nodoc: attr_accessor :disable_ddl_transaction # :nodoc: -> def nearest_delegate # :nodoc: delegate || superclass.nearest_delegate end
accessor :delegate
がある。あと下の方に、def initialize(name = self.class.name, version = nil) @name = name @version = version @connection = nil end # instantiate the delegate object after initialize is defined self.delegate = newとかあったので、今回の場合、
CreateUsers.delegate = CreateUsers.new(name : "CreateUsers", version: 2019102016020, connection: #<ActiveRecord::ConnectionAdapters::SQLite3Adapter:0x00007f9f74064350 .....>)ということ?
結び
引き続き調査します。
- 投稿日:2019-12-18T13:47:54+09:00
自分なりのテストの書き方 by RSpec
くふうカンパニーアドベントカレンダー20日目の記事です。
今年の新卒研修の中で自動テストの研修を担当してRSpecを使ったテストの書き方を説明しました。
しかしながら新卒エンジニア達からRSpecの使い方はわかったけれど、テストってどうやって書いていけばいいのと質問を受けたので私が普段どのようなステップを踏んでテストを書いているか振り返ってみることにしました。まだまだ不十分な研修で申し訳ない気持ちです?
使用するテストフレームワークは実務で利用しているRSpecです。新卒たちの質問
- そもそもテストって何をテストするの
- どこまでテストを書けばいいの
- どこから書き始めてばいいの
そもそもテストって何をテストするの
作っている機能が動作することを確認(テスト)します。
例えば名前とメールアドレスを登録できる機能があったとします。ユーザ目線ではフォームから名前とメールアドレス入力して確定すると、登録されたというメッセージと登録情報が画面に表示される機能となります。そして内部構造に注目するとデータの有効性を検証しデータベースに保存されて登録完了画面にリダイレクトする機能になります。
これらの機能が仕様通り振る舞いが正しく動作しているかテストで確認するようにします。
ユーザ目線で仕様通り動作するか確認するテストはE2EテストとしてSystem specで、開発者目線で動作しているメソッドの振る舞いを確認するテストはユニットテストとしてModel specやController specで書いています。
今回はとりあえずModel specを例に書きました。どこまでテスト書けばいいの
どの程度の品質基準を目指すのかはソフトウェアテストの基準を参考にしています。
例としてUserクラスに以下のようなメソッドがあったとします。
ユニットテストに関してはホワイトボックステストの条件網羅(C2)を目指して書いています。下のようにシンプルな処理の時は分岐網羅(C1)で留めているものもあったりしますが。。def able_to_send_mail? if name && email true else false end条件網羅の時は、nameとemailの両方がtrue、emailのみfalse、nameのみfalseの3つのテストケースのテストを書くようにします。nameがfalseであればemailの真偽は判定に影響しないのでnameとemailがfalseのケースは省略しています。条件を書くか省くか迷った場合は書いておけば漏れはないので問題はありません。
name name.present? email.present? if文の判定 ケース1 'bob' 'bob @mail.com' true true true ケース2 'bob' nil true false false ケース3 nil 'bob @mail.com' false true false 分岐網羅の時はこのようなコードがあった場合はif文がtureになるテストケースとfalseになるテストケースの2種類のテストを書くようにします。
name name.present? email.present? if文の判定 ケース1 'bob' 'bob @mail.com' true true true ケース2 nil 'bob @mail.com' false true false どこから書き始めればいいの
テストで網羅するテストケースはわかったので次は実際テストを順序立てて組み立てていきます。
テスト用のファイルが自動生成されていればそれを使って、なければ新しく作成します。テスト対象を書く
require 'rails_helper' RSpec.describe User, type: :model do describe '#able_to_send_mail?' do end # 他のメソッドがある場合は同じインデントで並べて書きます describe '他のメソッド' do end enddescribeでテスト対象のメソッド名を書きます。
他にもテスト対象のメソッドが追加された場合は同じインデントでテストを追加していきます。あり得るテストケースを書き出す
require 'rails_helper' RSpec.describe User, type: :model do describe '#able_to_send_mail?' do context 'nameとemailが存在する場合' do end context 'emailがnilの場合' do end context 'nameがnilの場合' do end end endcontextで先ほど網羅したメソッドの実行する時にあり得るテストケースを書き出します。コンテキストをネストして書くこともできますが、ネストが3重くらいになってくるような場合はメソッドの設計を見直すことを考えます。
日本語で書くか英語で書くかは全体で決まっていればどちらでもいいと思います。期待する結果を書き出す
require 'rails_helper' RSpec.describe User, type: :model do describe '#able_to_send_mail?' do context 'nameとemailが存在する場合' do it 'trueを返すこと' do end end context 'emailがnilの場合' do it 'falseを返すこと' do do end context 'nameがnilの場合' do it 'falseを返すこと' do end # 他にも期待する結果がある場合は書きます it '他の期待値' end end end enditを使って期待する結果を書き出します。
ここまで書けると後からテストを見た時にメソッドがどのような振る舞いをするかわかるようになってきました。テストコードを実装する
require 'rails_helper' RSpec.describe User, type: :model do describe '#able_to_send_mail?' do context 'nameとemailが存在する場合' do it 'trueを返すこと' do user = User.create(name: 'bob', email: 'bob@mail.com') expect(user.able_to_send_mail?).to eq true end end context 'emailがnilの場合' do it 'falseを返すこと' do user = User.create(name: 'bob', email: nil) expect(user.able_to_send_mail?).to eq false do end context 'nameがnilの場合' do it 'falseを返すこと' do user = User.create(name: nil, email: 'bob@mail.com') expect(user.able_to_send_mail?).to eq false end end end end期待する結果を検証するためのコードをexpectを使って書いていきます。
ここまでできれば一旦テストとしては完成です。テストのリファクタリング
require 'rails_helper' RSpec.describe User, type: :model do describe '#able_to_send_mail?' do subject { user.able_to_send_mail? } context 'nameとemailが存在する場合' do let(:user) { User.create(name: 'bob', email: 'bob@mail.com') } it { is_expected.to eq true } end end context 'emailがnilの場合' do let(:user) { User.create(name: 'bob', email: nil) } it { is_expected.to eq false } end context 'nameがnilの場合' do let(:user) { User.create(name: nil, email: 'bob@mail.com') } it { is_expected.to eq false } end end end重複を省いたり変数をletに書き出したりリファクタリングしました。
expect(user.able_to_send_mail?)
はsubjectでまとめて、ローカル変数だったuser
はletを使うようにしました。期待する値も真偽値のみなので自然言語で書かれた期待値は削除しました。
もっとも過度にリファクタリングしてコードの重複を省いた結果、コードを追うことが難しくなるようであれば後からメソッドの振る舞いを知るのが難しくなってしまうので、ある程度の重複は許容してもいいと考えています。まとめ
今回は私がどのように考えながらテストコードを書いていっているか振り返ってみました。
今年のフィードバックを糧に今後の新卒たちの研修を改善していきたいと思います。合わせて読みたい
第6回 Webアプリケーションのテスト
ホワイトボックステストにおけるカバレッジ(C0/C1/C2/MCC)について
【初心者向け】テストコードの方針を考える(何をテストすべきか?どんなテストを書くべきか?)
RSpecえかきうた
- 投稿日:2019-12-18T12:20:22+09:00
Rails6 のちょい足しな新機能を試す112(database.yml warning編)
はじめに
Rails 6 に追加された新機能を試す第112段。 今回は、
database.yml warning
編です。
Rails 6 では、複数データベースに対応していますが、database.yml
ファイルで複雑なERBが使われていると警告が出るようになっています。Ruby 2.6.5, Rails 6.0.1 で確認しました。 (Rails 6.0.0 でこの修正が入ったようです。)
$ rails --version Rails 6.0.1今回は、警告を出すために、 環境変数 DB によって、 MySQLデータベースかPostgreSQLデータベースにアクセスを切り変えるということで試してみたいと思います。
複数データベースの機能は使いません。Rails プロジェクトを作成する
$ rails new rails_sandbox $ cd rails_sandboxGemfile に mysql2 と pg を追加する。
MySQL と PostgreSQL どちらでも接続できるように、 Gemfile に mysql2 と pg を追加します。
Gemfilegem 'pg', '>= 0.18', '< 2.0' gem 'mysql2'bundle install を実行します。
$ bundle installdatabase.yml ファイルを編集する
database.yml
を変更します。MySQL 用の接続設定と PostgreSQL 用の接続設定を追加します。
環境変数によって、接続を切り変えられるようにします。
ここで、ポイントは、
<<: *<%= ENV.fetch("DB") { 'postgresql' } %>
となっている部分です。
この記述によって警告が出るようになります。config/database.ymlmysql: &mysql adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: host: db_mysql postgresql: &postgresql adapter: postgresql encoding: unicode pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> host: db_postgresql user: <%= ENV.fetch("POSTGRES_USER") %> password: <%= ENV.fetch("POSTGRES_PASSWORD") %> development: <<: *<%= ENV.fetch("DB") { 'postgresql' } %> database: app_development ... test: <<: *<%= ENV.fetch("DB") { 'postgresql' } %> database: app_test ... production: <<: *<%= ENV.fetch("DB") { 'postgresql' } %> database: app_production ...スクリプトを作成する
確認のためだけなのですが、 database.yml を ERB でパースして結果を出力するスクリプトを書いておきます。
scripts/parse_database_yml.rbyml = ERB.new(File.read(Rails.root.join('config/database.yml'))) print yml.resultスクリプトを実行する
スクリプトを実行します。特にエラーもなくパースできています。
development:
の次の行が、<<: *postgresql
と処理できていることに注意してください。$ bin/rails runner scripts/parse_database_yml.rb Running via Spring preloader in process 104 mysql: &mysql adapter: mysql2 ... postgresql: &postgresql adapter: postgresql ... development: <<: *postgresql database: app_development ...db:create を実行する
db:create
を実行します。
Rails couldn't infer whether you are using multiple databases from your database.yml ... please simplify your ERB.
と警告が出ます$ bin/rails db:create Rails couldn't infer whether you are using multiple databases from your database.yml and can't generate the tasks for the non-primary databases. If you'd like to use this feature, please simplify your ERB. Created database 'app_development' Created database 'app_test'なぜ警告が出るのか
複数データベースに対応するために、 Rails は、 boot 完了する前に、
database.yml
を読んで、task を生成しているそうです。task 生成する時点では、 ERB を使っておらず DummyCompiler を使って解析しており、この DummyCompiler では処理できない場合に警告を出すようにしたということらしいです。どうすれば良いのか
結論としては、ERB の部分を警告が出ないように書き変えれば良いということになります。
では、一般的にどう書き変えれば良いのかは、筆者にもわかっていません。ワーニングを出ないように修正する
今回の場合は、以下のように書き変えることで警告が出ないようにすることができました。
default
を追加してif
文使って切り変えるようにして、development
などの環境では、<< *default
を使って ERB を使わないように修正しました。config/database.ymlmysql: &mysql adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: host: db_mysql postgresql: &postgresql adapter: postgresql encoding: unicode pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> host: db_postgresql user: <%= ENV.fetch("POSTGRES_USER") %> password: <%= ENV.fetch("POSTGRES_PASSWORD") %> default: &default <% database = ENV.fetch("DB"){ 'postgresql' } %> <% if database == 'postgresql' %> <<: *postgresql <% else %> <<: *mysql <% end %> development: <<: *default database: app_development ... test: <<: *default database: app_test ... production: <<: *default database: app_production ...試したソース
試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try112_warn_database_yml - 警告が出る方
https://github.com/suketa/rails_sandbox/tree/try112_warn_database_yml_fix - 警告が出ないように修正した方参考情報
- 投稿日:2019-12-18T11:41:34+09:00
.present?はnilかそうじゃないか、という場合は不要
キャンペーンコードから適用されるキャンペーンとプランに紐づくキャンペーンがあるんだけど、キャンペーンコードからのキャンペーンを配列の一番最初に表示させたかった時の実装。
辞書型(hash型)なのが注意。
key
でキャンペーンコードからのものを探して一旦削除してunshift
で先頭にpushしてるんだけどいまいち納得いってない。。もっといい方法ないのかな?修正前
order_base_decorator.rbdef order_campaigns c = options['campaigns'].to_a cc = c.find{|x| x["key"] == "campaign-code"} c.delete(cc) if cc.present? c.unshift(cc) if cc.present? if c.present? unless c.first == {} || cc return c.unshift({}).to_a end return c end return c end修正後
order_base_decorator.rbdef order_campaigns c = options['campaigns'].to_a cc = c.find{|x| x["key"] == "campaign-code"} # cのなかで見つからなかったらccはnil if cc c.delete(cc) c.unshift(cc) end if c.present? unless c.first == {} || cc return c.unshift({}).to_a end return c end return c end
- 投稿日:2019-12-18T11:33:24+09:00
Rails: exifにタイムゾーンがないからどんな感じで保存しようかなと思った話
Classi Advent Calendar 2019 の18日目、Railsアプリケーションエンジニアをやっている@HokotaMisakiです。
今回書くお話はClassiではなく、自宅で作っている野鳥観察記録サービスで体験したものなのですが、マニアックでピンポイントなケースだと思ったので紹介します(テーマ的にRails絡んでるからセーフだと思ってる)。
主に、exifにタイムゾーンの有り無しを確認することになった経緯についての話になります。
※ なおexifとは、画像の中に埋め込まれているメタデータのことです。位置情報や時刻、撮影カメラの設定情報などが保存されています。
まずは結論から
この2つのあわせ技で行くことにしました。
- ユーザに自身のタイムゾーンを登録してもらう
- アプリケーションはユーザ毎のタイムゾーンで動き、時刻をそのままアプリケーションのタイムゾーンで保存する
「えっ、exifらしさもないし、なんか普通・・・」
って感じなんですが、ではなぜこれに行き着いたのか、なぜこれをしなければならなかったのか、を以下に記載します。
前提
自宅で作っている野鳥観察記録サービス「ZooPicker」は、世界中で撮った野鳥の写真とフィールドノート(観察場所や時間、観察種類などを記録するもの)を投稿していくサービスです。
投稿したものは公開されるので、野鳥情報の共有サービスとも言えます。
ゆくゆくは海外展開もしていきたいので、I18n系の対応も徐々に進めています。ことの発端
バーダー(野鳥を追いかけている人たちのこと)にとって、野鳥が「どこで」「いつ」出たのか、は大事な情報です。
なので素敵な野鳥写真が投稿されると、「これ!!!!どうやったら見れるの???」となります。撮った写真の場所を登録する機能はあったので(exifの位置情報は捨ててるので手入力なんですが)、
撮った写真の時刻(⇒ 「撮影現地時刻」)も登録できるようにしよう!と思いました。なお、写真の投稿はだいたい帰宅後に行うので、投稿日時は使えません。
やりたいこと: 世界の誰が見ても、撮った現地の時刻で表示させたい
ご存知のとおり、時刻およびデータベースの時刻系カラムにはタイムゾーンがあり、
かつ、ActiveRecordはそれをアプリケーションのタイムゾーンに応じて出してくれるという便利機能を持っています。加えて、写真にはexif情報があるので、
このexifにきっと時刻情報あるだろうから、それ保存すればいいじゃーんと思いました。下の絵のように、「撮影現地時刻」がアプリケーションのタイムゾーンに自動変換されちゃう事態は避けたかったわけです。
つまづいた点
exifに時刻はあるけどタイムゾーンは無い
はい。タイトルのとおりなのですが。おおよそ無いようです。
Wikipediaより:
Exchangeable image file format撮影日時の情報は、UTCとタイムゾーンを組み合わせたものではなく、機種依存のローカルタイム(現地時刻)のみで記録され、タイムゾーン情報が記録されていないので、海外旅行や出張などタイムゾーンをまたいで移動、生活する際に問題となることもある。なお、タイムゾーン情報が記録できるカメラなどもあるが機種依存の機能であって、Exif共通の仕様ではタイムゾーン情報の付加には対応していない。
言われてみればそうかーという感じです。カメラがWebに繋がんないとですもんね・・・
なお、時刻情報は
OriginalTime
という項目で存在します。「投稿日時」は時差計算されてもいいけど、「撮影時刻」は現在地の時刻を出したい
ActiveRecordは、アプリケーションのタイムゾーンに応じて時刻を表示してくれますね。
ところが、「撮影現地時刻」だけは自動変換されてほしくありません。
じゃあどこのタイムゾーンで表示するの!カメラに現地時刻入ってないのに!となりました。カメラのOriginalTimeがどこのタイムゾーンかの判定方法がわからない
わかれば、場所と照らし合わせて撮影時刻算出できるのになぁ。
結果
とりあえず ユーザのタイムゾーン≒カメラのタイムゾーン ってことにしよう
今回のケースを実現するロジックは、
1. 何らかでタイムゾーンを保存(exifの位置情報でタイムゾーンを算出 or ユーザにタイムゾーンを設定してもらう) 2. カメラのOriginalTimeから時差計算して「撮影現地時刻」を算出 3. 保存 4. 表示するときに「保存したタイムゾーンで出す」をやると思っています。
exifからの算出は、もっとタイムゾーン付きカメラが普及してからにしよう・・・と思いました。
それまでは、多少そぐわないケースもあるかもしれないけど、だいたい一致する「ユーザの希望タイムゾーン」で。現在は、時刻が取れたらそのまま自動入力、修正がある場合は手入力可、としています。
ちなみに1度「文字列で時刻保存する・・・?」と思ったです。
できあがったコード
datetime = if v[:exif][:dateTimeOriginal] # exifにはタイムゾーン情報が基本ない # とりあえずtimezone設定を使う timezone = Time.current.strftime('%z') DateTime.strptime( v[:exif][:dateTimeOriginal].gsub('-', ':') + " #{timezone}", '%Y:%m:%d %H:%M:%S' + " %z" ) end何も解決してない
実は「ユーザ自身のタイムゾーンを登録する機能」は未実装なので、サービス的にはまだ何も解決してません!
参考: システム的なこと
- Rails5.1(当時)
- 画像投稿するときは、フロントでバイナリからexifを取り出してサーバにリクエストしている
- exifは保存してない
- exif-jsを使ってる
おわりに
おおよそのケースではRailsはタイムゾーンのことをほとんど意識せずに済むような仕組みが整っていると思います。
今までの業務でも海外展開しているサービスは触ったことがなかったので、今回改めて勉強になったなぁと思いました。明日は @hakshu さんです!
- 投稿日:2019-12-18T08:27:21+09:00
5kg太ったのでイケメンがダイエットを応援してくれるLine bot作ろうとしてた話
クソアプリ Advent Calendar 2019がバズってるのを見て、約2年前に作ろうとしたクソ設計☆クソアプリを思い出した。。。
どんなアプリ
イケメン(仮)に「目標体重」と「今日の体重」を教えて、その差分から罵倒されたり褒められたりするLineBot。
2年前の私
ワイ 「おもちもおせちも美味しい。みかんも食べ放題。幸せだ...」
(久々に体重計乗る)
ワイ 「うわ!5kg太った!!!!!!(泣)」
ワイ 「痩せなきゃ。どうすればいいんだ。イケメンに叱られたら痩せるのに。」
ワイ 「....」
ワイ 「そうか、罵倒してくれるイケメンおらんかったら作ればいいのか(脳死)」使ってた技術
- Messaging API (LINE)
- ruby on rails 5.2.3
- Heroku
おそらく参考にしてた記事
今の私
とりあえず、Line Bot自体はみつからなかったから、コードだけよんでみよう...
ん?なんだこれは.....
クソアプリ以前にクソ設計じゃん!!!!(大声)【クソポイント 1】 私の目標体重はいずこへ...
とりあえず、データの構造を見てみよう...
ん...?app/models
の中に何も書いてない。もちろんdb
の中にも何もない...私の目標体重はいずこへ...
クラス変数に格納されてた。
このアプリでは、
linebot_controller.rb
にしかほとんんどコードが書いておらず、そこに全ての処理が書いてあった。linebot_controller.rbclass LinebotController < ApplicationController require "line/bot" # gem "line-bot-api" # callbackアクションのCSRFトークン認証を無効 protect_from_forgery :except => [:callback] def client @client ||= Line::Bot::Client.new { |config| config.channel_secret = ENV["LINE_CHANNEL_SECRET"] config.channel_token = ENV["LINE_CHANNEL_ACCESS_TOKEN"] } end def callback body = request.body.read signature = request.env["HTTP_X_LINE_SIGNATURE"] unless client.validate_signature(body, signature) error 400 do "Bad Request" end end events = client.parse_events_from(body) ・・・ events.each { |event| case event when Line::Bot::Event::Message case event.type when Line::Bot::Event::MessageType::Text if event.message["text"].match(/設定/) if t = event.message["text"].match(/([1-9][0-9]{0,2}|0)(\.[0-9])?/) @@targetWeight = t[0].to_f ・・・ end elsif ・・・ ・・・ end client.reply_message(event["replyToken"], message) end end } end endうん、つっこみどころ満載。
Messaging APIのリクエストボディでは、
destination
(ボットのユーザーID)とevents
(ユーザーが発火)が格納されており、events
の中は下記のようなjsonオブジェクトになってます。{ "replyToken": "", "type": "message", "mode": "active", "timestamp": 1462629479859, "source": { "type": "user", "userId": "..." }, "message": { "id": "325708", "type": "text", "text": "目標体重を50kgに設定して" } }ユーザーを識別することは非常に簡単そう。
それなのにユーザーごとのデータを管理せずにクラス変数で目標体重を管理している。
つまり、この目標体重は全ユーザー共通で書き換え自由な状態になっていたのだ。
自分の目標体重、丸わかり。。。。あれかな。みんなのイケメン(仮)になるのが嫌だったのかな。
【クソポイント 2】 性癖.yml
DBがないけどイケメンはセリフを返す。
しかも、目標体重に近づけば優しくなる。
これはどうゆう仕組みなんだろ...と探っていったら...イケメン(仮)に言われたいセリフがみっちり書いてある
性癖.yml
が発掘されました。
全部自分で羅列してったのか...痛いぞ、自分。また、yamlも絶妙に読みづらい。
入れ子になった配列がずらーーーっとならんでおり、voice["message"][差分][乱数]
で取ってくるって感じ。
配列にする必要なかった部分まで配列...linebot_controller.rb# ymlファイル読み込み voice = YAML.load_file("#{Rails.root}/config/project.yml") # diffは目標体重と現在の体重の差分 message_length = voice["message"][diff].length - 1 diet_message = voice["message"][diff][rand(message_length)] message = { type: "text", text: "今日の体重は#{weight}kgで、目標体重の#{@@target}まであと#{sprintf("%.1f", diff)}kg減か!#{diet_message}" }【クソポイント 3】
ワイ『今日は23歳の誕生日!50kgだったよ!』
イケメン(仮)『今日の体重は23kg』messageに数字があったら、必ず最初に出てくる数字を体重だと扱う処理がかいてあるため、イケメン(仮)と円滑な会話が全くできない仕様。
書き方もつっこみどころ満載...if myWeight = event.message["text"].match(/([1-9][0-9]{0,2}|0)(\.[0-9])?/) myWeight = myWeight[0].to_f #やばい # 果てしなく痛い処理の連続 end実際のところ
Q. このbotをつかいましたか?
A. 使ってないです。性癖.ymlを書いてる時に目が覚めました。Q. 痩せました?
A. 全くもって痩せなかったです。さいごに
しょうもない記事を最後まで読んでくださり、ありがとうございます。
いつもは結構真面目に記事を書いてるので、よかったら読んでください。ちなみに、ダイエットに一番効力あったのは、好きな人に言われた「肉団子」のひとこと。
痩せたい。
- 投稿日:2019-12-18T00:07:35+09:00
Rails のバグ報告テンプレートがすごい!
この記事は
ruby-jp の #rails チャンネルで知った Rails のバグ報告テンプレートが目からウロコだった話をします。bug_report_templates/active_record_gem.rb の存在を知らない方向けです。
概要
気軽に ActiveRecord の動作を確かめたい
ちょっと ActiveRecord の動作を確かめたい。でも手元にちょうといい Rails プロジェクトがない。rails new するのは面倒だ……。マイグレーションも書かなきゃ……。
そんなとき、Rails のリポジトリに含まれている次の Ruby スクリプトが役に立ちます。Rails のバグ報告テンプレートだそうです。
https://github.com/rails/rails/blob/v6.0.2/guides/bug_report_templates/active_record_gem.rb
active_record_gem.rb# frozen_string_literal: true require "bundler/inline" gemfile(true) do source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } # Activate the gem you are reporting the issue against. gem "activerecord", "6.0.0" gem "sqlite3" end require "active_record" require "minitest/autorun" require "logger" # This connection will do for database-independent bug reports. ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") ActiveRecord::Base.logger = Logger.new(STDOUT) ActiveRecord::Schema.define do create_table :posts, force: true do |t| end create_table :comments, force: true do |t| t.integer :post_id end end class Post < ActiveRecord::Base has_many :comments end class Comment < ActiveRecord::Base belongs_to :post end class BugTest < Minitest::Test def test_association_stuff post = Post.create! post.comments << Comment.create! assert_equal 1, post.comments.count assert_equal 1, Comment.count assert_equal post.id, Comment.first.post.id end end解説
このスクリプトは次のことを行っています。
- bundler/inline を使って Ruby スクリプト上で bundle install を行っている。
gemfile
メソッドの第 1 引数にtrue
を渡すことで、まだインストールしていない Gem をインストールする。- (Bundler のバージョンが 2.1.0 以上の場合)
gemfile
メソッドの第 2 引数にquiet: true
を渡すと bundle install 時のログ出力を抑制できる。BUNDLE_PATH
は無視する。つまり Gem は必ずグローバルにインストールされる。- ActiveRecord::Base.establish_connection の database オプションに
:memory:
を渡すことで、SQLite のデータベースをメモリ上に作成している。- ActiveRecord::Schema.define を使うことで、スクリプト内でテーブルを定義、作成している。
- スクリプト内でモデルを定義している。
- Minitest::Test で単体テストを実行している。
いろんなテクニックがシンプルに詰まっていて、いいスクリプトだなと思いました
参考
- Bundler
- bundler/inline should ignore BUNDLE_PATH and install gems to GEM_HOME by robuye · Pull Request #7154 · bundler/bundler
- bundler/inline での
BUNDLE_PATH
の扱いについて- SQLite
- In-Memory Databases
- インメモリのデータベースについて
- 投稿日:2019-12-18T00:04:10+09:00
Rails6 Bootstrap link_toを使ってmethodとclassを両方とも指定しよう
目的
- ログアウトボタンを作る時にlink_toを使用して
method
とclass
を指定する際に若干詰まった内容をまとめる結論
methodとclassを個々に中括弧でくくるのではなく一緒に定義する。
<!-- NG --> <li class="navbar-item"><%= link_to("ログアウト", "/logout", {method: "delete"}, {class: "nav-link"}) %></li> <!-- OK --> <li class="navbar-item"><%= link_to("ログアウト", "/logout", {method: "delete", class: "nav-link"}) %></li>情報
header内をBootstrapを使用して書いている。
今まで
method
を指定していなかったのでそのままaタグを使用してheaderメニューを書いてた。aタグを
link_to
に置き換えたい。困ったこと
link_to
を使用して個々の中括弧でmethod
とclass
を指定すると構文エラーが出た。下記にエラーが出た時のコードを記載する。
<li class="navbar-item"><%= link_to("ログアウト", "/logout", {method: "delete"}, {class: "nav-link"}) %></li>解決法
- 同じ中括弧内で
method
とclass
を指定すれば良い。下記に正常に表示された時のコードを記載する。
<li class="navbar-item"><%= link_to("ログアウト", "/logout", {method: "delete", class: "nav-link"}) %></li>