- 投稿日:2019-12-11T23:17:19+09:00
Rails新規アプリケーション作成(haml版)①大枠作り
書いてあること
1.新規アプリ作成(バージョン指定をしてrails newをする)
2.Git管理下に置いてリモートリポジトリへ
・.gitignoreについて
3.haml実装
4.コントローラー・ビュー・ルーティングの作成
5.Sassの導入(リセットcss"YUI3"導入)
6.ビューをマークアップ(大枠作成)
7.参考ページ
・参考ソース
8.終わりに1.新規アプリ作成
Ruby on Railsのバージョンを変更
ターミナル$ cd ~ # ホームディレクトリに移動 $ gem install rails --version="5.0.7.2" #バージョン指定ターミナル$ rbenv rehashバージョン指定して新規作成
cd
でアプリを作りたいディレクトリを指定して移動ターミナル$ rails _5.2.3_ new 新規アプリ名 -d mysql
cd
で今作ったディレクトリへ移動ターミナル$ rails db:create2.Git管理下に置いてリモートリポジトリへ
【参照】
アプリ作成〜SNSグループ作成と連携
- gitの管理下におく
ターミナル/git管理下に置きたいディレクトリへcdで移動し下記実行/ $git init $git add . /.は全部という意味/ $git status /ちゃんと移動したか確認/
- ローカルリポジトリに追加(コミットするよ)
ターミナル$git commit -m "initial commit"
リモートリポジトリ作成完了!
.gitignore
コミットしたくないファイル・ディレクトリを指定できる設定ファイル。画像投稿機能などがあるものは画像が保管されるpublic/uploadsディレクトリを.gitignoreに追加しておくと、画像がGithubにコミットされない.gitignore# 末尾に次の記述を追加 public/uploads/*3.haml実装
・gem導入
Gemfilegem "haml-rails", ">= 1.0", '<= 2.0.1' # 最下部に記載ターミナル$ bundle
導入完了
・hamlに変換
ターミナル$ rails haml:erb2haml途中でターミナルに出てきたらWould you like to delete the original .erb files? (This is not recommended unless you are under version control.) (y/n) #yを入力してエンター
hamlへ変換完了
4.コントローラー・ビュー・ルーティングの作成
rails g
の前にやっておくこと
不要なファイルを生成しないように記述しておくconfig/application.rb# 省略 (例) module ChatSpace class Application < Rails::Application config.generators do |g| g.stylesheets false #←このfalseで作成しないファイルを指定できます g.javascripts false g.helper false g.test_framework false end end endコントローラ作成
ターミナル$ rails g controller コントローラ名(例:popcorn)コントローラにindexアクション作るよ
popcorn_controllerdef index end
app/views/コントローラ名と同じ名前のディレクトリ/index.html.haml
を作成(例)app/views/popcorn/index.html.hamlhello haml!(おすきに入力)ルーティングを設定(root_pathにアクセスした場合indexに飛ぶ設定)
routes.rbroot to: 'popcorn#index' #'コントローラ#indexのビューへ飛んでね'の意味5.Sassの導入(リセットcss"YUI3"導入)
application.css
は削除し、application.scss
を作成application.scss@import "scssファイル名"; #importしたいファイル名の頭は_(アンダーバー)始まり。
_reset.scss
をapplication.scssと同じディレクトリ内に作成YUI3導入
YUI3
Source code(zip)
をダウンロードしてファイルを開き全てコピーして_reset.scss
に貼り付けapplication.scss@import "reset";6.ビューをマークアップ(大枠作成)
紙にボックスの配置を書いて命名する(後でどこに何の名前のクラスを配置したか見返しやすく、sassを当てたりイベント発火させるクラスがわかりやすくなって便利)
7.参考ページ
参考ソース
_reset.scss/* TODO will need to remove settings on HTML since we can't namespace it. TODO with the prefix, should I group by selector or property for weight savings? */ html{ color:#000; background:#FFF; } /* TODO remove settings on BODY since we can't namespace it. */ /* TODO test putting a class on HEAD. - Fails on FF. */ body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fieldset, legend, input, textarea, p, blockquote, th, td { margin:0; padding:0; } table { border-collapse:collapse; border-spacing:0; } fieldset, img { border:0; } /* TODO think about hanlding inheritence differently, maybe letting IE6 fail a bit... */ address, caption, cite, code, dfn, em, strong, th, var { font-style:normal; font-weight:normal; } ol, ul { list-style:none; } caption, th { text-align:left; } h1, h2, h3, h4, h5, h6 { font-size:100%; font-weight:normal; } q:before, q:after { content:''; } abbr, acronym { border:0; font-variant:normal; } /* to preserve line-height and selector appearance */ sup { vertical-align:text-top; } sub { vertical-align:text-bottom; } input, textarea, select { font-family:inherit; font-size:inherit; font-weight:inherit; *font-size:100%; /*to enable resizing for IE*/ } /*because legend doesn't inherit in IE */ legend { color:#000; }8.終わりに
①ってタイトルつけたけど果たして②はできるのかな・・・?(②にはページ遷移をまとめたいが。)
MVCの流れを追いつつ変数(インスタンス変数)の定義の仕方や記述の仕方が甘いので、そこをまとめたいと思うのですが、整理しながらだと思ったようにかけないですね(あれもこれも詰め込みたくなるので)
今週の目標はメッセージ送信機能実装かインスタンス変数を定義して7つのアクションごとにページ遷移の記述方法がかけたらと思います。jsもスクレイピングも復習したい。。。上手な頭の中身の整理方法があったらぜひ教えてくださいませφ(・・
記述ミスや説明不備がございましたらご指摘いただければ幸いです。
- 投稿日:2019-12-11T22:56:45+09:00
コスモを感じた初めてのRSpec
自己紹介
さっそくだが、君は、小宇宙(コスモ)を感じたことがあるか!?
はじめまして。営業からRuby on RailsのWebエンジニアになったたっきー(半年)と言います。
僕は文系出身で開発は全くの未経験ですが、一念発起してJOBチェンジした者です。
とても優しく指導して下さっている先輩たちのもと、コツコツと勉強を進めているのですが、やはり分からない事も多く、先輩たちの時間を頂くことが多い恐縮な日々を過ごしています。。とりあえずRubyを触って半年経ちました。
formの動きとか基礎的なものはようやく動かせるようになりましたが、まだまだ半人前。
そんなちょっとやそっと触っただけで、できる甘い世界ではないですね。家はシェアハウスに住んでいて住人達から黒魔術ってタイトルのQiita記事をネタにされてブラックマジシャンって呼ばれてます。
今回は、会社で自分の書いたコードのテストを書いてねって言われた10月を振り返ります。スキル
HTMLとCSSは最低限作りたいイメージのものは作れるようになった。
JSはまだまだテンプレートとか調べながらじゃないと、全然できません。
rubyは分岐やら正規表現やら初歩中の初歩は理解したけど、綺麗には書いていない(らしい)
railsはチュートリアル二周半回って、イメージはつくけどまだお手本ないと辛い感じ。テスト元のコントローラー
細かくは書けないですが、ユーザーに設定してもらうユーザーに設定してもらうparamsを削除・作成・編集(RENAME)するものを作りました。
アクティブレコードで対象のモデルからデータを引っ張ってきて、idで合わせている書き方をしています。文法
作法は難しい。
これまで書いていたRails的なdef...endみたいな単純な書き方ではないからだ。
それでも任されたからに書き始めないといけない・・・STEP1 RSpecのファイルを作る。該当のcontrollerの名前に合致するようにspecファイルを作成してください。
$ touchでコマンドで書いてもよし、該当フォルダで右クリックしてファイルを作ってもよし、まあ良しなにやってくれ。STEP2 ファイルの中身をまずはdescribeを書いてく
例えば、GET関連のテストを書きたい時には一番最初にこのように書くんだ。
categories_controller_spec.rbdescribe'⚪︎◯#show'do コメント it "●●" do expect(××).to eq □□ end end※実際にどのように書いていくかは伊藤さんの記事がとても丁寧に紹介されているよ。
https://qiita.com/jnchito/items/42193d066bd61c740612RSpecという小宇宙
テストを書く意義はテストにアサインされる身になるとよくわかる。
何十何百ってパターンを回さないといけないようなテストを人力なんかでやっちゃいけない。
勿論、必要なケースもあるだろうが、あまり生産性を感じない。その点自動テストは、勝手にやってくれる。ロジックに誤りがないならヒューマンエラーも起きない。
ヒューマンエラーってのは最悪だ。時間を使って、無駄なことをしている。
だから、効率化効率化なんていろんなところで言われているんだ。自分は自動テストというものに途轍もない「小宇宙(コスモ)」を感じてしまった。
勉強法
書籍
https://leanpub.com/everydayrailsrspec-jp
おすすめ記事
https://qiita.com/jnchito/items/42193d066bd61c740612
実践
http://tech-drill.in/questions/12最後に
割愛しすぎた部分もあるので、今後はもう少しhogeとか使って、イメージしやすい記事にします!
- 投稿日:2019-12-11T21:33:10+09:00
Railsでのhaml実装
書いてあること
1.haml-railsを用いた実装
- gem導入
- hamlに変換
2.参考ページ
3.終わりに1.haml-railsを用いた実装
・gem導入
Gemfilegem "haml-rails", ">= 1.0", '<= 2.0.1' # 最下部に記載ターミナル$ bundle
導入完了
・hamlに変換
ターミナル$ rails haml:erb2haml途中でターミナルに出てきたらWould you like to delete the original .erb files? (This is not recommended unless you are under version control.) (y/n) #yを入力してエンター
hamlへ変換完了
2.参考ページ
3.終わりに
hamlはhtmlよりもコードの記述が少し減るので、かきやすいですがAWSにアップロードする際は脆弱性の問題もあるので対策をしないと怖いですね(私まだしてないけど。)
対策についてもまとめたらUPします。
記述等に誤りがございましたらご指摘いただければ幸いです。
- 投稿日:2019-12-11T21:27:31+09:00
Ruby on Rails、ハマったところ
Ruby on Railsを使っている中でハマったポイントを適宜追加して行きます。
・gem
paranoia
を利用中、application_record.rb
にacts_as_paranoid
と記述してしまい、論理削除を必要としないテーブルで
UnknownAttributeError: unknown attribute 'deleted_at' for Bill.
と怒られまくってしまった。
- 投稿日:2019-12-11T21:12:33+09:00
Ruby 価値が大きくなる組み合わせ問題 解いてみた
はじめに
毎月先輩から出していただいた課題に取り組んでいます、 mi0です。
11月は価値が大きくなる組み合わせ問題
、いわゆる最適化問題
を解きました。
この記事は解く〜レビューをいただくまでの過程を纏めた備忘録です。
こうやったらもっとよくなる、などのご指摘があればコメント頂けると嬉しいです!過去の記事はこちら!↓
登場人物
- 私
- 社会人2年目PG。
3年目
が最近の恐怖ワード。最近異様に#select
を乱用している気がして不安になってきた。- 出汁巻先輩
- オムレツちゃんの先輩。大根おろしはほどほどにしてほしいと思っている。コーディングが得意。
- オムレツちゃん
- 私の心の中に住んでいる妖精。ケチャップはしっかりかけて欲しいらしい。
出題された問題
価値が大きくなる組み合わせ問題
問題
- 価格と重量のある、いくつかの品物を重量制限のある入れ物で運ぶ際、価格の合計が大きくなる組み合わせを出力する
メニューの元データ
db/data.csv
から取得要求仕様
- 詰め込む入れ物の重量制限は指定できる
- 品物は 1 つずつ詰め込むことができる
- 同じ商品を複数詰め込むことができない
- 提案する組み合わせ数は指定できる
- 組み合わせは価格が大きくなる順に並び替える
- 価格が同じ場合は、重量の重い順に並び替える
- 組み合わせる品物は
id
の昇順- 引数値の検証は不要
0 <= 入れ物の制限 <= 13
0 <= 提案する組み合わせ数 <= 10
- 使用するデータの価格と重量の組み合わせはユニーク
- 例) 価格: 2、重量: 3 の商品は 1 つのみで複数あることは考慮しないものとする
- 1 つも品物を詰め込まない組み合わせは除外すること
- 処理が複雑になる場合は、コメントや gem を追加しても良い
メソッドの返す値の形式
例)下記の形式(答えでは無い)
Array
- Hash
- key: Symbol
- value:
- total_price: 合計価格
- total_weight: 合計重量
- records: Array
- Hash: CSV の1レコード(一部型変換)
- key: Symbol
- value:
- id: Integer
- price: Integer
- weight: Integer
[ { records: [{ id: 1, price: 1, weight: 1 }, { id: 2, price: 2, weight: 2 }, { id: 3, price: 3, weight: 3 }], total_price: 10, total_weight: 10 }, { records: [{ id: 1, price: 1, weight: 1 }, { id: 2, price: 2, weight: 2 }], total_price: 9, total_weight: 9 }, { records: [{ id: 1, price: 1, weight: 1 }], total_price: 8, total_weight: 8 } ]※CSVの中身は以下。
data.csvid,price,weight 1,2,3 2,3,4 3,2,1 4,3,2 5,6,3フェーズ1 自分で考える
私「CSVかぁ…………(バッチバチの
Ruby
でCSVいじったこと無くないか?って顔)」オムレツちゃん「大丈夫、それくらいならチョチョイよ!それにしても出汁巻先輩、なかなかの問題を出してきたわね……!」
私「そーなの?何となく出来そうな気がしているんだけども……組み合わせを作っていくんでしょ?その組み合わせを作るのも、パターンだからメソッド使ったらうまいこと出来るんじゃないかなあ(無知)」
オムレツちゃん「(大丈夫かしらこいつ……)」
私「とりあえず全然イメージ湧かないから
具体的に組み合わせを作ってみよう
!」私「とりあえず
価値が大きいものからぶち込んでいけばその入れ物の中の価値は最大になる
んじゃないかな?同じ価値の場合は重量が軽いものを優先的に入れる
。と、いい感じに組み合わせが作れるんじゃないでしょうか!」私「実際のデータを使って纏めてみよう!」
私「ふんふん、具体的な組み合わせがあると考えやすいな〜。これでテストデータも出来たようなものだし、ガシガシ書いて行こう」
私「やることとしてはこんな感じかな〜」
- ソートされたデータを作る
- 袋の容量よりも大きいものは除いておく
- 大きいものから入れられるだけ袋に詰める
- 詰めたものの中で可能な組み合わせを作る
- 組み合わせを元に重量と価値を計算する
実際に解いていく
私「よし!かくぞ!」
私「出汁巻先輩から事前にいただいたコードは……と。」
require 'csv' class Suggest def initialize(limit, combinations) @limit = limit @combinations = combinations @data = CSV.read('db/data.csv', headers: true) end def result [] end end私「なるほどなるほど……CSVの取り込みのところだけ少しいじろうかな。ヘッダーの情報とかいらないもんな」
require 'csv' class Suggest def initialize(limit, combinations) @limit = limit @combinations = combinations @data = CSV.read('db/data.csv', headers: true) end def result sorted_data.map { |data| create_suggest(data) }[0..(@combinations - 1)] end end私「ついでに
result
メソッドの理想形も書いておいたぞ!ソートしたデータを使ってなんかメソッド呼んだらいい感じの配列ができて、そこから指定範囲分取り出す!いい感じじゃない!?」私「もう少し具体的に
create_suggest
を書いておこう………」require 'csv' class Suggest def initialize(limit, combinations) @limit = limit @combinations = combinations @data = CSV.read('db/data.csv', headers: true) end def result sorted_data.map { |data| create_suggest(data) }[0..(@combinations - 1)] end private def create_suggest create_records.map do |result| { result: result, total_price: result.sum { |item| item['price'].to_i } total_weight: result.sum { |item| item['weight'].to_i } } end end私「組み合わせを作ってくれる
create_result
メソッドを呼んだ後に欲しい形にデータを整形してくれるメソッド!うんうん、大まかn私「よしよし……次はデータをソートしよう!」
require 'csv' class Suggest def initialize(limit, combinations) @limit = limit @combinations = combinations @data = CSV.read('db/data.csv', headers: true) end # 中略 private # 中略 def sorted_data @data.sort_by { |data| data['price'].to_i }.reverse.map do |data| { id: data['id'].to_i, price: data['price'].to_i, weight: data['weight'].to_i } end end end私「まず
sort_by
で価値順に並び替える。sort_by
だと価値が低い順に並んじゃうからreverse
メソッドで価値が高い順に並び替える。その上でそれぞれの値を文字列から数値
に置換しておくよ。以降の処理では値のことを気にしなくてよくなるもんね!」※
sort_by
のブロック内のdata['price'].to_i
を負の数にしてあげるとreverse
が不要になることに後日気付きましたが、直感的じゃないような気も……。メソッド呼び出しが少なくなるから-(data['price'].to_i )
みたいに書いてあげるのがいいんでしょうか……私「次はそれぞれの組み合わせを作ってくれるメソッドを作ろう!」
require 'csv' class Suggest def initialize(limit, combinations) @limit = limit @combinations = combinations @data = CSV.read('db/data.csv', headers: true) end # 中略 private def sorted_data @data.sort_by { |data| data['price'].to_i }.reverse.map do |data| { id: data['id'].to_i, price: data['price'].to_i, weight: data['weight'].to_i } end end def create_records sorted_data.each_with_object([]) do |record, array| next if @limit < record[:weight] end end end私「とりあえず
ソートしたデータを使って配列を作る
からeach_with_object
を使うよ〜!そんでもって袋の重量を超過している品物は最初から省いてしまう
処理を書いておく。」私「次は袋に入るだけ詰め込んでいくよ〜!」
私「サンタクロースみたいで楽しいね!(??????)」
require 'csv' class Suggest def initialize(limit, combinations) @limit = limit @combinations = combinations @data = CSV.read('db/data.csv', headers: true) end # 中略 private def sorted_data @data.sort_by { |data| data['price'].to_i }.reverse.map do |data| { id: data['id'].to_i, price: data['price'].to_i, weight: data['weight'].to_i } end end def create_records sorted_data.each_with_object([]) do |record, array| next if @limit < record[:weight] array << create_records_pattarn_array end end def create_records_pattarn_array sorted_data.each_with_object([]) do |target_item, result| result << target_item if result.sum { |r| r[:weight] } + target_item[:weight] <= @limit end end end私「今ある品物の中で一番価値の高い組み合わせを作る。それを最終的な組み合わせを格納するarrayに入れるよ。」
私「で、この最大の組み合わせから作ることのできる全パターンを作ればいいんだけど……。」
私「ど………………………………………………………………。」
私「これ組み合わせの個数バラバラじゃんね……………………?」
私「
combination
使えないじゃんね……………………?」オム「気付くのおそ………………」
require 'csv' class Suggest # 中略 private # 中略 def create_records sorted_data.each_with_object([]) do |record, array| next if @limit < record[:weight] pattarns = create_records_pattarn_array array << pattarns [pattarns.first].product(pattarns - [pattarns.first]).each { |p| array << p } if pattarns.size > 2 end end # 略 end私「3つ入っている時は、6つの組み合わせが作れるように、
3つ以上品物が入っている時はそれぞれの組み合わせを作れるはず
。」私「1回のループでその組み合わせは作りきれないから、ひとまず
今できている組み合わせの、一番最初に入っているもの
ベースで作れる組み合わせを全部作る。」私「
create_records
を全部回し切ったら全パターン作れてるはず!」オム「それだと被りが出ちゃうんじゃない?」
私「う、うーん……」
私「
uniq
で……。」オム「ダメじゃん……………。」
私「これだと処理がめちゃ遅なのは分かるんだけど……今の私ではもう……これくらいしか思い浮かばない……。」
〜〜その後、
create_records
をベースにデータを整え返すような実装に整えた結果が以下〜〜require 'csv' class Suggest def initialize(limit, combinations) @limit = limit @combinations = combinations @data = CSV.read('db/data.csv', headers: true) end def result create_suggests.sort_by { |suggest| suggest.values_at(:total_price, :total_weight) }.reverse[0..(@combinations - 1)] end private def sorted_data @data.sort_by { |data| data['price'].to_i }.reverse.map do |data| { id: data['id'].to_i, price: data['price'].to_i, weight: data['weight'].to_i } end end def create_suggests records_array = create_records return [{ records: [], total_price: 0, total_weight: 0 }] if records_array.empty? records_array.map do |records| { records: records.sort_by { |record| record[:id] }, total_price: records.sum { |record| record[:price] }, total_weight: records.sum { |record| record[:weight] } } end.uniq end def create_records sorted_data.each_with_object([]) do |record, array| next if @limit < record[:weight] pattarns = create_records_pattarn_array array << pattarns [pattarns.first].product(pattarns - [pattarns.first]).each { |p| array << p } if pattarns.size > 2 end end def create_records_pattarn_array sorted_data.each_with_object([]) do |target_item, result| result << target_item if result.sum { |r| r[:weight] } + target_item[:weight] <= @limit end end endフェーズ2 レビューをいただく
出汁巻「じゃあレビューしていこう」私「お願いします!」
出汁巻「まず私さんのコードなんだけど」
私「はい」
出汁巻「
combinations
に0を渡すと組み合わせが全パターン取得できちゃうんだよね」私「そんな…………」
出汁巻「これをよく見て」
reverse[0..(@combinations - 1)]私「…………」
私「アッ!!!!!!!!!」
私「うそ……無理…ダメすぎる……これはダメ……
[0..(0-1)]
は[0..-1]
で全部じゃん……ちょっとこれは恥ずかしすぎました……[0...@combinations]
ですね…………」出汁巻「そうだね。
[0, @combinations]
とも書ける。」私「はい………」
出汁巻「次!
#sorted_data
の中でデータをto_i
してたけど、せっかくなら#initialize
の中でやった方がより良かったかな。sorted_data
の時点で同じキーの値にto_i
を二回当ててたけど、そういうのがなくなる。」私「あ……確かに。」
出汁巻「それから、以下みたいなコードは早期
next
した方が可読性が上がるよ」[pattarns.first].product(pattarns - [pattarns.first]).each { |p| array << p } if pattarns.size > 2 ↓ next if pattarns.size <= 2 [pattarns.first].product(pattarns - [pattarns.first]).each { |p| array << p }出汁巻「最後になんだけど……私ちゃんの処理、
sorted_data
の個数分同じパターンを作ってたよ」私「えっ」
出汁巻「以下のメソッド見て。」
def create_records sorted_data.each_with_object([]) do |record, array| next if @limit < record[:weight] pattarns = create_records_pattarn_array array << pattarns [pattarns.first].product(pattarns - [pattarns.first]).each { |p| array << p } if pattarns.size > 2 end end def create_records_pattarn_array sorted_data.each_with_object([]) do |target_item, result| result << target_item if result.sum { |r| r[:weight] } + target_item[:weight] <= @limit end end出汁巻「
sorted_data
起点でやってるから、何回繰り返しても処理が走っちゃって、同じ組み合わせが出来ちゃうよね。これ、本当ならcreate_records_pattarn_array
にrecord
を渡すんだったんじゃない?」私「…………………………………………(死)」
出汁巻「それから、重複する組み合わせが発生してるってことは
その分余計なループが回ってる
ってことだから、その分処理も遅くなるよね」私「はひ…………。」
出汁巻「この記事とかを参考にしたら、基本の考え方は分かり易かったと思う。私ちゃんは力技でやって一番いいパターンは正解するんだからすげーよ。」
私「あは……(頑張って具体例を出して考えたことが唯一活かされた瞬間)」
※以下、先輩の解答例コード
require 'csv' # # 組み合わせを提案するクラス # class Suggest # # 提案クラスインスタンス作成 # # @param [Integer] limit 重量の上限 # @param [Integer] combinations 組み合わせの要求提案数 # # @return [Object] インスタンス # def initialize(limit, combinations) @limit = limit @combinations = combinations @data = CSV.read('db/data.csv', headers: true).map do |row| { id: row['id'].to_i, price: row['price'].to_i, weight: row['weight'].to_i } end end # # 結果を取得 # # @return [Array<Hash>] 価値、重量の高い順の組み合わせ # @example # Example return output: # # [{:records=> # [{:id=>2, :price=>3, :weight=>4}, # {:id=>3, :price=>2, :weight=>1}, # {:id=>4, :price=>3, :weight=>2}, # {:id=>5, :price=>6, :weight=>3}], # :total_price=>14, # :total_weight=>10}, # {:records=> # [{:id=>1, :price=>2, :weight=>3}, # {:id=>3, :price=>2, :weight=>1}, # {:id=>4, :price=>3, :weight=>2}, # {:id=>5, :price=>6, :weight=>3}], # :total_price=>13, # :total_weight=>9}, # {:records=> # [{:id=>1, :price=>2, :weight=>3}, # {:id=>4, :price=>3, :weight=>2}, # {:id=>5, :price=>6, :weight=>3}], # :total_price=>11, # :total_weight=>8}] # def result r = calculation.sort do |a, b| (b[:total_price] <=> a[:total_price]).nonzero? || b[:total_weight] <=> a[:total_weight] end r[0, @combinations] end # # 組み合わせの算出 # # @return [Array<Hash>] 価値、重量、データ毎にまとめた組み合わせ # @example # Example return output: # # [{:total_price=>2, # :total_weight=>1, # :records=>[{:id=>3, :price=>2, :weight=>1}]}, # {:total_price=>3, # :total_weight=>2, # :records=>[{:id=>4, :price=>3, :weight=>2}]}, # {:total_price=>6, # :total_weight=>3, # :records=>[{:id=>5, :price=>6, :weight=>3}]}] # def calculation aggregate.inject([]) do |array, data| next array if data[:records].size.zero? array << { total_price: data[:value], total_weight: data[:records].map { |record| record[:weight] }.sum, records: data[:records].sort_by { |record| record[:id] } } end end # # 価値毎に集計した組み合わせ # # @return [Array<Hash>] 価値、重量、データ毎にまとめた組み合わせ # @example # Example return output: # # [{:value=>0, :records=>[]}, # {:value=>2, :records=>[{:id=>3, :price=>2, :weight=>1}]}, # {:value=>3, :records=>[{:id=>4, :price=>3, :weight=>2}]}, # {:value=>6, :records=>[{:id=>5, :price=>6, :weight=>3}]}] # def aggregate array = work_array @data.each do |data| (0..@limit).to_a.reverse.each do |i| next if array[i][:value].nil? || (i + data[:weight]) > @limit array[i + data[:weight]] = format_value(array[i], data) end end array end # # 集計時に使用する作業領域 # # @return [Array<Hash>] 初期化された作業領域(重量の上限数 + 1) # @example # Example return output: # # [{:value=>0, :records=>[]}, # {:value=>nil, :records=>[]}, # {:value=>nil, :records=>[]}] # def work_array array = Array.new(@limit + 1) { { value: nil, records: [] } } array[0][:value] = 0 array end # # 集計時に # # @param [Hash] value 加算先のHashオブジェクト # @example # Example value output: # # {:value=>0, :records=>[]} # @param [Hash] data 加算元のHashオブジェクト # @example # Example data output: # # {:id=>1, :price=>2, :weight=>3} # # @return [Hash] 初期化された作業領域(重量の上限数 + 1) # @example # Example return output: # # {:value=>2, :records=>[{:id=>1, :price=>2, :weight=>3}]} # def format_value(value, data) { value: value[:value] + data[:price], records: value[:records] + [data] } end end最後に
今回は初歩的なミスが目立ったのが良くなかったです。自力でやったが故に、迷走に迷走を重ねてしまったのも良くなかったです。もう少し
既に存在する知識
を調べて活用しなければ……と痛感しました。最適化問題、難しいですね……。考え方を理解した上で、有事の時に活かせるようにしたいです。
- 投稿日:2019-12-11T20:54:18+09:00
[初歩的ミス]データベース作成でつまづいた時の話ー原因から解決まで
個人アプリを作成にあたり、DBを作成していたときにつまづいた話を以下に記載します。
カリキュラムなど指示通りにしかやっていなかったりすると、中々気づけないポイントだったと思います。何につまづいたのか+試してみたこと。
そもそも何につまづいたのか、以下に記載します。
個人アプリでDBを準備しようといつも通り下記コマンドを実行して、DB作成をしました。terminal$ rails db:create $ rails db:migrateその後、DBが作成できているか「Sequel Pro」を確認したところ、できていないことが判明。。。
念のため、ターミナルでもmysqlを確認しましたが、見つかりませんでした。そこで、何かコマンドミスがあったことを疑い、改めてDB作成コマンドを実行しました。
terminal$ rails db:create Database 'db/development.sqlite3' already exists Database 'db/test.sqlite3' already exists既にDBがあるよ!とのこと。でもやっぱり見つかりません。。。
ここで気づかないのが、初学者な私。まだまだでした。
その後、いろいろ調べた結果、原因が判明します。原因
まずは結論から、今回の問題の原因は、Database.ymlの記述とSequel proの設定が違うことでした!
以下、細かく記載していきます。
そもそもですが、$rails db:createは[Database.yml]の記述をベースにDBを作成します。
そこで、今まで私が作成していた記述と今回の記述を確認しました。database.yml#今までのファイル default: &default adapter: mysql2 #今回のファイル default: &default adapter: sqlite3上記は一部抜粋ですが、adapterが違ってます!
Sequel proはmysqlのデータを引っ張ってきているので、いくらDBを作成しても出てくるわけないですね。。。
これで原因はわかりました!早速修正していきます。解決方法
まずはdatabase.ymlを今までのものをベースに書き換えます。
database.ymldefault: &default adapter: mysql2まだこのままでは終わりません!
adapterを変更するにはgemも変更する必要があります。Gemfilegem 'sqlite3' #以下に変更 gem 'mysql2', '>= ver記載bundle installも忘れずに行いましょう。
terminal$bundle install以上で変更は完了です!
改めてDB作成コマンド実行し確認したところ、Sequel proでも確認できました!やはり自分で作成してみると、新たな気づきがありますね!
引き続き色々試しながらアプリ作成をがんばっていきたいと思います!以上となります。最後までご覧いただき、ありがとうございました!
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします!
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。参照
[初学者]既存アプリのDBをMySQLに変更する方法
https://qiita.com/shi-ma-da/items/caac6a0b40bbaddd9a6f
- 投稿日:2019-12-11T20:53:32+09:00
(雑記)bundle install時のmysqlのエラー対応とか参考にしたコマンドとか
bundle install時のmysqlのエラーに始まり、深い沼にはまりました。というか現在も沼の中にいます。
並行してosアップデートしたせいでなんかえらいことになっています。
いろいろとコマンドいじりすぎて訳がわからなくなったので確認が取れる範囲で覚え書きをば。書いていて落とし処がわからなくなってきたので、また整理するか、要点だけ改めてまとめるかもしれません。
エラー内容
ターミナルGem::Ext::BuildError: ERROR: Failed to build gem native extension. (中略) Make sure that `gem install mysql2 -v '0.5.2' --source 'https://rubygems.org/'` succeeds before bundling.
やったこと
まず指摘されたコマンド打ってみた → 解決せず
gem install mysql2 -v '0.5.2' --source 'https://rubygems.org/'バージョン変更で解決するよ!(バージョンが違うよ!)って記事をいくつかみたので実行(下記のバージョン記述変えたり) → 解決せず
Gemfilegem 'mysql2', '>= 0.4.4'このへんで最初のターミナルの中に
ターミナルYou have to install development tools first.
って書いているのに気づいた。なんだ、これで調べれば解決じゃん! → 解決せず。
ちなみに 「 xcode-select --install 」ってコマンドが調べれば出てきます。
これで解決してる人いっぱいいるのに...。
そもそもxcodeって触り始めたくらいの時に入れてるっぽいので入ってるよ!って思いましたが、os更新した後とか、このコマンド入れると解決したりするみたいです。OS更新した時とかに出番ありそうなのでメモ。とっても役にたったコマンド
こちらも自分の覚え書きですが。
brew doctorbrew doctorすると、brewの設定とかで不具合がある場合とかに忠告が出るようです。
Warning: You have unlinked kegs in your Cellar. Leaving kegs unlinked can lead to build-trouble and cause brews that depend on those kegs to fail to run properly once built. Run `brew link` on these: pkg-config libtool ilmbase little-cms2 libpng heroku openjpeg libde265 webp ruby-build yarn xz openexr nodebrew heroku-node pcre jpeg telnet上の場合だと、brew linkしろっていってるのでそのまま実行したら解決しました。(この問題については)
Ken@MacBook-Pro ~ % brew link pkg-config Linking /usr/local/Cellar/pkg-config/0.29.2... 4 symlinks created Ken@MacBook-Pro ~ % brew link libtool Linking /usr/local/Cellar/libtool/2.4.6_1... 20 symlinks created Ken@MacBook-Pro ~ % brew link ilmbase Linking /usr/local/Cellar/ilmbase/2.3.0... 18 symlinks createdひとまず
まだ道半ばですが、ひとまず備忘録として重要そうな部分を書いておきます。
今のところ、mysqlのバージョンの差異によるもの、パスの設定ミス、もしくは少し前に設定をいじってしまったと思われる権限関係のミスと思われます。
(AWSデプロイの際にbundle installができなくなり、その時はrbenvあたりの権限が変わってしまっていたことが原因でした。)
バージョン違いの解決策で解決せず、合間に何をトチ狂ったのかOSアップデートしてしまい他のエラーがわんさか出ているので、ひとまずbrewをきれいにするところからかなぁと。参考にさせていただいた先人たち
(他にもいらっしゃいましたが数が多すぎるので特に参照した方々を)
https://qiita.com/tktcorporation/items/0ef8c930fc18ce72c301
https://qiita.com/Yuki-k-lion/items/82a4e0490e9ed38ce545
https://qiita.com/motofumi/items/0f2e7ae1b852f118fe95
https://qiita.com/kota-es/items/98ae6ee84fc59aaae2ea
- 投稿日:2019-12-11T20:11:37+09:00
ユーザのプロフィール画像を追加する方法(devise使ってる前提)
ImageMagick(画像変換ツール)の導入
$sudo yum install ImageMagickmini_magicのインストール
ImageMagicをrailsアプリから使えるようにする。
Gemfilegem 'mini_magick' bundle installdeviseのクラス(今回はUser)のモデルの一番最初に以下を追加
User.rbclass User < ApplicationRecord has_one_attached :image ~ endviewファイルでform_forでプロフィール画像を追加
<%= f.file_field :image, class: "form-control floating-label", placeholder: "画像" %>viewファイルで画像の表示
<div class="image" style="background-image: url(<%= rails_blob_path(@user.image) %>) "></div>
- 投稿日:2019-12-11T20:03:55+09:00
【プログラミング歴1年】初心者が全力でアプリを作ってみる。
はじめに
自己紹介
未経験の23歳からIT教育関係に転職し、2019の年末でプログラミング歴が1年になります。
副業でスタートアップの企業にも参加して色々教えてもらってきました。しかし、仕事が忙しくポートフォリオを作ろうとしても途中で辞めてしまう事が多く・・・
そうだ人生初!ポートフォリオを作成してまとめよう
という事で1〜10までの足跡を記事にしていきたいと思います。ベテランエンジニアの方からは見苦しい内容かもしれませんが、
アドバイス頂けると幸いです
!!!※半年後の完成を描いています
目次
目次は随時更新します!!
(最終更新日2019/12/11)
アプリケーションについて
名前
未定
目的
新しい物語の伝え方を開拓する。
概要
写真家/イラストレーター
と小説家
のマッチングアプリ供給できるもの
- 写真やイラスト付きの
イメージ膨らむ小説
写真やイラストと小説の相乗効果
を生んだ新しい作品- 新感覚の小説の楽しみ方
- クリエイターの活躍機会を増やす
ペルソナ
写真家/イラストレーター,小説家
- 写真/イラストが好き
- 小説が好き
- 他人に見てもらう機会がない
- いい作品ができても披露する機会がない
- 投稿日:2019-12-11T19:38:51+09:00
(備忘録・思い出)railsでページネーションを手作りする
ほとんどの人が見たことあると思います。記事系のサイトだと必ずいるやつ。今回はこれを手作りします!
posts_controller.rbdef index @current_page = params[:page].nil? ? 1 : params[:page].to_i @max_page = (Post.all.size.to_f / 3).ceil @posts = Post.all.order(created_at: :desc).limit(3).offset(3 * (@current_page -1)) @pagenation = {} if @max_page == 0 @current_page = 1 elsif @current_page > @max_page && @max_page != 0 @current_page = @max_page end @pagenation['<<'] = 1 if @max_page >= 5 && @current_page >= 4 if @max_page >= 4 && @current_page >= 3 if @current_page == @max_page @pagenation['<'] = @current_page - 3 else @pagenation['<'] = @current_page - 2 end end if @max_page >= 0 && @max_page == @current_page @pagenation.merge!({@current_page - 2 => @current_page - 2, @current_page - 1 => @current_page - 1, @current_page => @current_page}) else @pagenation.merge!({@current_page - 1 => @current_page - 1, @current_page => @current_page }) end delete_list = [0, -1] @pagenation.delete_if do |key, value| delete_list.include?(value) end if @current_page == 1 && @max_page >= 3 @pagenation.merge!({@current_page + 1 => @current_page + 1, @current_page + 2 => @current_page + 2 }) elsif (@current_page == 1 && @max_page == 2) || (@current_page != 1 && @max_page != @current_page) @pagenation.merge!({@current_page + 1 => @current_page + 1 }) end if @max_page >= 4 && @current_page == 1 @pagenation['>'] = @current_page + 3 elsif (@max_page >= 4 && @current_page != 1 && @max_page - @current_page >= 2) @pagenation['>'] = @current_page + 2 end @pagenation['>>'] = @max_page if @max_page >= 5 && @max_page - @current_page >= 3 endこの記事を読んでわかること/感じるかもしれないこと
・kaminariというgemを使わずページネーションをどうやって作るのか理解できる(ほとんどタイトル通り・・・。)
・手作り感が味わえる(きっと楽しいはず!!)気をつけるポイント
・ところどころ寄り道します。無駄な話はしませんので許してください? 寄り道の際には「寄り道します!」と一声かけます
・文才がないので読みにくいかも
・コードが汚いかもしれません(確実に汚い)
・分量が長すぎるくらい長いこの記事を書くに至った経緯
おそらく皆さんも自分の書いたコードに思い出があると思います。僕の場合は、ページネーションを作成しようとしていたある日、どこの記事を読んでも、「gemを使ってページネーションを実現してみた」といったものが多く、「gem!gem!gem!ってうるさい!えええいgemを使わず手作りしてやる!」とカップラーメンを食べながら心に決めた瞬間でした(「gemを使ってページネーションを実現してみた」系の記事をあげている方すみません。)
そんな思い出があるコードをもう一度見てみると、???となる部分があり、自分の為にも、皆さんのためにも記事を書くことにしました。どんなページネーションを作成するのか
ポイント
①「<<」をクリックすると一番最初の記事、つまり1ページ目に飛ぶ。「>>」をクリックすると一番最後の記事に飛ぶ。
②「<」をクリックすると、表示されていないページ数の1つ前のページに飛ぶ。「>」をクリックすると、表示されていないページ数の1つ後のページに飛ぶ。例えば、現在のページ数が4ページ目であった場合、
<< < 3 4 5 > >>
「<」をクリックすると、2ページ目に飛ぶということです。(3ページ目ではないのであしからず、、、。)、反対に、「>」をクリックすると6ページ目に飛びます。
③最大のページ数が5ページだった場合、6ページ目は存在しないので、「>>」「>」は共に表示しない。反対に、現在のページ数が1ページ目であった場合は、0ページ目は存在しないので「<<」「<」は表示しない。
④「<<」と「<」のどちらをクリックしても、同じページに飛ぶ場合は、「<<」は表示しない。反対に、「>>」と「>」のどちらをクリックしても、同じページに飛ぶ場合は、「>>」は表示しない。
例えば、現在のページ数が3ページ目であった場合、「<<」「<」は共に1ページ目を表すので
<< < 2 3 4 > >>
こうではなく、
< 2 3 4 > >>
このように表示します。
⑤現在のページ数を表示されているページ数の真ん中に置きます。qiitaのページネーションがわかりやすいはずです。
※注意点として、最大のページ数が6ページだった場合、6ページ目を真ん中には置きません(次ページがないため)。同様に1ページ目も真ん中に置きません(0ページ目は存在しないため)。文字ばかりで辛い、、、。と思われた方もいると思うので、gifを作成しました。
最大ページ数が3ページの時の挙動
最大ページ数が4ページの時の挙動
最大ページ数が5ページの時の挙動
最大ページ数が6ページの時の挙動
この動きと、5つのポイントと照らし合わせて、全体の理解をしていただけれたらと思います?
それでは作成!
ここからがワクワクする部分。まずルーティングを定めます。routes.rbに以下を記述しました。
routes.rbget 'posts/:page/index' => 'posts#index', as: :posts_page_index表示をするだけなので、「get」を指定し、postsコントローラーのindexアクションを呼んでいます。また重要な部分は、「posts/:page/index」のところです。この部分の「:page」で現在のページ数を取得します。
例えば、URL上で「posts/1/index」なら1ページ目、「posts/4/index」なら4ページ目といった具合です。
ここまできたら次はpostsコントローラーのindexアクションの記述に移ります。
必要な変数などを定義します。
①現在のページ数を表す「@current_page」@current_page = params[:page].nil? ? 1 : params[:page].to_i記述方法に少し、??が浮かぶかもしれませんが、「三項演算子」というのを使っています。(響きがまたカッコいい)
三項演算子は、if文を短く記述できる優れものです。文法はこんな感じです。条件文 ? trueの場合 : falseの場合
params[:page].nil?が条件文で、trueの場合は@current_pageは、1になり、falseの場合は、posts/:page/indexの「:page」にある値になります。params[:page]でposts/:page/indexの「:page」を取得しているわけです。
params[:page].nil? と条件を置いている理由は、:pageがnilの場合を避ける為です。nilであったらきちんと1ページ目に飛ばしてあげようという算段です。さらに、重要な点は、paramsで受け取った値は文字列ですので、きちんと数値にしてあげる必要があります。なので、params[:page].to_i とし、数値に直しています。(iはintegerの略)
②最大ページ数を表す「@max_page」
@max_page = (Post.all.size.to_f / 3).ceilここでは、最大ページ数を取得します。取得の仕方が面白い!
全体の投稿数が10であるとして、1ページにつき3つ投稿を表示するとしましょう。そうすると、最大ページ数は、4ページになるはずです。3/3/3/1 といった具合ですね。そのように考えると、10/3 となり、3.33333・・・なので、小数点以下は値を切り上げてあげれば良いはずです。つまり
⑴全体の投稿数を、表示したい値で割る
⑵小数点以下を切り上げる
これらが必要になります。⑴ですが、Postモデルがあるとしてそこから、Post.all.sizeで全体の投稿数を取得しています。次にto_fをつけて少数の値にしてあげます(fはfloatの略)。なんでこんなことをするかというと、割った際にきちんと小数点以下を出してあげるためです。モデルから全体の数を取り出す時、その値は必ず整数になるはずです。3人のユーザーが一人1記事投稿して、全体の総数が2.8などになるはずはありません。3のはずです。しかしそうなると、割った際に小数点以下は無視されてしまいます。整数型は小数点以下を無視するからです。
そこでto_fをつけると、3は3.0となるんです。?
次は⑵ですが、この部分に当たるのはceilです。ceilは小数点以下を切り上げます。
1.2でも切り上がって2になります。-----ここで寄り道します!-----
小数点を扱うメソッドとしてceilの他に、round、floorなどが存在します。
ceil → 小数点以下を切り上げ
floor → 小数点以下を切り捨て(1.8は1になります)
round → 小数点以下を四捨五入今回は、小数点以下は必ず切り上げて欲しいので、ceilを利用します。
-----寄り道終わり-----よって@max_pageは最大ページ数が入ることになります!
③viewで用いる「@posts」
@posts = Post.all.order(created_at: :desc).limit(3).offset(3 * (@current_page -1))この部分ですが、viewのところで説明します。
④空ハッシュな「@pagenation」
空ハッシュを用意します。簡単に説明すると、この空ハッシュに、ketとvalueの1セットをたくさん入れていき、あとはviewの方で取り出していきます。
-----ここで寄り道します!-----
ハッシュについて聞きなれない方もいると思うので、説明します。「keyとvalueを1セットにして管理する」ハッシュという仕組みには、言語によって様々な呼び方があります。(あーあのことかと思う人もいるはず。。。)ruby ・・・ハッシュ
php ・・・連想配列
swift ・・・ディクショナリー
などなど響きがかっこいいのは、連想配列です。でも僕はディクショナリーの方がしっくりします。というのもkeyとそれに紐づけられたvalueという形はまさに辞書(ディクショナリー)らしいからです。
keyとvalueの関係とは、
fruits = {"apple": "100円", "orange": "80円", "melon": "500"}このような感じです。"apple"がkeyであり、それに"100円"というvalueが結びついています。配列よりも優れている点は、「順番を気にしなくてもいい」というところです。配列だと先頭が0になるので、順番によって取り出す値が変わってしまいます。しかしハッシュはkeyにvalueが結びついているので、それがどの順番であろうとも、keyを指定すれば取り出すことができます。(今回はview内で@pagenationを先頭から取り出していく為、その利点は失われていますが)
また、rubyには「シンボル」というものが存在します。:pageみたいに先頭に「:」がついたやつです。ここの部分を少しまとめます。シンボルの利点は以下になります。
①表面上は文字列なので、プログラマが理解しやすい
②内部的には、整数で管理されるので、コンピュターは高速に値を比較できる
③同じシンボルは同じオブジェクト(IDが一緒)であるので、メモリの使用効率がいい
④イミュータブルなので、勝手に値を書き換えられない(破壊的なオブジェクトメソッドが効かない)またシンボルには様々な形があります。
①基本構文
“日本” => “japan”②keyをシンボル化
:日本 => “japan”③=>を:で代用する。この時の:はシンボルではない。また、keyの:は省略される
日本 : “japan”④valueをシンンボル化
日本 : :japan特にrailsでは、「:key」の形が多く見受けられる。
-----寄り道終わり-----
こうしてコントローラー内で重要な要素、
①現在のページ数を表す「@current_page」
②最大ページ数を表す「@max_page」
④空ハッシュな「@pagenation」
が出揃いました!なので、コードを上から順番に説明したいと思います。if文が多いので、それを1つのワンブロックと考えて説明します。
※@current_pageも@max_pageも「ページ数」であることを念頭に置いてくださいね?if @max_page == 0 @current_page = 1 elsif @current_page > @max_page && @max_page != 0 @current_page = @max_page endまず、@max_pageが0である場合は、@current_pageを1にします。次に、@max_pageが0でなく、かつ@current_pageが@max_pageよりも大きい場合@current_pageは@max_pageの値にします。
例えば、@max_pageが6である時、ユーザーがいたずらでURLをposts/100/index
とした場合、100ページ目はありません。この時@current_pageは@max_pageを超えてしまうので、@current_page = 6になるようにして調整しています。
@pagenation['<<'] = 1 if @max_page >= 5 && @current_page >= 4空ハッシュに値を入れていきます!後ろにif文がついていますが、条件とtrueの場合のすることが逆転してるだけなので、抵抗感はありますが、形自体は理解できるはずです?
@pagenation['key'] = value の関係です。<< は1になるので、そこの部分はokなはずです。次に条件文の@max_page >= 5 && @current_page >= 4 ですが、ここは理解しにくいはずなので図で説明します!(@current_pageは赤色です)
これを見ると、@max_page >= 5でかつ@current_page >= 4の時に「<<」が出現することが分かりますね!。なのでこのような条件文にしました。
次に「<」の出現条件です。
if @max_page >= 4 && @current_page >= 3 if @current_page == @max_page @pagenation['<'] = @current_page - 3 else @pagenation['<'] = @current_page - 2 end endここはif文の中にif文を入れています(if文の入れ子)。また先ほどの図を見れば分かりやすいと思いますが、
「<」が出現する条件として、@max_page >= 4 かつ @current_page >= 3でなければなりません。その場合、@current_page == @max_pageであるならば、「<」は@current_page - 3 を意味します。
それ以外は、@current_page - 2 になります。
if @max_page >= 0 && @max_page == @current_page @pagenation.merge!({@current_page - 2 => @current_page - 2, @current_page - 1 => @current_page - 1, @current_page => @current_page}) else @pagenation.merge!({@current_page - 1 => @current_page - 1, @current_page => @current_page }) endさあここで、mergeというメソッドを使っていきます。mergeをすることで、ハッシュ同士を結合することができます。
例えば、
hash1 = {"経済学" => 80, "財政学" => 70, "会計学" => 60} hash2 = {"経営学" => 75, "会社法" => 65} hash3 = hash1.merge(hash2) p hash3 #=> {"経済学"=>80, "財政学"=>70, "会計学"=>60, "経営学"=>75, "会社法"=>65}このように、元のハッシュのお尻にハッシュが結合されていくわけです。
元のハッシュである@pagenationには、上記の条件を満たした場合、「<<」「<」というkeyがあり、それに結びついたvalueがあるはずです。ここに新たにmergeを使うことで足していきます。まず条件として、@max_page >= 0 && @max_page == @current_page を満たした場合、
@pagenation.merge!({@current_page - 2 => @current_page - 2, @current_page - 1 => @current_page - 1, @current_page => @current_page})が行われます。先ほどの図を見てもいいのですが分かりにくいかもしれないので、具体的な例を出しますね!
@max_page = 6 で @current_page = 6の場合、その条件を満たすはずです。その時、ページネーションはこのような形になります。
<< < 4 5 6
この時、4は@current_page - 2であり、5は@current_page - 1、6は@current_pageになります。
よって @pagenation = {'<<' => 1, '<' => 3, 4 => 4, 5 => 5, 6 => 6}
の形になります。@max_page = 4 で @current_page = 4の場合、<<は出現しませんから、
@pagenation = {'<' => 1, 2 => 2, 3 => 3, 4 => 4}
の形になります。@max_page >= 0 && @max_page == @current_pageを満たさない場合、例えば
@max_page = 4 で @current_page = 3の場合< 2 3 4
この時は、
@pagenation.merge!({@current_page - 1 => @current_page - 1, @current_page => @current_page })を実行します。
よって @pagenation = {'<' => 1, 2 => 2, 3 => 3} となります。(4はこの時@pagenationにありません。後ほど付け足す感じです。)
しかしこのようにしていくと問題が発生します。
@max_page >= 0 && @max_page == @current_pageを満たさない場合、@max_page = 4 で @current_page = 1の時は一体どうなるでしょう。@pagenation.merge!({@current_page - 1 => @current_page - 1, @current_page => @current_page })を実行しますから、
@pagenation = {0 => 0, 1 => 1}
になるはずです。(<< < は、それぞれの条件を満たさないのでありません。)
ですが、ページ数に0ページ目なんてものはありません。そこで重要になるのが、
delete_list = [0, -1] @pagenation.delete_if do |key, value| delete_list.include?(value) endです!目を通しただけで何をしているかわかると思いますが、delete_list = [0, -1] から、@pagenationにその値が、valueにあれば(含めば)、消去しています。
delete_ifについては、この記事が参考になったので、見ていただければ良いと思います。
https://qiita.com/zom/items/4461d786c35ae9eced0bこれで、
@pagenation = {0 => 0, 1 => 1} ↓ @pagenation = {1 => 1} になるのです!ここまでで前半部分は終了です。どうでしょうか。次は後半部分ですが、少しややこしいかもです!
@current_pageまでを@pagenationに入れてきました。後半部分では、それ以降を@pagenationに入れていきます。
次に、「>」「>>」を除く、@current_page以降、表示される数字の部分です。以下の1ブロックは、@current_page以降、数字を2つつけるべきなのか、1つつけるべきなのか条件づけています。
if @current_page == 1 && @max_page >= 3 @pagenation.merge!({@current_page + 1 => @current_page + 1, @current_page + 2 => @current_page + 2 }) elsif (@current_page == 1 && @max_page == 2) || (@current_page != 1 && @max_page != @current_page) @pagenation.merge!({@current_page + 1 => @current_page + 1 }) endここも、if文の入れ子になっていますが、まず最初の条件として
@current_page == 1 であり、かつ @max_page >= 3 の時です。この場合は、@current_page以降、数字を2つつけなけらばなりません。
図を再掲しますが、@current_pageが1であり、@max_pageが3以上の時は、2つ以上数字が並ぶことがわかると思います!
この場合、
@pagenation.merge!({@current_page + 1 => @current_page + 1, @current_page + 2 => @current_page + 2 })を行います。おなじみのmergeですね!先頭に2つつけるので、
@current_pageに+1し、さらに+2したものを結合しています。さて次に、@current_page以降に数字を1つつける場合はどうでしょう。
(@current_page == 1 && @max_page == 2) || (@current_page != 1 && @max_page != @current_page①@current_pageが1であり、かつ@max_pageが2である時、
「または」
②@current_pageが1ではなく、かつ@max_pageが@current_pageと値が異なる時です。②から見ていきましょう。というのも、①は条件が具体的でピンポイントですので、②を押さえてから、①を見た方が、理解しやすいと思うからです。
②の時は、@current_pageと@max_pageが同じであったら、次に表示される数字はありません。ですので@max_page != @current_page をつかって避けないといけません。
しかし、
@current_page != 1 && @max_page != @current_page 時の条件と、初めの
@current_page == 1 && @max_page >= 3の条件のうち、外れる条件がでてきます。それが
①@current_pageが1であり、かつ@max_pageが2である時です。この時、@current_page以降に表示される数字は、1つなので、きちんと条件づけています。
これらの条件を満たした場合、
@pagenation.merge!({@current_page + 1 => @current_page + 1 })
を行い、@current_pageに+1したものを追加してあげます。
次のブロックで、「>」をつけていきます。
if @max_page >= 4 && @current_page == 1 @pagenation['>'] = @current_page + 3 elsif (@max_page >= 4 && @current_page != 1 && @max_page - @current_page >= 2) @pagenation['>'] = @current_page + 2 end「>」が現れる時の条件と、それを満たした場合、「>」がどんな値を持つかが重要になります。
まず、
@max_page >= 4 && @current_page == 1ですが、
この時に初めて、「>」は出現します。その際当然、「>」は@current_page+3の値を持つことになりますので、
@pagenation['>'] = @current_page + 3になります。
しかし、それ以外では、「>」は@current_page+2の値を持たせねばなりません。この時の条件が
@max_page >= 4 && @current_page != 1 && @max_page - @current_page >= 2になります。&&で区切って見ていきますが、まず@max_pageが4以上である必要があります。そして、@current_pageは1であってはなりません。次に、@max_page - @current_page した場合、2以上であれば「>」は@current_page+2の値になります。
そして最後に、「>>」を見ていきます!
@pagenation['>>'] = @max_page if @max_page >= 5 && @max_page - @current_page >= 3この文法は、「<<」で見た時と同じ形ですので、大丈夫でしょう。それでは、この時、「>>」はどのような条件で出現し、どのような値をもつべきなのでしょうか。
まず、
@max_page >= 5 && @max_page - @current_page >= 3が、条件文になりますが、@max_pageが5以上である時にしか「>>」は出現しません。また、@max_page - @current_page >= 3でない限り、「>>」は出現してはいけません。というのも
「<<」と「<」のどちらをクリックしても、同じページに飛ぶ場合は、「<<」は表示しない。反対に、「>>」と「>」のどちらをクリックしても、同じページに飛ぶ場合は、「>>」は表示しない。
と決めているからです、、、。こちらは僕の勝手な都合です?
その条件を満たした場合、「>>」は最後のページに飛ぶことになりますから、最後のページ = 最大ページ数になり、それはすなわち
@pagenation['>>'] = @max_pageで表すことができます!
これで@pagenationには、その時々の値を持つことができています!
@max_page = 3 で@current_page = 2の時は、 @pagenation = {1 => 1, 2 => 2, 3 => 3} @max_page = 6 で@current_page = 3の時は、 @pagenation = {'<' => 1, 2 => 2, 3 => 3, 4 => 4, '>' => 5, '>>' => 6}といった具合です!
さて次に@pagenationと、先述した@postsを携えて、viewの方で使っていきましょう!!
index.htmk.erb<div> <% unless @max_page <= 0 %> <ul> <% @pagenation.each do |key, value| %> <li class="<%= ' active' if value == @current_page %>"> <%= link_to key, posts_page_index_path(value) %> </li> <% end %> </ul> <% end %> </div>まず、@max_page <= 0 でない時に、ページネーションを表示させてあげています。
unlessはifと反対の意味を持ち、if文が「〜である時」なのに対して、unlessは「〜でない時」を表します。
そして、@pagenationをeach文で回し、それぞれkeyとvalueを取り出していきます!
<li class="<%= ' active' if value == @current_page %>">ここのclassについてですが、@current_pageであれば、そのページネーションの表示の色を、他のページ数で表示されている色と変えてあげたいはずです。ですので、このように
if value == @current_page とすることで、valueと@current_pageの値が等しければ、activeクラスが付与されて、そこをcssで変えることができるという流れになっています。
また、keyの部分に関しては、aタグをつけたいので、link_toを用いて、そのvalueそれぞれにリンクを貼ります。
posts_page_index_path(value)
となっているのは、ルーティング設定の時に、
routes.rbget 'posts/:page/index' => 'posts#index', as: :posts_page_indexと、asで名前を変更しているからです。当然、posts_page_indexだけでは、どのページ数か判別できないので、posts_page_index_path(value)と(value)をつけています。(pathもつけることもお忘れなく、、、。)
そして最後に、コントローラー内で登場した、
@posts = Post.all.order(created_at: :desc).limit(3).offset(3 * (@current_page -1))を説明します。
@max_page = (Post.all.size.to_f / 3).ceilで、「最大ページ数」を取得することはできました。しかし実際に、「取り出す順番」であったり、「取り出す投稿」は取得していません。
少し疑問に思われる方もいると思うので、踏み込んで説明します。
@max_pageでは、「1ページにつき、最大3投稿表示できるとしたら、最大ページ数はいくつか」を取得したものであり、1ページごとに、どの順番で、どの投稿を表示させるのかは取得できておらず、あくまでその全体の枠組みだけです。ですので、@postsでそれらを取得しています。もちろん、@max_pageと対応させなければ、なりません。もし、全体の投稿数が10で、1ページにつき、投稿を3つまで表示できるとしたら、@max_page = 4になるはずですが、@postsの方で、1ページあたり4つ投稿を取得するとしたら、@max_pageとずれてしまうからです。今回は1ページにつき最大3つ投稿を表示できるようにしていますから、それに合わせます。
@posts = Post.all.order(created_at: :desc).limit(3).offset(3 * (@current_page -1))まず、Post.allでモデルからデータを引っ張ってきます。その時、
①取り出す順番
②取り出す投稿を指定しています。①については、order(created_at: :desc) とすることで、作成された日付順に投稿を取得します。日付が新しい投稿を先に取得していくわけです。
そして②です。
limit(3).offset(3 * (@current_page -1))limitで取り出す投稿の最大数を取得します。当然3つまで投稿を取得して欲しいので、limit(3)です。そして次に@current_pageで取得した、ページ数に合わせて、取得する3つの投稿数を変更させます。それが
offset(3 * (@current_page -1))
にあたる部分です。offsetメソッドは、指定した位置からデータを取得します。例を出すと
12/10
12/9
12/8
12/7
12/4
12/3
11/20
11/18
11/16というようなデータ(降順で表示)があった場合、limit(3).offset(0)なら、
12/10
12/9
12/8が取得され、limit(3).offset(3)なら、
12/7
12/4
12/3が取得されます。なんとなく雰囲気はつかめるはず?
なので、@current_pageとoffsetを組み合わせて、@current_pageが1なら(つまり1ページ目)
offset(3 * (@current_page -1))
↓
offset(3 * (1 -1))
↓
offset(3 * 0)
↓
offset(0)になるといった具合です。
これで、@postsには、その@current_pageに合わせて、投稿を@max_pageに対応させながら取得することに成功しました!
あとは、viewで表示する際に、
index.html.erb<% @posts.each do |post|%> 省略 <% end %>とすればいいでしょう!
終わりに
短く書くつもりが、あれもこれもとなってしまい、非常に読みにくくなってしまったかもしれません。?
重複するコードや説明の間違いなどあるかもしれません。ですが、少しでも手作り感を味わっていただければ幸いです。
「あれ、これってどうだったっけ?」というのが、取り除かれてスッキリしました!それでは!参考
「プロを目指す人のためのRuby入門」伊藤淳一、技術評論社、2019年
配列の複数要素の削除はdelete_ifかreject、-なんかを使おう
Rubyのmergeメソッドでハッシュを結合する方法【初心者向け】
kaminariではないバニラなページネーションをやっていく
- 投稿日:2019-12-11T19:03:47+09:00
【Ruby】Discogs::Wrapper でアーティスト情報とかもろもろを取得する
discogsでアーティスト名、ジャンル、リリースされたトラックの情報を取得したい。
discogs APIのwrapperを利用して適宜ロジック等を記載していく。ドキュメント等
discogsについて
https://ja.wikipedia.org/wiki/Discogs公式
https://www.discogs.com/ja/gem
https://github.com/buntine/discogsアクセストークンの取得
wrapperを使う前にアクセストークンを取得する。
googleアカウントでdiscogsにサインアップ後、User Tokenを取得する。個人的に利用する分には当面はUserTokenで良さそう。
手順
使ってみる
- 投稿日:2019-12-11T18:45:45+09:00
Railsチュートリアル 第12章 パスワードの再設定 - 「ログイン画面に、パスワード再設定用のリンクが存在することに対するテスト」に対するテストを実装する
テストコード
変更対象となるテストコードは
test/integration/users_login_test.rb
です。test/integration/users_login_test.rbclass UsersLoginTest < ActionDispatch::IntegrationTest ...略 test "login with invalid information" do get login_path assert_template 'sessions/new' + assert_select "a[href=?]", new_password_reset_path post login_path, params: { session: { email: "", password: ""} } assert_template 'sessions/new' assert_not flash.empty? assert_select 'div.alert-danger' get root_path assert flash.empty? end ...略 end
テストを実行してみる
この時点でテストは成功します。
# rails test Running via Spring preloader in process 14842 Started with run options --seed 21965 46/46: [=================================] 100% Time: 00:00:04, Time: 00:00:04 Finished in 4.81443s 46 tests, 198 assertions, 0 failures, 0 errors, 0 skipsこのテストは、どうすれば失敗するようになるのか
例えば以下のように
app/views/sessions/new.html.erb
を変更すると…app/views/sessions/new.html.erb<% provide(:title, "Log in") %> <h1>Log in</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(:session, url: login_path) do |f| %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> - <%= link_to "(forgot password)", new_password_reset_path %> + <%# <%= link_to "(forgot password)", new_password_reset_path %> %> <%= f.password_field :password, class: 'form-control' %> <%= 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 %> <p>New user? <%= link_to "Sign up now!", signup_path %></p> </div> </div>以下のようにテストが失敗します。
# rails test test/integration/users_login_test.rb:12 Running via Spring preloader in process 14868 Started with run options --seed 5918 FAIL["test_login_with_invalid_information", UsersLoginTest, 2.484278399962932] test_login_with_invalid_information#UsersLoginTest (2.48s) Expected at least 1 element matching "a[href="/password_resets/new"]", found 0.. Expected 0 to be >= 1. test/integration/users_login_test.rb:12:in `block in <class:UsersLoginTest>' 4/4: [===================================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.48610s 1 tests, 2 assertions, 1 failures, 0 errors, 0 skips「/password_resets/new へのリンクが1つも描画されていない」という趣旨のメッセージですね。想定したテストが正しく実装されていると言えそうです。
- 投稿日:2019-12-11T18:45:45+09:00
Railsチュートリアル 第12章 パスワードの再設定 - 「ログイン画面に、パスワード再設定用のリンクが存在すること」に対するテストを実装する
テストコード
変更対象となるテストコードは
test/integration/users_login_test.rb
です。test/integration/users_login_test.rbclass UsersLoginTest < ActionDispatch::IntegrationTest ...略 test "login with invalid information" do get login_path assert_template 'sessions/new' + assert_select "a[href=?]", new_password_reset_path post login_path, params: { session: { email: "", password: ""} } assert_template 'sessions/new' assert_not flash.empty? assert_select 'div.alert-danger' get root_path assert flash.empty? end ...略 end
テストを実行してみる
この時点でテストは成功します。
# rails test Running via Spring preloader in process 14842 Started with run options --seed 21965 46/46: [=================================] 100% Time: 00:00:04, Time: 00:00:04 Finished in 4.81443s 46 tests, 198 assertions, 0 failures, 0 errors, 0 skipsこのテストは、どうすれば失敗するようになるのか
例えば以下のように
app/views/sessions/new.html.erb
を変更すると…app/views/sessions/new.html.erb<% provide(:title, "Log in") %> <h1>Log in</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(:session, url: login_path) do |f| %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> - <%= link_to "(forgot password)", new_password_reset_path %> + <%# <%= link_to "(forgot password)", new_password_reset_path %> %> <%= f.password_field :password, class: 'form-control' %> <%= 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 %> <p>New user? <%= link_to "Sign up now!", signup_path %></p> </div> </div>以下のようにテストが失敗します。
# rails test test/integration/users_login_test.rb:12 Running via Spring preloader in process 14868 Started with run options --seed 5918 FAIL["test_login_with_invalid_information", UsersLoginTest, 2.484278399962932] test_login_with_invalid_information#UsersLoginTest (2.48s) Expected at least 1 element matching "a[href="/password_resets/new"]", found 0.. Expected 0 to be >= 1. test/integration/users_login_test.rb:12:in `block in <class:UsersLoginTest>' 4/4: [===================================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.48610s 1 tests, 2 assertions, 1 failures, 0 errors, 0 skips「/password_resets/new へのリンクが1つも描画されていない」という趣旨のメッセージですね。想定したテストが正しく実装されていると言えそうです。
- 投稿日:2019-12-11T17:30:48+09:00
Rails+MySQL+Docker+AWS(EC2, RDS, ALB, Route53, S3)で作成したポートフォリオについて
記事の概要
私が作成したポートフォリオ、「GoodCoffeeByGoodBarista」を解説します。
なぜ作ったか、どう作ったか、今後どうしていくかをまとめました。実際に作成したサイトやソースコードは下記のリンクからご覧いただけます。
GoodCoffeeByGoodBarista
GitHubこちらの記事の書き方は下記の記事を参考にさせていただきました。
PHP+MySQLでポートフォリオ作成なぜ作ったか
私は、愛知県名古屋市にある個人経営のカフェで、2年半ほどバリスタとして勤務しておりました。
ある時、バリスタの大会に出場したのですが、結果は惨敗でした。
バリスタは、生豆の仕入れからロースト、抽出までを全て個人で行い、それをプレゼンするのですが、超小規模店舗で勤務していた私は、生豆の仕入れルートもなければ高価な焙煎機もない、大会で使われる最新のエスプレッソマシンもない、練習に使うミルクも何百本と自腹で用意するしかない環境下で戦うしかありませんでした。
対して、大きな企業が経営するカフェに勤めているバリスタは、会社が持っているルートから最高品質の生豆を仕入れ、会社が用意する材料を使って、最新のエスプレッソマシンで毎日練習ができるのです。私は、環境の差による大きな挫折を味わいました。
バリスタを辞め、エンジニアになるべく勉強をしていた私は、「エンジニアリングの力で、バリスタ業界を良くできないか」と考えるようになりました。
エンジニアのように気軽に転職をする事ができる文化があれば、スキルを身に付けたいバリスタはより大きな企業に転職できるし、大企業にいてスキルはあるけれどもっと個人の店舗で接客を身に付けたいバリスタの願いも叶えられる。
バリスタが、自分の所属する環境でスキルを身に付けられなかったり、夢を諦めたりしなくて良くなるのではないか。
そんな思いから、バリスタ版Wantedlyのようなサービスを作成し、私のポートフォリオとすることにしました。スペック
言語
Ruby 2.5.3
フレームワーク
Ruby on Rails 5.2.2
CSSフレームワーク
Bootstrap4
データベース
MySQL 5.7
WEBサーバ
Nginx 1.15.8
開発環境
Docker 19.03.5
docker-compose 1.21.1バージョン管理
Git 2.24.0
本番環境
AWS (EC2, RDS, ALB, Route53, S3)
主な機能
・バリスタ(カフェ)一覧表示
バリスタの一覧、またはカフェの一覧を表示します。
ログインしていなくてもアクセスできる仕様です。・バリスタ(カフェ)を検索
バリスタの場合、性別で検索できます。
カフェの場合、店舗名、雇用形態、所在地で検索できます。
・バリスタ登録
バリスタはトップページから新規作成のリンクを踏むことでユーザー登録ができます。
入力内容は最低限必要な内容のみです。
入力に誤りがあれば、登録はされずエラ〜メッセージが表示されます。・オーナー登録
オーナーはナビゲーションにある「採用担当者の方」というところからオーナー用のトップページに移動し、そこから新規作成画面に移動できます。
入力内容は最低限必要な内容のみです。
入力に誤りがあれば、登録はされずエラ〜メッセージが表示されます。・バリスタプロフィール編集
登録では基本情報のみの入力なので、プロフィール編集で詳細な情婦を入力していきます。
上記のリンクから自分のプロフィールを確認しながら編集できます。・オーナーカフェ情報編集
登録では基本情報のみの入力なので、プロフィール編集で詳細な情婦を入力していきます。
上記のリンクから自分のプロフィールを確認しながら編集できます。・ログイン
ログインできます。
バリスタとオーナーではフォームが分けてあります。・面談したい(面談に誘いたい)バリスタ(カフェ)に対してメールを送信
ログインしている状態で、気になるバリスタ(カフェ)の詳細ページから面談を申し込む(誘う)内容のメールを送る事ができます。
・退会
プロフィール編集の画面から退会できます。
開発手順
1.要件定義
今回作成するアプリに必要な機能は、
・バリスタユーザー登録機能
・オーナーユーザー登録機能
・ユーザー一覧表示機能
・ユーザー検索機能
・応募プロフィールや、求人情報作成機能
・面談応募(勧誘)機能のため、ユーザー情報を保存しておくデータベースが必要であり、尚且つデータベースに保存した情報を動的に表示できるビューが必要です。
また、面談の応募(勧誘)にはメイラー機能を使いたいので、メール用のサーバーも用意する必要があります。
バリスタユーザーとオーナーユーザーで動線を分けたいので、わかりやすい動線づくりを心がけます。
2.環境選定
言語は、自分にとって技術的資産の多いRubyを選択しました。
よってフレームワークもRuby on Railsとしました。データベースは、もっともメジャーに、広く使用されているMySQLを選択しました。
今回はバックエンド開発がメインだったため、フロントエンド開発の工数を減らす目的でCSSフレームワークを使用しました。
CSSフレームワークにはネット上に公開されている情報のリソースが多いことから、Bootstrapを使用しました。WEBサーバーには、pumaとの連携が簡単で、かつネット上に情報のリソースが多かったNginxを使用します。
なお、これらの環境はDocker,docker-composeを使用して構築しています。
Dockerを使用したのは、最終的にCircleCIやcapistoranoを用いた自動テスト&ビルド&デプロイを行いたいと考えているためです。そして、本番環境はAWSのEC2,RDS等を用いて構築します。
これは、個人的にAWSに興味があったため使ってみたかったのと、転職用のポートフォリオとして使用する際、クラウドにAWSを取り入れている企業様が多いと感じたことから、技術アピールができると考えたからです。3.データベース設計
今回は、2種類のユーザーを作成する必要があるため、ユーザーモデルを一つ作成し、オーナーフラグがtrueかfalseかでユーザーを識別するか、ユーザーモデルを二つ作成してそれぞれで管理するかで悩みましたが、
今回は後者の方法でデータベースを作成することにしました。
理由は、私が参考にしている著書「達人に学ぶDB設計徹底指南書」にて、一つのモデルはなるべくシンプルにし、分けられるところは分けて管理するのが良いとされていたので、その教えを守る形にしました。4.コーディング
コーディングの際に注意した点は以下の通りです。
・一つの機能を実装する度にRSpecでSystemスペックを記述する
・GitHubFlowを意識した開発(マスターブランチでの作業は基本的にはしない、擬似的にプルリクエストを作成して、マスターブランチにマージする -> リモートリポジトリの変更を、ローカルにpullする)また、ユーザーをバリスタユーザーとオーナーユーザーに分けて実装していましたが、自身の練習もかねて、バリスタユーザーの登録や認証周りをdeviseで実装し、オーナーユーザーの登録はscaffoldで作成、認証は簡易的なsessionで実装しました。
ただ、ログイン状態のバリスタユーザーをcurrent_userで取得し、ログイン状態のオーナーユーザーをcurrent_ownerで取得するように実装したのですが、これはベストプラクティスではないように感じました。
次回また似たようなアプリケーションを作成する際は、この辺りのDB設計に関してきちんと見直す必要がありそうです。この段階で、動作確認も兼ねてdevelopment環境でもAWSのS3に画像が保存されるように実装しました。
5.デプロイ
本番環境はAWSで構築しました。
docker-composeでアプリケーション用のコンテナ、Nginxのコンテナ、メイラーのコンテナを用意していたので、それらをまとめてEC2でビルド&実行するようにしました。
データベースはRDSを使用しています。
画像の保存には、S3を使用しており、こちらはproduction環境だけでなく、development環境でもS3に保存するようにしています。
また、今回はEC2は一つしか用意していませんが、後々HTTPS化するのに必要だったので、ALBを配置しました。
独自ドメインは、過去にブログを運営していたときにも利用していて使い慣れたお名前.comから取得し、Route53で設定しました。AWSを使ったデプロイのために何冊も書籍を読み、様々な記事を読んだので、ネットワーク周りの知識がかなりついたと実感しました。
今後の改善点や追加実装について
アプリケーション自体としては、SNSログインや画像アップロードの際のプレビュー表示などを実装したいと考えております。
加えて、特にフォーム関連のUIを向上させたいと考えています。
具体的には、入力するべき項目をplaceholderでわかりやすくしたりできるかと考えております。また、要件定義の段階から考えているCircleCIを使用した自動ビルド&テスト、capistoranoを使用した自動デプロイを実装していきたいです。
そのためにも、現在はCircleCIの公式ドキュメントを読みながら準備をしている段階です。それから、AWSに関しても、画像の配信をCloudFlontで行うことで、より高速化を測ってみたいと思います。
参考文献
・Ruby on Rails5.2 速習実践ガイド
・プロを目指す人のためのRuby入門
・Docker/Kubernetes 実践コンテナ開発入門
・Amazon Web Services 基礎からのネットワーク&サーバー構築 改訂版
・ゼロからわかる Amazon Web Services超入門 はじめてのクラウド かんたんIT基礎講座
・Webを支える技術 ―― HTTP,URI,HTML,そしてREST
・リーダブルコード
・達人に学ぶDB設計 徹底指南書
- 投稿日:2019-12-11T16:46:02+09:00
画像アップロード時に「Missing required arguments: aws_access_key_id, aws_secret_access_key」エラーが発生する
プログラミングスクールのカリキュラムでメルカリのクローンサイトを開発中、商品の画像をデータベースに保存しようとしたところ、タイトルのエラーが発生。
検索しても、該当する記事が見当たらなかったので記事にしました。
ローカルで画像を保存しようとしてるだけだし、AWSとか関係ないだろーと思って解決方法を模索していましたが、辿り着いた答えは単純でした。
画像をアップロードするためにCarrierWaveを使用していたのですが、「image_uploader.rb」の記述に問題がありました。エラーの原因
image_uploader.rb# Choose what kind of storage to use for this uploader: # storage :file storage :fogstorage :fogが適用されているせいでCarrierWaveの保存先がS3になっていました。
解決方法
storage :fogをコメントアウトしてあげて、storage :fileのコメントアウトを解除してあげればエラーが無事解決します。
image_uploader.rb# Choose what kind of storage to use for this uploader: storage :file # storage :fog補足
ローカルでの作業後、CarrierWaveの保存先をS3に戻す場合は逆の修正を行えば簡単に戻せます。
- 投稿日:2019-12-11T16:18:22+09:00
ActiveStorageをcarrierwaveに置換する方法
やりたいこと
- 既にActiveStorageでのファイルアップロード機構がある
- ActiveStorageによってアップロードされているファイル群をCarrierwaveに置換したい
知識
- ActiveStorageでアップロードされているファイルは、以下でURLを特定することができます。
'https://your_domain.com' + Rails.application.routes.url_helpers.rails_blob_path(task.image, options = {only_path: true})
- carrierwaveにはURLからファイルをアップロードできる機能があります。
# 例 Galaxy.create!(name: 'Andromeda', remote_photo_url: 'http://apod.nasa.gov/apod/image/1407/m31_bers_960.jpg', address: 'next to the Milky Way')実装
上記の2つの知識を組み合わせて、ActiveStorageでアップロード済みの動画をCarrierwaveで置換していきます。
task has_one_attached image
とします。- carrierwave用として
image_file
というカラムを追加したとします09_task.rb# seedファイルで作成 Task.all.each do |task| image_url = 'https://domain.com' + Rails.application.routes.url_helpers.rails_blob_path(task.image, options = {only_path: true}) task.update!(remote_image_file_url: image_url) p '###################################' p task.title + 'の画像アップロードが完了しました' p '###################################' end上記をseed_fuファイルで作成し、Herokuで反映させたい時には以下のコマンドから実行できます。
$ heroku run rails db:see_fu FILTER=09 --app your_app_name
- 投稿日:2019-12-11T16:09:54+09:00
【Rails】Facebookでログインできるようにする
バージョン
Rails 5.1.6
ruby 2.4.0
前提
devise導入済み
herokuにデプロイする1.Gemfileの編集
gem 'omniauth' gem 'omniauth-facebook'を追加して
$ bundle2. Facebook Developerの登録
https://developers.facebook.com/
にてアクセスfacebookログインを追加
すると、右側のメニューにfacebookログインという項目が追加される
3.色々な設定
プライバシポリシーのURLを埋める
私の場合、プライバシーポリシのページを作ってなかったので適当に拝借しました本番環境では有効なOAuthリダイレクトURIの設定が必要
facebookログイン > 設定 > 有効なOauthリダイレクトURLに
...(サイトURL)/users/auth/facebook/callback
を設定4.Railsでの設定
omniauth用のカラムをuserモデルに追加
$ rails g migration add_columns_to_users provider uid name image $ rails db:migrateconfig/initializers/devise.rbDevise.setup do |c| c.omniauth :facebook, 'App ID', 'App Secret' end※ このままgitのリモートリポジトリにあげないこと
userモデルにメソッドを追加
models/user.rbclass User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :omniauthable # :omniauthable を追加 def self.form_omniauth(auth) where(provider: auth.provider, uid: auth.uid).first_or_create do |u| u.provider = auth.provider u.uid = auth.uid u.name = auth.name u.email = auth.info.email u.password = Devise.friendly_token[0, 20] u.image = auth.info.image.gsub("picture", "picture?type=large") if u.provider == "facebook" end end endコールバックの設定
config/routes.rbRails.application.routes.draw do root to: "home#index" devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' } endコントローラの設定
controllers/omniauth_callbacks_controller.rbclass Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController def facebook @user = User.from_omniauth(request.env["omniauth.auth"] if @user.persisted? sign_in_and_redirect @user, event: authentication set_flash_message(:notice, :success, kind: "Facebook") if is_navigational_format? else session["devise.facebook_data"] = request.env["omniauth.auth"].except("extra") redirect_to new_user_restration_url end end def failure redirect_to root_path end end
- 投稿日:2019-12-11T16:06:58+09:00
facebook-ruby-business-sdkで広告結果データを取得しchartkickでグラフ化
実装したいこと
- facebookページでかけた広告結果データを取得して画面描画したい
- 広告結果データ取得にはfacebook-ruby-business-sdkを使う
- グラフ描画にはchartkickを使用する
トークン取得
developer for facebookにアクセスしてアクセストークンを取得します、下記キャプチャから取得できます
画面左側メニューの「設定 > ベーシック」から「app secret」も取得できるので、これも控えておきます。
ここで取得したトークンとシークレットをcredentialsに入れておきます。
Railsアプリから呼び出す
トークンを設定できたら呼び出す準備は完了しているので、必要な記述を追加していきます。
taskテーブル
カラム名 型 name string ad_id string dataテーブル
カラム名 型 impressions integer country_name string task_id bigint
task has_many datas
タスクを登録した時に、広告結果を取得してみます。
app/models/task.rbrequire 'facebookbusiness' class Video < ApplicationRecord # facebook marketing API FacebookAds.configure do |config| config.access_token = Rails.application.credentials.dig(:facebook_ads, :access_token) config.app_secret = Rails.application.credentials.dig(:facebook_ads, :app_secret) end # callback after_save :fetch_facebook_insights def fetch_facebook_insights ad = FacebookAds::Ad.get(ad_id) insightss = ad.insights( fields: 'impressions', breakdowns: 'country', date_preset: 'this_quarter' ) insightss.all.each do |insight| data.create( country_name: insight.country, impressions: insight.impressions.to_i ) end end end地味に気を付けたい点としては、
- impressionsが
String
で返ってくるので型変換が必要- countryは
AU
などの略称で返ってくるので、UIで日本語表示したい場合は対応が必要2点目については、facebookの国コード一覧などが役に立つかと思います。
chartkickでグラフ化
READMEに沿って進めれば簡単にグラフ化できます。
https://github.com/ankane/chartkick
show.html.erb<%= bar_chart @task.datas.group(:country_name).sum(:impressions) %>
- 投稿日:2019-12-11T15:38:14+09:00
ユーザー属性によってdeviseの新規登録フォームを変える方法
実装したいこと
- ユーザー属性によって異なるフォームパーツを出力する
- 会社ユーザーであれば、会社名・email・password
- 通常ユーザーであれば、email・password
- 認証周りはdeviseで一括管理したいけれど、むやみにcontrollerやviewを増やしたくない
結論: パラメータで条件分岐
deviseのcontrollerは
users
下のものを使用するroutes.rbdevise_for :users, controllers: { sessions: 'users/sessions', registrations: 'users/registrations', passwords: 'users/passwords', confirmations: 'users/confirmations' }app/controller/users/registrations_controller.rb# GET /resource/sign_up def new super do |resource| if params[:as] == 'company' build_resource({}) self.resource.company = Company.new end end endapp/views/users/registrations/new.html.erb<% if params[:as] == 'company' %> <h2 class="has-text-weight-bold">会社アカウント作成</h2> <%= f.fields_for :company do |company_form| %> <div class="field"> <%= company_form.label :name, class: 'label has-margin-top is-inline-block' %> <span class="has-text-danger">*</span> <%= company_form.text_field :name, autofocus: true, class: 'input' %> </div> <% end %> <% end %>こんな感じで書いておけば、パラメータで
company
が渡ってきた時だけ、フォームパーツを出力することができます。app/controller/application_controller.rbbefore_action :configure_permitted_parameters, if: :devise_controller? private def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys: [company_attributes: [:name]]) endサインアップの時にパラメータを送信できるよう許可しておく必要があります。
参考: https://github.com/plataformatec/devise#strong-parameters
- 投稿日:2019-12-11T15:09:51+09:00
railsでのデータ取得方法 find,where,group by など
はじめに
railsの基本的なデータ取得方法をまとめます。
allメソッド
全ての要素を取り出すメソッドです。モデルのすべての要素を配列で取得します。
#モデル名.all #以下が例となります。 User.allfindメソッド
モデルのidをキーとして検索するメソッドです。
idに当たるレコードがない場合はエラー(ActiveRecord::RecordNotFound)を起こします。#モデル名.find(取得したいレコードのid) #以下が例となります。 User.find(1)find_byメソッド
条件にマッチしたレコードを1件だけ返すメソッドです。
条件に一致するものがない場合はnilを返します。#モデル名.find_by(検索するテーブルのカラム名: 検索したいデータ) #以下が例となります。 User.find_by(name: "たかし")whereメソッド
条件に対しての結果をすべて返すメソッドです。
条件に一致するものがない場合はActiveRecord_Relationクラスを返します。# モデル.where(検索条件) #nameがたかしのものを検索 User.where(name: "たかし") #nameがたかしかつseiがすずきのものを検索 User.find_by("name = ? and sei = ?", "たかし", "すずき")終わりに
railsで基本的なデータの取得方法について紹介しました。
条件に一致するものがないときはそれぞれ違う結果を返すことは頭に入れておきたいです!
- 投稿日:2019-12-11T15:04:40+09:00
RSpecを学ぶ part1
RSpecを使えるように設定
RSpec は Rails ア リケーシ ンにデ ォルトでは含まれていないため、まずインストールする必要
があります。RSpec をインストールするためには Bundler を使います。group :development, :test do gem 'rspec-rails', '~> 3.6.0' # Rails で元から追加されている gem は省略 endbundle install
次に、テスト用のdbが存在するかどうかを確認する
sqliteの場合、
config/database.ymltest: <<: *default database: db/test.sqlite3このように記載されているはず
これで、アプリケーションにspecフォルダを追加して、RSpecの基本的な設定ができるようになりました。
次のコマンドを使って、RSpecをインストールしようrails generate rspec:install
このコマンドにより
Running via Spring preloader in process 28211 create .rspec create spec create spec/spec_helper.rb create spec/rails_helper.rb4つのファイルが作成される。
次にRSpecの出力をデフォルトからドキュメント形式にしてみよう
.rspecファイルに、/sampleapp/.rspec--require spec_helper --format documentationとしよう。
次に
rspec binstub を使ってテストスイートの起動時間を速くしよう次に RSpec テストランナーのために binstub をインストールしましょう。こうしておくとアプリケーションの起動時間を素早くするSpringの恩恵が受けられます。
group:developmentdo # 元から書かれている gem は省略 ... gem 'spring-commands-rspec' endそれから新しい binstub を作成 します。
$ bundle exec spring binstub rspecこうするとアプリケーションの bin ディレクトリ内に rspec という名前の実行用ファイルが作成される。
そしたら、rspecがちゃんとインストールされているか確認してみよう
$ bin/rspec
このコマンドを実行し、以下のように出力されたらちゃんとインストールできている証拠ec2-user:~/environment/sample_app (master) $ bin/rspec Running via Spring preloader in process 23528 No examples found. Finished in 0.00044 seconds (files took 0.19004 seconds to load) 0 examples, 0 failures最後に、generateコマンドを使ってアプリにコードを追加する際にRSpec用のファイルも一緒に作ってもらうようにrailsに設定しよう。
ちなみに、RSpecをインストールしたため、generateコマンドを実行してもminitestファイルは作成されなくなった。代わりにスペックファイルが作成されるようになった。
ただ、scaffoldなどをするときに、不必要なスペックファイルまで作成されてしまう可能性があるので、それが作成されないように設定をしておこう。
config/application.rbrequire_relative 'boot' require 'rails/all' # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) module SampleApp class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 5.1 config.generators do |g| g.test_framework :rspec, fixtures: false, view_specs: false, helper_specs: false, routing_specs: false end # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. # 認証トークンをremoteフォームに埋め込む config.action_view.embed_authenticity_token_in_remote_forms = true end endこのように追加してあげる。
以上で開発環境を作る作業は完了。
Q&A
• test ディレクトリは削除しても良いのですか?➡️ゼロから新しいア リケーシ ンを作るのであれば イエスです。これまでにア リケーシ ンをある程度作ってきたのであれば、まず rails test コ ンドを実行し、既存のテストがないことを確認してください。既存のテストがあるなら、それらを RSpec に移行させる必要があるかもしれません。
• なぜビューはテストしないのですか?
➡️信頼性の高い ーのテストを作ることは非常に面倒だ からです。さらに ンテナンスしようと思ったらもっと大変になります。ジェ レータを設定する際 に私が述べたように、UI 関連のテストは統合テストに任せようとしています。これは Rails 開発者 の中では標準的な ラクティスです。第3章モデルスペック
モデルはアプリのコアとなるコード。この部分をしっかりテストできていれば、堅牢な土台を作ることができる。
例えば、モデルスペックは次のような感じになる。
describe User do # 姓、名、メール、パスワードがあれば有効な状態であること it "is valid with a first name, last name, email, and password" # 名がなければ無効な状態であること it "is invalid without a first name" # 姓がなければ無効な状態であること it "is invalid without a last name" # メールアドレスがなければ無効な状態であること it "is invalid without an email address" # 重複したメールアドレスなら無効な状態であること it "is invalid with a duplicate email address" # ーザーのフルネームを文字列として返すこと it "returns a user's full name as a string" endまた、itの後の""は必ず動詞から始まっていることにも注目しよう。
では、早速ファイルを作っていこう。
rspec:model シェネレータを以下のコマンドで実行してください。
$ bin/rails g rspec:model user
これにより以下のファイルが作成される。
spec/models/user_spec.rbrequire 'rails_helper' RSpec.describe User, type: :model do pending "add some examples to (or delete) #{__FILE__}" endここでは、describeメソッドを使って、Userという名前のモデルに関するテストをここに書くよーと宣言してる。
ここで、bin/rspec コマンドでテストの結果を見てみよう。
すると、このような結果が出てきた。
ec2-user:~/environment/sample_app (master) $ bin/rspec Running via Spring preloader in process 5058 User add some examples to (or delete) /home/ec2-user/environment/sample_app/spec/models/user_spec.rb (PENDING: Not yet implemented) Pending: (Failures listed here are expected and do not affect your suite's status) 1) User add some examples to (or delete) /home/ec2-user/environment/sample_app/spec/models/user_spec.rb # Not yet implemented # ./spec/models/user_spec.rb:4 Finished in 0.00182 seconds (files took 0.34229 seconds to load) 1 example, 0 failures, 1 pendingでは、次にdescribeの大枠はそのままに、中身に先ほどの例を書いていこう。
書き終わると以下のようになる
spec/models/user_spec.rbrequire 'rails_helper' RSpec.describe User, type: :model do # 姓、名、メール、パスワードがあれば有効な状態であること it "is valid with a first name, last name, email, and password" # 名がなければ無効な状態であること it "is invalid without a first name" # 姓がなければ無効な状態であること it "is invalid without a last name" # メールアドレスがなければ無効な状態であること it "is invalid without an email address" # 重複したメールアドレスなら無効な状態であること it "is invalid with a duplicate email address" # ーザーのフルネームを文字列として返すこと it "returns a user's full name as a string" end詳細はこの後記載していくが、この状態で bin/rspec を実行して結果を見てみよう。
出力結果は以下のようになる。
ec2-user:~/environment/sample_app (master) $ bin/rspec Running via Spring preloader in process 5312 User is valid with a first name, last name, email, and password (PENDING: Not yet implemented) is invalid without a first name (PENDING: Not yet implemented) is invalid without a last name (PENDING: Not yet implemented) is invalid without an email address (PENDING: Not yet implemented) is invalid with a duplicate email address (PENDING: Not yet implemented) returns a user's full name as a string (PENDING: Not yet implemented) Pending: (Failures listed here are expected and do not affect your suite's status) 1) User is valid with a first name, last name, email, and password # Not yet implemented # ./spec/models/user_spec.rb:5 2) User is invalid without a first name # Not yet implemented # ./spec/models/user_spec.rb:7 3) User is invalid without a last name # Not yet implemented # ./spec/models/user_spec.rb:9 4) User is invalid without an email address # Not yet implemented # ./spec/models/user_spec.rb:11 5) User is invalid with a duplicate email address # Not yet implemented # ./spec/models/user_spec.rb:13 6) User returns a user's full name as a string # Not yet implemented # ./spec/models/user_spec.rb:15 Finished in 0.00355 seconds (files took 0.21558 seconds to load) 6 examples, 0 failures, 6 pendingこのpendingというのはスペックの中身がないため、保留にしといたよーという意味。
では、実際にテストの中身を実装していこう。
*もしテスト環境 で イグレーシ ンが未実行になっているというエラーが出た場合は、bin/rails db:migrate RAILS_ENV=test というコ ンドを実行してデータ ースの構造を最新にしてください。
RSpecの構文
以前まではshould構文が主流だったが、最近expect構文というのも出てきたらしい。
この2つを比較してみよう。# 2と1を足すと3になること it "adds 2 and 1 to make 3" do (2 + 1).should eq 3 endこれに対し、expect構文は
it "adds 2 and 1 to make 3" do expect(2 + 1).to eq 3 endとなる。
eqはマッチャーと呼ばれていて、左辺と右辺を比較するものらしい。
では、実際に1つめの具体的なテストを書いていこう。
spec/models/user_spec.rbrequire 'rails_helper' RSpec.describe User, type: :model do # 姓、名、メール、パスワードがあれば有効な状態であること it "is valid with a first name, last name, email, and password" do user = User.new( name: "hogehoge", email: "hogehoge@gmail.com", password: "hogehoge") expect(user).to be_valid end # 名がなければ無効な状態であること it "is invalid without a first name" # 姓がなければ無効な状態であること it "is invalid without a last name" # メールアドレスがなければ無効な状態であること it "is invalid without an email address" # 重複したメールアドレスなら無効な状態であること it "is invalid with a duplicate email address" # ーザーのフルネームを文字列として返すこと it "returns a user's full name as a string" endここでは、be_validというマッチャーを使って、作られたインスタンスが有効かどうかを判断してくれる。
この場合は有効なので、bin/rspecをすると、以下のように出力される。
ec2-user:~/environment/sample_app (master) $ bin/rspec Running via Spring preloader in process 6451 User is valid with a first name, last name, email, and password is invalid without a first name (PENDING: Not yet implemented) is invalid without a last name (PENDING: Not yet implemented) is invalid without an email address (PENDING: Not yet implemented) is invalid with a duplicate email address (PENDING: Not yet implemented) returns a user's full name as a string (PENDING: Not yet implemented) Pending: (Failures listed here are expected and do not affect your suite's status) 1) User is invalid without a first name # Not yet implemented # ./spec/models/user_spec.rb:13 2) User is invalid without a last name # Not yet implemented # ./spec/models/user_spec.rb:15 3) User is invalid without an email address # Not yet implemented # ./spec/models/user_spec.rb:17 4) User is invalid with a duplicate email address # Not yet implemented # ./spec/models/user_spec.rb:19 5) User returns a user's full name as a string # Not yet implemented # ./spec/models/user_spec.rb:21 Finished in 0.01955 seconds (files took 0.24068 seconds to load) 6 examples, 0 failures, 5 pendingpendingの数が1つ減り、テストが一つ成功したことを意味する。
逆に、メールアドレスを消して、パスワードを6文字以下にしてみると、
以下のように出力された。Failures: 1) User is valid with a first name, last name, email, and password Failure/Error: expect(user).to be_valid expected #<User id: nil, name: "hogehoge", email: "", created_at: nil, updated_at: nil, password_digest: "$2a$04$0MOLfzpAa7BViDYHGAzPo.0zB8zmmXcoGuDA7fFJpHZ...", remember_digest: nil, admin: false, activation_digest: nil, activated: false, activated_at: nil, reset_digest: nil, reset_sent_at: nil> to be valid, but got errors: Email can't be blank, Email is invalid, Password is too short (minimum is 6 characters) # ./spec/models/user_spec.rb:10:in `block (2 levels) in <top (required)>' # /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/spring-commands-rspec-1.0.4/lib/spring/commands/rspec.rb:18:in `call' # -e:1:in `<main>' Finished in 0.49088 seconds (files took 0.24127 seconds to load) 6 examples, 1 failure, 5 pending Failed examples: rspec ./spec/models/user_spec.rb:5 # User is valid with a first name, last name, email, and password横スクロールしないと見えづらいが、
but got errors: Email can't be blank, Email is invalid, Password is too short (minimum is 6 characters)このようにエラーメッセージで何が原因でテストに落ちたか教えてくれる。
バリデーションをテストする
次にバリデーションをテストしていく。
spec/models/user_spec.rbrequire 'rails_helper' RSpec.describe User, type: :model do # 姓、名、メール、パスワードがあれば有効な状態であること it "is valid with a name, email, and password" do user = User.new( name: "hogehoge", email: "hogehoge@gmail.com", password: "hogehoge") expect(user).to be_valid end # 名がなければ無効な状態であること it "is invalid without a first name" do user = User.new(name: nil) user.valid? expect(user.errors[:name]).to include("can't be blank") endまず、user.valid? をすることで、falseになる。
それにより、
errors = { name: "~~~"}というエラーメッセージが格納される。で、includeマッチャにより、can't be blankが含まれているかどうかを確認している。
誤判定ではないか証明しよう
まずは、.to を .to_notに変えて、エクスペクテーションを反転させて見ましょう。
to_notに変えてテストを実行すると、以下のように出力された。
Failures: 1) User is invalid without a first name Failure/Error: expect(user.errors[:name]).to_not include("can't be blank") expected ["can't be blank"] not to include "can't be blank" # ./spec/models/user_spec.rb:16:in `block (2 levels) in <top (required)>' # /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/spring-commands-rspec-1.0.4/lib/spring/commands/rspec.rb:18:in `call' # -e:1:in `<main>' Finished in 0.45446 seconds (files took 0.23381 seconds to load) 5 examples, 1 failure, 3 pending Failed examples: rspec ./spec/models/user_spec.rb:13 # User is invalid without a first nameもう一つの方法が、アプリケーション側のコードを変更してテストの実行結果をみる方法がある。
Userモデルのファイルを開いて、nameのバリデーションをコメントアウトしてみよう。
Failures: 1) User is invalid without a first name Failure/Error: expect(user.errors[:name]).to include("can't be blank") expected [] to include "can't be blank" # ./spec/models/user_spec.rb:16:in `block (2 levels) in <top (required)>' # /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/spring-commands-rspec-1.0.4/lib/spring/commands/rspec.rb:18:in `call' # -e:1:in `<main>' Finished in 0.46719 seconds (files took 0.21363 seconds to load) 5 examples, 1 failure, 3 pendingこのような出力結果が返ってきた。
注目すべきは、expected [] to include "can't be blank"
である。
to_notの時は、
expected ["can't be blank"] not to include "can't be blank"
と出力された。つまり、バリデーションを消したので、エラーメッセージが格納されなかったということ。
ちなみに、モデルはTDDで実装するのに適している。
バリデーションの追加は思った以上に忘れやすく、また、テストを書きながらモデルが持つべきバリデーションについて考えれば忘れにくくなる。emailのユニークバリデーションをテストしてみよう
spec/models/user_spec.rbit "is invalid with a duplicate email address" do User.create( name: "hogehoge", email: "hogehoge@gmail.com", password: "hogehoge") user = User.new( name: "foobar", email: "hogehoge@gmail.com", password: "foobar") user.valid? expect(user.errors[:email]).to include("has already been taken") endprojectモデルのテストをしてみよう
現段階ではそんなモデルはないが、ある亭で。
プロジェクトは各ユーザーごとにユニークじゃなければいけないというバリデーションをテストしたい。
つまり、各ユーザーは同じ名前のプロジェクト名を作ることができないが、他人と被るぶんには良いという話だ。
このテストは以下のように作成できる。
まずはテストファイルを作成しよう
$ bin/rails g rspec:model project
spec/models/project_spec.rb#ユーザー単位では重複したプロジェクト名を許可しない it "does not allow duplicate project names per user" do user = User.create( name: "hogehoge", email: "hogehoge@gmail.com", password: "hogehoge") user.projects.creates( name: "test project") new_project = user.projects.build( name: "test project") new_project.valid? expect(new_project.errors[:name]).to include("has already been taken") end #2人のユーザーが同じ名前を使うことは許可すること it "allows two users to share a project name" do user = User.create( name: "foobar", email: "foobar@gmail.com", password: "foobar") user.projects.create( name: "test project") other_user = User.create( name: "hogehoge", email: "hogehoge@gmail.com", password: "hogehoge") other_project = other_user.projects.build( name: "test project") expect(other_project).to be_valid endこんな感じになる。
では projectモデルには一体どのようなバリデーションがされていたのか?
app/models/project.rbvalidates :name, presence: true, uniqueness: { scope: :user_id }ということである。
また、テストを書いたら、ちゃんと失敗するかどうかも確認しよう。
例えば、to_notにしたり、バリデーションの部分をコメントアウトにしたり、数値しか受け付けないバリデーションなら文字列を渡して見たり、4〜8文字のバリデーションなら、3文字と9文字を渡して見たりということをする。
インスタンスメソッドをテストする
これも例えばの話だが、firstname と lastnameを繋げてフルネームを返してくれる name というインスタンスメソッドがあったとする。
その挙動を確かめるには以下のようなテストを書いていく。
spec/models/user_spec.rbit "returns a user's full name as a string" do user = User.new( first_name: "John", last_name: "Doe",) expect(user.name).to eq "John Doe" endという感じになる。
今回はeqというマッチャを使用している。RSpecで等値のエクスペクテーションを書く時は == ではなく、 eqを使う。
クラスメソッドとスコープをテストする
これも例えの話だが、渡された文字列でnoteを検索する機能があったとする。
それをテストするにはどうすれば良いだろうか?
spec/models/note_spec.rbit "returns notes that match the search term" do user = User.create( . . .) project = user.projects.create( name: "test project",) note1 = project.notes.create( message: "this is the first note.", user: user,) note2 = project.notes.create( message: "this is second note.", user: user,) note3 = project.notes.create( message: "first, preheat the oven.", user: user,) expect(Note.search("first")).to include(note1, note3) expect(Note.search("first")).to_not include(note2) end失敗をテストする
次は先ほどの検索結果のテストに追加していく。
もし、検索したワードにヒットするNoteがなかったらという条件でテストをするspec/models/note_spec.rbit "returns notes that match the search term" do user = User.create( . . .) project = user.projects.create( name: "test project",) note1 = project.notes.create( message: "this is the first note.", user: user,) note2 = project.notes.create( message: "this is second note.", user: user,) note3 = project.notes.create( message: "first, preheat the oven.", user: user,) expect(Note.search("message")).to be_empty endというテストになる。検索した場合だけではなく、結果が返ってこない場合もテストをすると良い。
be_emptyは配列が?空かどうをチェックするマッチャと言える。
describe,context,before,afterを使ってdryなコードを書こう
先ほどのNoteに関するテストは冗長なので、リファクタリングする必要がある。
まず必要なのが、検索機能をテストするdescribeをdescribe Noteブロックの中に作成すること。
これは検索機能にフォーカスするため。
次のような形になる。spec/models/note_spec.rbrequire 'rails_helper' RSpec.describe Note, type: :model do #その他のスペックが並ぶ describe "search message for a term" do #検索用のexampleが並ぶ end endさらに、contextブロックを2つ加えて、exampleを切り分けよう。
1つ目は一致するデータが見つかるとき、
2つ目は一致するデータが一つも見つからない時次のようになる。
spec/models/note_spec.rbrequire 'rails_helper' RSpec.describe Note, type: :model do #その他のスペックが並ぶ describe "search message for a term" do context "when a match is found" do #一致する場合のexampleが並ぶ。 end context "when no match is found" do #一致しない場合のexampleが並ぶ end end endこのようにdescribeはクラスや昨日のアウトラインを記述し、
contextでは特定の条件におけるアウトラインを記述するようにするとわかりやすい。次はbeforeブロックを使ってさらにdryなコードを書いていこう。
beforeブロック内に書かれたコードは、各テストが実行される前に実行される。
ただ、beforeブロックが書かれているdescribeの外側はスコープ適用外なので注意。
次のような形になる。
spec/models/note_spec.rbrequire 'rails_helper' RSpec.describe Note, type: :model do before do #このファイルの全テストで使用するデータをセットアップ end #その他のスペックが並ぶ describe "search message for a term" do before do #検索機能の全テストに関連する追加のテストデータをセットアップする end context "when a match is found" do #一致する場合のexampleが並ぶ。 end context "when no match is found" do #一致しない場合のexampleが並ぶ end end endafterで後処理をすることも可能だが、RSpecの場合、デフォルトでデータベースの後片付けをやってくれるので、afterを使う場面は少ないらしい。
これを元にNoteクラスのテストを書き直していこう。
spec/models/note_spec.rbrequire 'rails_helper' RSpec.describe Note, type: :model do before do @user = User.create( name: "hogehoge", email: "hogehoge@gmail.com", password: "hogehoge") @project = @user.projects.create( name: "test project") end it "is valid with a user, project, and message" do note = Note.new( message: "foobar", user: @user, project: @project,) expect(note).to be_valid end describe "search message for a term" do before do @note1 = project.notes.create( message: "this is the first note.", user: user,) @note2 = project.notes.create( message: "this is second note.", user: user,) @note3 = project.notes.create( message: "first, preheat the oven.", user: user,) end context "when a match is found" do it "returns notes that match the search term" do expect(Note.search("first")).to include(@note1, @note3) end context "when no match is found" do it "returns an empty collection" do expect(Note.search("message")).to be_empty end end end endこのような形になる。
三章まとめ
1.期待する結果は能動形で記述すること。
2.起きてほしいことと、起きて欲しくないことをテストすること。
3.describe,context,before,afterを使ってスペックを整理すること。
sample appのuser_specを完成、リファクタリングしてみよう
自分でリファクタリングして見たよ!
spec/models/user_spec.rbrequire 'rails_helper' RSpec.describe User, type: :model do before do @user = User.create( name: "hogehoge", email: "hogehoge@gmail.com", password: "hogehoge") end # 姓、名、メール、パスワードがあれば有効な状態であること it "is valid with a name, email, and password" do expect(@user).to be_valid end # 名がなければ無効な状態であること it "is invalid without a first name" do @user.name = nil @user.valid? expect(@user.errors[:name]).to include("can't be blank") end # メールアドレスがなければ無効な状態であること it "is invalid without an email address" do @user.email = nil @user.valid? expect(@user.errors[:email]).to include("can't be blank") end # 重複したメールアドレスなら無効な状態であること it "is invalid with a duplicate email address" do other_user = User.new( name: "foobar", email: "hogehoge@gmail.com", password: "foobar") other_user.valid? expect(other_user.errors[:email]).to include("has already been taken") end #パスワードが6文字以下なら無効であること it "is invalid password with 5 strings" do invalid_user = User.new( name: "foobar", email: "foobar@gmail.com", password: "fooba") invalid_user.valid? expect(invalid_user.errors[:password]).to include("is too short (minimum is 6 characters)") end endこれを実行すると、
ec2-user:~/environment/sample_app (master) $ bin/rspec Running via Spring preloader in process 6877 User is valid with a name, email, and password is invalid without a first name is invalid without an email address is invalid with a duplicate email address is invalid password with 5 strings Finished in 0.48302 seconds (files took 0.24129 seconds to load) 5 examples, 0 failuresこのような仕様書が出来上がる!!!!!
注意点としては、itで定義した後に、do を忘れないこと。
お疲れ様でした。
- 投稿日:2019-12-11T12:22:38+09:00
[Rails]検索ロジックをmodelで書く(gemなし)
gemなしでの検索機能
controller
sample_controller.rbdef index if params[:title] @post = Post.title_status(params[:title]) elsif params[:status] @status = params[:status].to_i @post = Post.search_status(@status) else @post = Post.all end endmodel
sample.rbclass Post < ActiveRecord::Base enum status: %i( 闇属性 光属性 水属性 ) scope :search_title, -> (title) { where('title LIKE ?', "%#{title}%") if title.present? } scope :search_status, -> (status) { where(status: status) if status.present? } endview
index.html<%= form_with(url:posts_path, method: :get, local: true) do |f| %> <%= f.label title %> <%= f.text_field :title %> <%= f.status %> <%= f.select :status, [["闇属性", 0],["光属性", 1],["水属性", 2]], :include_blank => true %> <%= f.submit "検索" %> <% end %>おまけ
new.html<%= form_with(@post, local: true) do |f| %> <%= f.label title %> <%= f.text_field :title %> <%= f.status %> <%= f.select :status, [["闇属性", 0],["光属性", 1],["水属性", 2]], :include_blank => true %> <%= f.submit "送信" %> <% end %>テスト
FactoryBot.define do factory :post_1 do name { '遊戯王カード' } status { '闇属性' } end factory :post_2 do name { '遊戯王カード' } status { '光属性' } end endspec/system/post_spec.rbcontext '検索機能のテスト' do before do FactoryBot.create(:post_1) FactoryBot.create(:post_2) end it 'タイトルとステータスで検索' do visit posts_path fill_in 'title', with: '遊戯王カード' select 'status', from: '闇属性' click_button "検索" page.html expect(page).to have_content '闇属性' end end
- 投稿日:2019-12-11T08:41:41+09:00
VSCodeでRails開発
VSCodeでRails開発
VSCodeで、主にRuby(Railsアプリ)を書く際のストレスを解消するために作った拡張機能を紹介します。
Rails専用
Rails Routes
config/routes.rb
の内容に応じて、URLヘルパの入力補完とジャンプが可能になります。
入力補完が効かない箇所でも、railsRoutes.insert
コマンドによる入力が可能です。Rails Partial
パーシャルファイルの入力補完、ジャンプ、作成が可能になります。
Quick Open Rails
Railsアプリ内の各種ファイルを種類別に簡単に開けるようになります。
似たような拡張機能はいくつかあったのですが、微妙に欲しいものと違ったので作りました。
デフォルト設定の状態で以下の特徴があります。(変更可能)
- appディレクトリ以下は自動的にサブディレクトリ名でカテゴライズされる
- マイグレーションファイルは新しい順に表示
Rails DB Schema
db/schema.rb
の内容に応じて補完やジャンプが可能になります。
補完が効かない箇所でもrailsDbSchema.insert
コマンドによる入力が可能です。Rails I18n
入力補完やマウスホバーでの翻訳表示が可能となります。
類似パッケージとの違いとしては以下になります。
- 補完対象のメソッドを設定可能
- localesファイルへのジャンプは未サポート
- 対象ファイルに応じたprefixが補完候補に優先表示される
- 例: app/views/home/show.html.hamlの場合は
views.home.show
Haml Lint
VSCodeからhaml-lintを利用できます。
一部quick-fixにも対応しています。Rails Extension Pack
自作以外のパッケージも含めたRails向け拡張パックです。
Rails以外でも使える拡張
Autocomplete Symbols
ワークスペース内のシンボルによる入力補完が可能となります。
デフォルトでは4文字以上入力した場合にその他補完候補が存在しない場合のみ表示されるようになっていますが、任意のキーで呼び出すことも可能です。
何となくしか名前を覚えていないメソッドを呼びたい場合に重宝します。
シンボルの解析についてはruby拡張のrubyLocate設定を有効にしています。(vendor/bundle
以下にGemをインストールしておけばGemのメソッドも補完できます)Autocomplete Words
開いている他のタブからも単語を入力補完の候補として表示します。
Atomではデフォルトで同様の動きなのですが、VSCodeはアクティブなファイルからしかキーワード補完による候補が表示されない点が困りました。
類似パッケージはあったのですが、動作が安定していなかったので自分が必要としている最低限の機能で作ってみました。Snippets by pattern
ファイル名のパターン別にスニペットを管理できます。
また既存のコードを簡単にスニペット化するためのコマンドも付いています。
- 投稿日:2019-12-11T08:04:20+09:00
Railsチュートリアル 第12章 パスワードの再設定 - 前提
何をしていくか
「パスワードを忘れた」というユーザーのために、「パスワードの再設定」という操作ができるようにします。
「パスワードの再設定」とは
パスワードをダイジェストとしてRDBに保存している場合、ハッシュ関数の性質上、パスワードそのものを通知することはできません。そのため、「再設定」という操作が必要になります。大まかな手順は以下です。
- パスワードを再設定するユーザーは、新たなパスワードをフォームに入力し、アプリケーションに送信する
- アプリケーションは、1.で設定されたパスワードを元に新たにハッシュ値を生成する
- 2.で生成したハッシュ値を、当該ユーザーの新たなパスワードダイジェストとしてRDBに保存する
ユーザーの真正性を確認する方法
「第11章 アカウントの有効化」と同様の手順となります。
- ユーザーは、パスワード再設定用リンクを受け取るためのメールアドレスをアプリケーションに送信する
- アプリケーションは、パスワード再設定用のトークンとダイジェストの組を生成する
- アプリケーションは、2.で生成されたパスワード再設定用ダイジェストをRDBに保存する
- アプリケーションは、2.で生成されたパスワード再設定用トークンを含むURLを生成する
- アプリケーションは、4.で生成されたURLを含むメールを作成し、1.で指定されたメールアドレスに送信する
- ユーザーは、5.で送信されたメールに記載されたURLをクリックする
- アプリケーションは、6.でクリックされたURL(トークン)と2.でRDBに保存されたダイジェストを比較し、ユーザーの真正性を確認する
- アプリケーションは、ユーザーにパスワード再設定用フォームを返す
「第11章 アカウントの有効化」との類似点と相違点
類似点
今回実装する「パスワードの再設定」と、第11章で実装した「ユーザーの有効化」には、以下のような類似点があります。
- トークンとダイジェストの組を生成する
- トークンを含むURLを生成する
- ユーザーの登録メールアドレスにメールを送信する
- トークンを含むURLへの
GET
リクエストをトリガーとして認証を行う相違点
一方で、以下のような相違点もあります。
- パスワードの再設定を実装する際には、既存のビューの変更を伴う
- ビューを2つ新たに実装する必要がある
- パスワード再設定用メールを受け取るメールアドレスの入力フォーム
- 再設定するパスワードそのものの入力フォーム
モックアップ
上述「相違点」でビュー3つが登場しました。Railsチュートリアル本文においては、当該ビューのレイアウトについて、以下のモックアップが提示されています。
- 投稿日:2019-12-11T03:33:16+09:00
まるで魔法だな(パーシャルをrenderし繰り返し処理をする)
魔法かな??
これはユーザーの一覧を取得したい!!(単一のユーザーを表す部分をパーシャルとして分けたい)って時に知って驚いたこと
とりあえずeachを使ってみる
一覧表示ということでeachかな?ってことでこう書いた
<% @users.each do |user| %> <%= render user %> <% end %>実はこれ良くなくてパーシャルをrenderする際はeachを使うべきではないみたい
@usersという変数がコントローラーにあることが前提になってしまうので、再利用性が低くなってしまい、せっかくパーシャルに分けたメリットが薄くなってしまうため。collectionを使ってみる
<%= render partial: 'users/user', collection: @users %>eachがダメなのはわかったのでcollectionで書いてみた これはキタかな?
しかし残念☆ 良いんだけど冗長!!!
どうすればいいんだ、、、たどり着いた良いコード
<%= render @users %>えっこれだけで一覧表示できんの!?
最初はすごく驚いた。まるで魔法かなんかかと
実際これでrender先のパーシャル、_user.html.erb(単一のユーザーを表す部分)を持ってきてユーザーの一覧を表示することができたこれを機に。。。
・ 繰り返し処理の時はeach確定!みたいな思考にならなくなった
・ 可読性の高いコードを意識するようになった最後に
これから一覧表示の課題に取り組むRUNTEQ生にネタバレを防ぐ形でユーザー一覧にしました
アドベントカレンダー素敵すぎてエモいです
- 投稿日:2019-12-11T00:46:08+09:00
Rails6のwebアプリをherokuでデプロイしてdb:migrateしたときのエラーを解決した話
目的
- コマンド
heroku run rake db:migrate
を実行したときに出たエラーの解決したときの話をまとめるエラー概要
herokuにpush後にコマンド
heroku run rake db:migrate
を実行した際に下記のエラーが出た。$ heroku run rake db:migrate Running rake db:migrate on ⬢ study-record... up, run.3764 (Free) rake aborted! Mysql2::Error::ConnectionError: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2) /app/vendor/bundle/ruby/2.5.0/gems/mysql2-0.5.2/lib/mysql2/client.rb:90:in `connect' /app/vendor/bundle/ruby/2.5.0/gems/mysql2-0.5.2/lib/mysql2/client.rb:90:in `initialize' /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_adapters/mysql2_adapter.rb:24:in `new' /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_adapters/mysql2_adapter.rb:24:in `mysql2_connection' /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_adapters/abstract/connection_pool.rb:879:in `new_connection' /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_adapters/abstract/connection_pool.rb:923:in `checkout_new_connection' /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_adapters/abstract/connection_pool.rb:902:in `try_to_checkout_new_connection' /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_adapters/abstract/connection_pool.rb:863:in `acquire_connection' /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_adapters/abstract/connection_pool.rb:587:in `checkout' /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_adapters/abstract/connection_pool.rb:431:in `connection' /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_adapters/abstract/connection_pool.rb:1111:in `retrieve_connection' /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_handling.rb:231:in `retrieve_connection' /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_handling.rb:199:in `connection' /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/tasks/database_tasks.rb:238:in `migrate' /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/railties/databases.rake:85:in `block (3 levels) in <top (required)>' /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/railties/databases.rake:83:in `each' /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/railties/databases.rake:83:in `block (2 levels) in <top (required)>' /app/vendor/bundle/ruby/2.5.0/gems/rake-13.0.0/exe/rake:27:in `<top (required)>' /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:74:in `load' /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:74:in `kernel_load' /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:28:in `run' /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:28:in `run' /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:28:in `run' /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/cli.rb:465:in `exec' /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run' /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/invocation.rb:126:in `invoke_command' /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor.rb:387:in `dispatch' /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/cli.rb:27:in `dispatch' /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/base.rb:466:in `start' /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/cli.rb:18:in `start' /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/exe/bundle:30:in `block in <top (required)>' /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/friendly_errors.rb:124:in `with_friendly_errors' /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/exe/bundle:22:in `<top (required)>' /app/bin/bundle:104:in `load' /app/bin/bundle:104:in `<main>' Tasks: TOP => db:migrate (See full trace by running task with --trace)エラーの予想
- pushまでは問題なかったため、直前に実施したDB系の設定がおかしいのでは??と予想した。
- ソケット系のエラーが出ているので、その辺の情報も怪しいと思った。
調査
直前に設定していたDB系の設定を確認
下記コマンドを実行して設定を確認した。
$ heroku config
- 上記コマンドの出力↓ ``` === study-record Config Vars CLEARDB_DATABASE_URL: mysql://b60b5336b9085d:754f140c@us-cdbr-iron-east-05.cleardb.net/heroku_f69d4fc63e3b43f?reconnect=true DB_HOSTNAME: us-cdbr-iron-east-05.cleardb.net DB_NAME: heroku_f69d4fc63e3b43f DB_PASSWORD: 754f140c DB_PORT: 3306 DB_USERNAME: b60b5336b9085d LANG: en_US.UTF-8 RACK_ENV: production RAILS_ENV: production RAILS_LOG_TO_STDOUT: enabled RAILS_SERVE_STATIC_FILES: enabled SECRET_KEY_BASE: 0d58d7c950379d4bfe741b3ae465b46aaa159ae398c2aaaea5010ae2d817b308a90a7d4702e6281ad609c94de1acf03cb50c1e39d21d9e66af636d812f2e823f ```おかしいところ発見
- 設定の
CLEARDB_DATABASE_URL
のURLがmysql
から始まっていることに気が付いた。- 自分が使用しているの
mysql2
なのにmysql
で良いのかと違和感があった。下記コマンドを実行して
mysql2
に修正した。$ heroku config:set DATABASE_URL='mysql2://b60b5336b9085d:754f140c@us-cdbr-iron-east-05.cleardb.net/heroku_f69d4fc63e3b43f?reconnect=true'再度下記コマンドを実行してheroku側でdb:migrateを行なったところ正常に実行できた。
$ heroku run rake db:migrate Running rake db:migrate on ⬢ study-record... up, run.5104 (Free) D, [2019-12-10T15:16:34.677916 #4] DEBUG -- : (2.4ms) SET NAMES utf8mb4, @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483 D, [2019-12-10T15:16:34.715120 #4] DEBUG -- : (2.2ms) SELECT @@innodb_file_per_table = 1 AND @@innodb_file_format = 'Barracuda' D, [2019-12-10T15:16:34.732371 #4] DEBUG -- : (16.9ms) CREATE TABLE `schema_migrations` (`version` varchar(255) NOT NULL PRIMARY KEY) ROW_FORMAT=DYNAMIC D, [2019-12-10T15:16:34.752768 #4] DEBUG -- : (13.9ms) CREATE TABLE `ar_internal_metadata` (`key` varchar(255) NOT NULL PRIMARY KEY, `value` varchar(255), `created_at` datetime(6) NOT NULL, `updated_at` datetime(6) NOT NULL) ROW_FORMAT=DYNAMIC D, [2019-12-10T15:16:34.757508 #4] DEBUG -- : (2.4ms) SELECT GET_LOCK('3434563884671206245', 0) D, [2019-12-10T15:16:34.774649 #4] DEBUG -- : (3.1ms) SELECT `schema_migrations`.`version` FROM `schema_migrations` ORDER BY `schema_migrations`.`version` ASC I, [2019-12-10T15:16:34.775957 #4] INFO -- : Migrating to CreateUsers (20191106122609) == 20191106122609 CreateUsers: migrating ====================================== -- create_table(:users) D, [2019-12-10T15:16:34.795305 #4] DEBUG -- : (15.9ms) CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, `name` varchar(255), `email` varchar(255), `created_at` datetime(6) NOT NULL, `updated_at` datetime(6) NOT NULL) ROW_FORMAT=DYNAMIC -> 0.0168s == 20191106122609 CreateUsers: migrated (0.0169s) ============================= D, [2019-12-10T15:16:34.811775 #4] DEBUG -- : (4.0ms) BEGIN D, [2019-12-10T15:16:34.814482 #4] DEBUG -- : primary::SchemaMigration Create (2.6ms) INSERT INTO `schema_migrations` (`version`) VALUES ('20191106122609') D, [2019-12-10T15:16:34.816842 #4] DEBUG -- : (2.2ms) COMMIT I, [2019-12-10T15:16:34.816964 #4] INFO -- : Migrating to CreatePosts (20191110100157) == 20191110100157 CreatePosts: migrating ====================================== -- create_table(:posts) D, [2019-12-10T15:16:34.832775 #4] DEBUG -- : (15.0ms) CREATE TABLE `posts` (`id` bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, `content` text, `created_at` datetime(6) NOT NULL, `updated_at` datetime(6) NOT NULL) ROW_FORMAT=DYNAMIC -> 0.0154s == 20191110100157 CreatePosts: migrated (0.0155s) ============================= D, [2019-12-10T15:16:34.835642 #4] DEBUG -- : (2.3ms) BEGIN D, [2019-12-10T15:16:34.838096 #4] DEBUG -- : primary::SchemaMigration Create (2.3ms) INSERT INTO `schema_migrations` (`version`) VALUES ('20191110100157') D, [2019-12-10T15:16:34.840385 #4] DEBUG -- : (2.1ms) COMMIT I, [2019-12-10T15:16:34.840476 #4] INFO -- : Migrating to AddStudyTimeHashTagToPosts (20191112145912) == 20191112145912 AddStudyTimeHashTagToPosts: migrating ======================= -- add_column(:posts, :study_time, "decimal") D, [2019-12-10T15:16:34.861032 #4] DEBUG -- : (19.9ms) ALTER TABLE `posts` ADD `study_time` decimal -> 0.0202s -- add_column(:posts, :hash_tag, "text") D, [2019-12-10T15:16:34.877828 #4] DEBUG -- : (16.4ms) ALTER TABLE `posts` ADD `hash_tag` text -> 0.0168s == 20191112145912 AddStudyTimeHashTagToPosts: migrated (0.0371s) ============== D, [2019-12-10T15:16:34.880640 #4] DEBUG -- : (2.2ms) BEGIN D, [2019-12-10T15:16:34.882939 #4] DEBUG -- : primary::SchemaMigration Create (2.1ms) INSERT INTO `schema_migrations` (`version`) VALUES ('20191112145912') D, [2019-12-10T15:16:34.885466 #4] DEBUG -- : (2.3ms) COMMIT D, [2019-12-10T15:16:34.898276 #4] DEBUG -- : ActiveRecord::InternalMetadata Load (2.7ms) SELECT `ar_internal_metadata`.* FROM `ar_internal_metadata` WHERE `ar_internal_metadata`.`key` = 'environment' LIMIT 1 D, [2019-12-10T15:16:34.909203 #4] DEBUG -- : (2.3ms) BEGIN D, [2019-12-10T15:16:34.911642 #4] DEBUG -- : ActiveRecord::InternalMetadata Create (2.3ms) INSERT INTO `ar_internal_metadata` (`key`, `value`, `created_at`, `updated_at`) VALUES ('environment', 'production', '2019-12-10 15:16:34.906194', '2019-12-10 15:16:34.906194') D, [2019-12-10T15:16:34.914188 #4] DEBUG -- : (2.3ms) COMMIT D, [2019-12-10T15:16:34.916800 #4] DEBUG -- : (2.4ms) SELECT RELEASE_LOCK('3434563884671206245')自分用メモ
- db:migrate系のエラーはだいたい設定のミスなのでよく見直すこと。
付録
- 誤っていたBDの設定ファイルと正常なDBの設定ファイルを下記に記載する。
誤っていたDBの設定ファイル↓
CLEARDB_DATABASE_URL: mysql://b60b5336b9085d:754f140c@us-cdbr-iron-east-05.cleardb.net/heroku_f69d4fc63e3b43f?reconnect=true DB_HOSTNAME: us-cdbr-iron-east-05.cleardb.net DB_NAME: heroku_f69d4fc63e3b43f DB_PASSWORD: 754f140c DB_PORT: 3306 DB_USERNAME: b60b5336b9085d LANG: en_US.UTF-8 RACK_ENV: production RAILS_ENV: production RAILS_LOG_TO_STDOUT: enabled RAILS_SERVE_STATIC_FILES: enabled SECRET_KEY_BASE: 0d58d7c950379d4bfe741b3ae465b46aaa159ae398c2aaaea5010ae2d817b308a90a7d4702e6281ad609c94de1acf03cb50c1e39d21d9e66af636d812f2e823f正常なDBの設定ファイル↓
CLEARDB_DATABASE_URL: mysql2://b60b5336b9085d:754f140c@us-cdbr-iron-east-05.cleardb.net/heroku_f69d4fc63e3b43f?reconnect=true DB_HOSTNAME: us-cdbr-iron-east-05.cleardb.net DB_NAME: heroku_f69d4fc63e3b43f DB_PASSWORD: 754f140c DB_PORT: 3306 DB_USERNAME: b60b5336b9085d LANG: en_US.UTF-8 RACK_ENV: production RAILS_ENV: production RAILS_LOG_TO_STDOUT: enabled RAILS_SERVE_STATIC_FILES: enabled SECRET_KEY_BASE: 0d58d7c950379d4bfe741b3ae465b46aaa159ae398c2aaaea5010ae2d817b308a90a7d4702e6281ad609c94de1acf03cb50c1e39d21d9e66af636d812f2e823f