【Rails5】数時間溶かしたエラーの原因はRoutingにあった
長時間悩んだ末にエラーが解決できると, なんとも言えない達成感があります. それと同時に,「俺の時間があああ」とも思いますが...
今回時間を奪ったものは正確に言うと, エラーではなくテストです. 何度やってもテストが通りませんでした.
しかしエラーも引き起こし得るミスですから気をつけたいところですね. ただしRESTfulにRoutingを組んでおけば起ることのない事態ですから, もっとRESTを意識しておけばよかったのかもしれません.
間違ったコード
テストが通らなかった状態では, 以下のような記述をしていました.
# routes.rb get :account_activation, to: 'account_activations#activate'
# account_activations_controller.rb def activate # params[:id] を使う処理 end
なにがまずいのか
上のRoutingではnamed path
の挙動が以下のようになります.
irb(main)> app.account_activation('IdForActivation') => "/account_activation.IdForActivation"
本当はaccount_activation/IdForActivation
というパスになって欲しいのに, そうはなっていないんですね.
正しいコード
:id
を使うことをRailsに教えてやればOKです.
# routes.rb get 'account_activation/:id', to: 'account_activations#activate', as: :account_activation
as
オプションは, named path
を設定するためのものです. これがないとnamed path
が使えませんでした.
補足とまとめ
はじめにも述べたように, resources
やresource
メソッドを用いてRESTfulにRoutingを組んでいれば起こらないミスでした. もしRESTfulにするならば,
resources :account_activations, only: [:edit]
のように書けばよかったですね. もちろんこの場合はアクション名もactivate
からedit
に変わりますが.
今回得た学びは, 便利メソッドがやってくれていることを理解しよう ということですね.
resources
というメソッドは7つものRoutingを一気に設定してくれます. しかしカスタマイズされたRoutingを設定したい場合は, resources
の仕事を肩代わりしなくてはなりません. そのためにはその仕事内容を理解する必要があるということでした.
【Rails5】Herokuへのデプロイ
基本的に【初心者向け】railsアプリをherokuを使って確実にデプロイする方法【決定版】を参考にさせてもらった. でもやっぱりね, エラーは出るもんですよ.
赤文字で出たエラーがこれ.
Precompiling assets failed.
だからこればっかり検索してたんだけど, あまりいい情報が出てこなかった. それもそのはず. もっと手前で問題が発生していた.
Precompiling assets
がfail
したのは, 単なる結果だったというわけ. もっとログを読めばよかったね. ログ全体を見て気になった点は以下の文言.
Warning: Multiple default buildpacks reported the ability to handle this app. The first buildpack in the list below will be used.
Warning: the running version of Bundler (1.15.2) is older than the version that created the lockfile (1.16.4).
Uglifier::Error: Unexpected token name «of», expected punc «;». To use ES6 syntax, harmony mode must be enabled with Uglifier.new(:harmony => true).
結論から言うと, 気にすべきなのは最後のエラーだけ. このエラーに関しては対処法が見つかった. config/environments/production.rb
を以下のように編集.
# config/environments/production.rb - config.assets.js_compressor = :uglifier + config.assets.js_compressor = Uglifier.new(harmony: true)
これでなんとかデプロイできたああ.
【Rails5】Autoprefixer doesn’t support Node v4.8.2. というエラーへの対処法
Rails5での開発中に
Autoprefixer doesn’t support Node v4.8.2.
というエラーに遭遇しました. (少なくとも私の環境では解決した)対処法をまとめました.
環境
対処法
検索してみると, Stack Overflowで質問されているのが見つかりました.
この解答の通り, mini_racer
というGemを追加したら直りました.
結局どんなエラーだったの?
経験が浅いなりに調べてみました. 結局どうしてmini_racer
を導入するとエラーが解消されたかははっきりわかっていません. しかし原因を調べていく上で自分にとって新しい知識が得られたのでまとめておきます. エラーが解消された理由を推測することはできるかもしれません.
Vender Prefixes とは
CSSにはVender Prefixesというものがありまして, これについては以下のブログにとてもわかりやすく書かれていました.
まだベンダープレフィックスで消耗してるの? – ysklog
少し引用しておきましょう.
「ベンダープレフィックス」とは、主に、CSS3で採用予定(草案段階)の機能を各ブラウザが先行的に実装し、その先行実装した機能を有効化させるために必要な各ブラウザ毎の「-moz-」や「-webkit-」という接頭辞のことをいいます。
つまり, 機能によってはブラウザごとに書かなくてはならない面倒なもの, ということですね.
Autoprefixerとは
面倒なVender Prefixesを自動でつけてくれるのが, Autoprefixer
というGemです. エラー文にも登場していますね.
ExecJSとは
ExecJSというGemにより, JavaScriptで書かれたコードをRubyから実行することができるようになります. ExecJSがサポートしているruntime
として, 以下などが挙げられています.
- therubyracer
- Node.js
- mini_racer
mini_racerとは
これはGitHubのReadmeを読んでもらうのが良いかと. GitHub - discourse/mini_racer: Minimal embedded v8
こちらも少し抜粋しておきましょう.
MiniRacer provides a minimal two way bridge between the V8 JavaScript engine and Ruby.
It was created as an alternative to the excellent therubyracer. Unlike therubyracer, mini_racer only implements a minimal bridge. This reduces the surface area making upgrading v8 much simpler and exhaustive testing simpler.
JavaScriptエンジンのV8とRubyの橋渡しをするものだそうですね. ExecJSはmini_racer
のようなものを使って, RubyからJavaScriptを実行するということなのでしょう.
エラーが解消された理由
すでに述べましたが, 正直よくわかっていません.
mini_racer
を導入することで, ExecJSはNode.js
ではなくmini_racer
を使うようになった, ということでしょうか.
【Rails5】`find`と`find_by`の違い
単一のレコードを取得する2つのメソッドの違いをまとめました. これについての記事はいくらでもあるけどね.
違い
find
id
で検索を行う.- レコードが見つからない場合はエラー.
User.find(1000) ActiveRecord::RecordNotFound (Couldn't find User with 'id'=10)
nil
を渡した場合も.
User.find(nil) ActiveRecord::RecordNotFound (Couldn't find User without an ID)
find_by
id
以外でも検索可能.
User.find_by(email: 'hello@world.com')
- レコードが見つからない場合は
nil
を返す.
User.find_by(id: 1000) #=> nil
nil
を渡した場合はIS NULL
で検索.
User.find_by(id: nil) User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IS NULL LIMIT 1
使い分け
404エラーを表示させたい場合はfind
例えばユーザーの詳細ページ(プロフィールページ)を考えてみましょう. コントローラでは以下のようにインスタンス変数を定義するでしょう.
def show @user = User.find(params[:id]) end
もしexample.com/users/1000
のように, 存在しないユーザーを表示させようとすると, 404エラーページを表示してくれます.
ここでfind_by
を使ってしまうと, @user = nil
となり, view側でエラーが発生してしまいますね.
nil
を期待する場合はfind_by
エラーを発生させたくない, そしてレコードがなければnil
を期待するという場合はfind_by
を使います. 例えばRails Tutorialで定義されているcurrent_user
ヘルパーメソッドの定義が以下です(ただし8章時点でのもの).
def current_user @current_user ||= User.find_by(session[:user_id]) end
ここでfind
を使うと, エラーが生じてしまい大変ですね.
id
以外で検索したいが, エラーを発生させたい場合
この場合はfind_by!
が用意されています. この記事を投稿して2日後に, ちょうどこのメソッドを使う機会があったので追記します.
例としてPost
という架空のモデルを考えます. Post
モデルはdraft
というboolean
型のattributeを持っているとします.
# schema.rb の省略版 create_table "post" do |t| t.string "title" t.text "body" t.boolean "draft" end
draft: true
によって下書きとしてPost
を保存できます. 他人は下書きにアクセスできないようにしたいとします. こんなときは次のように実装すればよさそうです.
class PostsController < ApplicationController def show @post = Post.find_by!(id: params[:id], draft: false) end end
下書き保存されたPost
にアクセスすると, まるでレコードが存在しないかのように振舞ってくれますね.
【Rails】データベース上のデータをシンボルで取得
前置き
私はRails Tutorialを1周しただけの初心者ですから, いろいろツッコミどころがあるかもしれません. ご容赦くださいませ.
やりたいこと
たとえばボクサーのデータベースシステムを作るとします(ボクシング全然知りませんが...). データベースには名前(name
)と階級(weight
)を保存するものとします. テーブル例は以下の通りです.
id | name | weight |
---|---|---|
1 | Muhammad Ali | heavy |
2 | Shinsuke Yamanaka | bantam |
この場合, 取得される階級は文字列型ですね.
(irb) boxer = Boxer.first (irb) boxer.weight #=> 'heavy'
しかしアプリケーション内において, 階級はシンボルで扱いたいとしましょう. つまり以下のような動作を実現させたいわけです.
(irb) boxer.weight #=> :heavy
実装
答えはわりと簡単です. アクセサメソッドを上書きすればいいのです.
class Boxer < ApplicationRecord def weight read_attribute(:weight).to_sym end end
実はモデルにはread_attribute
というメソッドが用意されています. これをつかってattribute
を取得し, シンボルに変換しています.
最後に
そもそも階級と自然数を対応付けて, データベースには自然数を保存すればいいのかもしれませんね. 独自にモジュールやクラスを定義すれば, 上でやりたいことは実現できます.
さらにボクシングの階級は全順序集合ですから, 自然数との対応付けがより意味をなします. つまり階級順に取得したいときは, 自然数の自然な順序を利用すればいいということです.
なんだかまとまらない記事でお粗末様でございました.
has_manyで関連づけられたモデルも一括保存する方法
Nested Attributes
によって, has_many
で関連づけられたモデルのデータも丸ごと保存する方法を紹介します.
モデル例
本記事では例として以下のようなモデルを扱うことにします.
class User < ApplicationRecord has_many :posts end class Post < ApplicationRecord belongs_to :user end
実現したいこと
やりたいことは, User
といくつかのPost
をフォームから同時に登録することです. もう少しだけ正確に言うと, User
の登録時に, いくつかのPost
もまとめて登録できるようにします. つまりメインのデータはUser
ということです.
では実装すべきコードを順に見ていきましょう.
モデルの実装コード
以下のようにUser
モデルに1行追加します.
class User < ApplicationRecord has_many :posts accepts_nested_attributes_for :posts end
これで, Post
も一緒に保存するための受け入れ体制ができました.
またPost
モデルからは, (もし書いていたら)以下のバリデーション を取り除きます.
class Post < ApplicationRecord belongs_to :user # validates :user_id, presence: true end
当然湧いてくるであろう疑問
- なぜこのバリデーションを除く必要があるのか
- バリデーションはどのように定義すればいいのか
については最後に説明しますのでご心配なく.
コントローラの実装コード
次はコントローラです.
class UsersController < ApplicationController def new @uesr = User.new 2.times { @user.posts.build } end def create @user = User.new(user_params) if @user.save flash[:success] = 'User created!' redirect_to root_url else render 'new' end end private def user_params params.require(:user).permit(:name, :age, posts_attributes: [:title, :body]) end end
細かいところは仕様によって変わるでしょうが, 最低限上記のような実装は必要になると思います. 詳しく見ていきましょう.
new
アクション
new
アクションでのポイントは以下のコードですね.
2.times { @user.posts.build }
単にUser.new
で終わらせるのではなく, 空のPost
オブジェクトもbuild
しておく必要があります. 今回は例としてPost
を2つ作成しましたが, ここは仕様に左右されるところでしょう.
Strong Parameters
Post
モデルも保存するわけですから, Strong Parameters
においてこれを許可しておかなくてはなりません. 書き方は以下の通りです.
posts_attributes: [:title, :body]
attributes
は複数形です. また今回はUser has many Posts
という関係ですから, posts
も複数形となります.
create
アクション
ここまでできていれば, Post
オブジェクト込みのUser
オブジェクトを作成することは簡単です.
User.new(user_params)
User
オブジェクト単体で作成するときと同じですね.
コントローラはどんな形でパラメータを受け取るのか
モデルとコントローラが実装できれば, integration test
を書くことができます. そのためには, コントローラがどんな形でパラメータを受け取るのかを知る必要がありますね. その答えは以下の通りです.
params: { user: { name: 'Ruby on Rails', age: 14, posts_attributes: [ { title: 'hey', body: 'Lorem ipsum' }, { title: 'hi', body: 'Thank you!'} ] } }
ビューの実装コード
フォームのコード例です.
<%= form_for @user do |f| %> <%= f.label :name %> <%= f.text_field :name %> <%= f.label :age %> <%= f.number_field :age %> <%= f.fields_for :posts do |post_fields| %> <%= post_fields.label :title %> <%= post_fields.text_field :title %> <%= post_fields.label :body %> <%= post_fields.text_field :body %> <% end %> <%= f.submit "Submit" %> <% end %>
簡単ですね! コントローラで2.times
としているので, 自動的にPost
用のフォームが2つ作られます.
Post
モデルのバリデーション
はじめにPost
モデルから以下のバリデーションを取り除きました.
validates :user_id, presence: true
これについて深掘りしていきます.
なぜこのバリデーションを取り除く必要があるのか
accepts_nested_attributes_for
により子モデルであるPost
もまとめて保存できるようにした場合, 以下の流れで処理が進みます.
User
のバリデーションPost
のバリデーションUser
の保存処理Post
の保存処理
これは以下の記事を参考にしました. Rails5でnested attributesに詰まった話 - ウェブエンジニア珍道中
この記事にもあるように上のバリデーションを適用してしまうと,
Post
のバリデーションの段階で, 未だ作られていないUser
オブジェクトのid
を要求してしまうため, 必ずバリデーション失敗する.
ということが起きてしまうわけです.
どのようにバリデーションを定義すべきか
失敗してしまうからといって, user_id
の存在性を諦める必要はありません. ちゃんと別の方法が用意されています. 各モデルを以下のように編集しましょう.
class User < ApplicationRecord has_many :posts, inverse_of :user accepts_nested_attributes_for :posts end class Post < ApplicationRecord belongs_to :user, inverse_of :posts validates_presence_of :user end
これで一括保存とバリデーションを両立することができます.
Railsチュートリアル 第14章
14.1 Relationshipモデル
14.1.1
マイグレーションした際にschema.rb
に反映されませんでした. しかしバッファにはなんのエラーも出ませんでした. ちょっと調べてみましょう.
以下のコマンドでデータベースに接続できます.
$ rails db
私のようにDockerで環境構築している場合は次のようになるでしょう. ちなみに私はMySQLをRailsとは別のコンテナで起動させています.
$ docker-compose exec web rails db
するとパスワードが求められるので, database.yml
で指定したパスワードを入力します. すると以下のようなプロンプトが表示されます.
MySQL [myapp_development]>
myapp_development
はデータベース名ですね. マイグレーションしたテーブルが存在するかを確認してみます.
MySQL [myapp_development]> show tables; +-----------------------------+ | Tables_in_myapp_development | +-----------------------------+ | ar_internal_metadata | | microposts | | relationships | | schema_migrations | | users | +-----------------------------+ 5 rows in set (0.01 sec)
しかしschema.rb
に反映されていないのはおかしいので, マイグレーションをやりなおしましょう.
MySQL [myapp_development]> exit Bye $ docker-compose exec web rails db:rollback $ docker-compose exec web rails db:migrate
すると以下のようなエラーが出ました.
Mysql2::Error: Table 'relationships' already exists
そもそも最初のマイグレーションがうまくいってないから, ロールバックによってテーブルが削除されなかったようですね. では手動でテーブルを削除しましょう.
$ docker-compose exec web rails db MySQL [myapp_development]> drop table relationships; Query OK, 0 rows affected (0.02 sec) MySQL [myapp_development]> exit Bye
ここでもう一度マイグレーションをしたところ, ちゃんとschema.rb
に反映されました.
演習1
答えが問題文中に書かれていますね. 図14.7を見ると以下の配列が得られることがわかります.
[2, 7, 10, 8]
演習2
id
が2であるユーザーはid
が1のユーザーしかフォローしていません. したがってこのユーザーだけからなる配列が得られます.
したがってuser.followind.map(&:id)
は[1]
に等しいですね.
14.1.2
演習1, 2
省略.
14.1.3
演習1
たしかにバリデーションを解除してもテストは成功しました.
14.1.4
演習1, 2
以下の通りです.
>> first = User.first >> second = User.second >> first.following?(second) SELECT 1 AS one FROM `users` INNER JOIN `relationships` ON `users`.`id` = `relationships`.`followed_id` WHERE `relationships`.`follower_id` = 1 AND `users`.`id` = 2 LIMIT 1 => false >> first.follow(second) INSERT INTO `relationships` (`follower_id`, `followed_id`, `created_at`, `updated_at`) VALUES (1, 2, '2018-07-05 15:19:20', '2018-07-05 15:19:20') >> first.following?(second) => true >> first.unfollow(second) DELETE FROM `relationships` WHERE `relationships`.`id` = 1 >> first.following?(second) => false
14.1.5
演習1
>> user = User.first >> second = User.second >> last = User.last >> second.follow(user) >> last.follow(user) >> user.followers.map(&:id) SELECT `users`.* FROM `users` INNER JOIN `relationships` ON `users`.`id` = `relationships`.`follower_id` WHERE `relationships`.`followed_id` = 1 => [2, 100]
演習2, 3
前者はデータベースの中で数えていますが, 後者はRubyの配列として数えています. したがって前者のほうがパフォーマンスが高いですね.
user.followers.count SELECT COUNT(*) FROM `users` INNER JOIN `relationships` ON `users`.`id` = `relationships`.`follower_id` WHERE `relationships`.`followed_id` = 1 => 2 >> user.followers.to_a.count => 2
14.2 [follow]のWebインターフェース
14.2.1
演習1, 2
>> user = User.first >> user.following.count => 49 >> user.followers.count => 38
14.2.2
演習1, 2
省略.
演習3
プロフィールテストに書きました.
# test/integration/users_profile_test.rb test "stats" do log_in_as(@user) get root_path assert_select '#following', text: "#{@user.following.count}" assert_select '#followers', text: "#{@user.followers.count}" get user_path(@user) assert_select '#following', text: "#{@user.following.count}" assert_select '#followers', text: "#{@user.followers.count}" end
以下のように数字のままだとダメでした.
assert_select '#following', text: @user.following.count
14.2.3
演習1, 2
省略.
14.2.4
演習1
省略.
演習2
Follow, Unfollowどちらを押しても, users/show
が描画されています.
Rendered users/show.html.erb within layouts/application
14.2.5
演習1
Safariの開発メニューからJavascriptを無効にした場合もちゃんと動作しました.
演習2
省略.
14.2.6
演習1, 2
省略.
14.3 ステータスフィード
14.3.1
演習1
新しい順に取得するので, 以下のようになるでしょう.
user.feed.map(&:id) => [10, 9, 7, 5, 4, 2, 1]
14.3.2
演習1
feed
メソッドを以下のように変更すればいいですね.
def feed Micropost.where("user_id IN (?)", following_ids) end
この場合はmichael
自身の投稿を確認するテストで失敗します.
演習2
これは13章で実装した通りですね.
def feed Micropost.where("user_id = ?", id) end
するとlana
の投稿を確認するテストで失敗します.
演習3
feed
メソッドを以下のように変更すればいいですね.
def feed Micropost.all end
するとarcher
の投稿を確認するテストで失敗します.
14.3.3
演習1
以下の通りです.
test "feed on Home page" do get root_path @user.feed.paginate(page: 1) do |micropost| assert_match CGI.escapeHTML(micropost.content), response.body end end
演習2
ヒントから, 以下のmicropostがまずいんでしょうが, よくわかりませんでした.
tone: content: "I'm sorry. Your words made sense, but your sarcastic tone did not." created_at: <%= 10.minutes.ago %> user: lana