【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が使えませんでした.

補足とまとめ

はじめにも述べたように, resourcesresourceメソッドを用いてRESTfulにRoutingを組んでいれば起こらないミスでした. もしRESTfulにするならば,

resources :account_activations, only: [:edit]

のように書けばよかったですね. もちろんこの場合はアクション名もactivateからeditに変わりますが.

今回得た学びは, 便利メソッドがやってくれていることを理解しよう ということですね.

resourcesというメソッドは7つものRoutingを一気に設定してくれます. しかしカスタマイズされたRoutingを設定したい場合は, resourcesの仕事を肩代わりしなくてはなりません. そのためにはその仕事内容を理解する必要があるということでした.

【Rails5】Herokuへのデプロイ

基本的に【初心者向け】railsアプリをherokuを使って確実にデプロイする方法【決定版】を参考にさせてもらった. でもやっぱりね, エラーは出るもんですよ.

赤文字で出たエラーがこれ.

Precompiling assets failed.

だからこればっかり検索してたんだけど, あまりいい情報が出てこなかった. それもそのはず. もっと手前で問題が発生していた.

Precompiling assetsfailしたのは, 単なる結果だったというわけ. もっとログを読めばよかったね. ログ全体を見て気になった点は以下の文言.

  • 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で質問されているのが見つかりました.

node.js - Ruby on Rails - Autoprefixer doesn’t support Node v4.9.1. Update it. How to fix? - 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で環境構築している場合は次のようになるでしょう. ちなみに私はMySQLRailsとは別のコンテナで起動させています.

$ 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