前回の復習(プラグインの導入準備)
RubyMine-EAPのウィンドウをクリックしてアクティブ状態にして、画面上部にマウスを移動し、メニューバーを表示してRubyMine-EAPタブのPreferences…をクリックで環境設定をする。
1:またはショートカットキーの「⌘,」で環境設定をする。
2:左のサイドバーのプラグインをクリック
3:上部中央にあるマーケットプレイスをクリックして準備完了 続きを読む
RubyMine-EAPのウィンドウをクリックしてアクティブ状態にして、画面上部にマウスを移動し、メニューバーを表示してRubyMine-EAPタブのPreferences…をクリックで環境設定をする。
1:またはショートカットキーの「⌘,」で環境設定をする。
2:左のサイドバーのプラグインをクリック
3:上部中央にあるマーケットプレイスをクリックして準備完了 続きを読む
システム環境設定→セキュリティーとプライバシー→デベロッパツールから[+]を押し、プログラムの登録ダイアログを表示する。
画面右上の検索窓からを選択(複数ある場合は一番新しくてファイルサイズの大きいもの)を指定します。JetBrains Toolboxを起動中の場合、JetBrains Toolboxを終了させる通知が表示されるので従う。
バージョン管理から取得を選択
任意のものをgithubからクローン
ダウンロードが完了すると、RubyMineが自動的にローカルのRakeを呼ぼうとしてエラーになるが無視する。
default: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password socket: /tmp/mysql.sock host: db development: <<: *default database: lifehack_development
またはショートカットキーの「⌘,」で環境設定をする。
カーソルがある箇所を囲んでいるカッコ()をハイライト表示して
ソースコードを読みやすくできるプラグイン
1:マーケットプレイスの下部にある検索窓にHighlightBracketPairで検索して
インストールをクリックしRestart IDEをクリックして再起動
使い方:プラグインを有効にしている限り有効
その他のプラグインに関しては先程のように検索窓で検索して追加していく。
オススメのプラグインはこちら↓で紹介されているのでチェック!
Jetbrains公式より引用
早期アクセスプログラム(EAP)では、プレスリリースビルドの製品を無料でご利用いただけます。 新しい機能をお試しいただき、貴重なご意見をお寄せください。
1ヶ月の無料期間が終わってまだ購入を検討している方などにオススメ
ただし日本語化をする場合、注意しないと起動しなくなるので注意!
起動しなくなった場合QiitaのRubyMine-EAP を日本語化を参照
1:Ruby Mine公式から左下の「Download」をクリック
2:「保存」をクリックしてダウンロード。
最後の難関、ステータスフィードの実装に取りかかりましょう。
本書の中でも最も高度なものです。完全なステータスフィードは、
13章で扱ったプロトフィードをベースにします。
現在のユーザーにフォローされているユーザーのマイクロポストの配列を作成し、現在のユーザー自身のマイクロポストと合わせて表示します。
このセクションを通して、複雑さを増したフィードの実装に進んでいきます。
これを実現するためには、RailsとRubyの高度な機能の他に、
SQLプログラミングの技術も必要です。
フィードに必要な3つの条件を満たすことです。具体的には、
の3つです。
まずはMichaelがLanaをフォローしていて、
Archerをフォローしていないという状況を作ってみましょう。
この状況のMichaelのフィードでは、Lanaと自分自身の投稿が見えていて、Archerの投稿は見えないことになります
先ほどの3つの条件をアサーションに変換して、
Userモデルにfeedメソッドがあることに注意しながら、
更新したUserモデルに対するテストを書いてみましょう。
結果を↓に示します。
ステータスフィードのテスト
test/models/user_test.rb
マイクロポストのidが正しく並んでいると仮定して
(すなわち若いidの投稿ほど古くなる前提で)、データセットで
user.feed.map(&:id)を実行すると、
どのような結果が表示されるでしょうか? 考えてみてください。
たdefault_scopeを思い出してください。
user.feed.map($:id) =>[1,2,7,8,10]
このように、引数として受け取った自分のidと、
フォローしているidが組み合わさって表示される。
早速フィードの実装に着手してみましょう。最終的なフィードの実装はやや込み入っているため、細かい部品を1つずつ確かめながら導入していきます。
このフィードで必要なクエリについて考えましょう。
ここで必要なのは、micropostsテーブルから、
あるユーザー (つまり自分自身) がフォローしているユーザーに対応するidを持つマイクロポストをすべて選択 (select) することです。
このクエリを模式的に書くと次のようになります。
SELECT * FROM microposts WHERE user_id IN (<list of ids>) OR user_id = <user id>
SQLがINというキーワードをサポートしていることを前提にしています
このキーワードを使うことで、
idの集合の内包 (set inclusion) に対してテストを行えます。
Active Recordのwhereメソッドを使っていることを思い出してください。このときに選択すべき対象はシンプルで、
現在のユーザーに対応するユーザーidを持つマイクロポストを選択すればよかったのでした。
Micropost.where("user_id = ?", id)
今回必要になる選択は、上よりも少し複雑で、例えば次のような形になります。
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
フォローされているユーザーに対応するidの配列が必要であることが
わかってきました。これを行う方法の1つは、Rubyのmapメソッドを使うことです。
このメソッドはすべての「列挙可能 (enumerable)」なオブジェクト
(配列やハッシュなど、要素の集合で構成されるあらゆるオブジェクト)で使えます。なお、このメソッドは前にもも出てきました。
mapメソッドを使って配列を文字列に変換すると、次のようになります。
rails console >> [1, 2, 3, 4].map { |i| i.to_s } => ["1", "2", "3", "4"]
& と、メソッドに対応するシンボルを使った短縮表記が使えます。
この短縮表記であれば、変数iを使わずに済みます。
>> [1, 2, 3, 4].map(&:to_s) => ["1", "2", "3", "4"] >> [1, 2, 3, 4].map(&:to_s).join(', ') => "1, 2, 3, 4"
上のコードを使えば、user.followingにある各要素のidを呼び出し、
フォローしているユーザーのidを配列として扱うことができます。
>> User.first.following.map(&:id) => [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
実際、この手法は実に便利なので、Active Recordでは次のようなメソッドも用意されています。
>> User.first.following_ids => [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
following_idsメソッドは、has_many :followingの関連付けをしたときにActive Recordが自動生成したものです。
これにより、user.followingコレクションに対応するidを得るためには、関連付けの名前の末尾に_idsを付け足すだけで済みます。
結果として、フォローしているユーザーidの文字列は、
次のようにして取得することができます。
>> User.first.following_ids.join(', ') => "3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51"
実際にSQL文字列に挿入するときは、このように記述する必要はありません。
実は、?を内挿すると自動的にこの辺りの面倒を見てくれます。
さらに、データベースに依存する一部の非互換性まで解消してくれます。
つまり、ここではfollowing_idsメソッドをそのまま使えばよいだけなのです。結果、最初に想像していたとおり、
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
app/models/user.rb
class User < ApplicationRecord
.
.
.
# パスワード再設定の期限が切れている場合はtrueを返す
def password_reset_expired?
reset_sent_at < 2.hours.ago
end
# ユーザーのステータスフィードを返す
def feed
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
end
# ユーザーをフォローする
def follow(other_user)
following << other_user
end
.
.
.
end
rails test
いくつかのアプリケーションにおいては、この初期実装だけで目的が達成され、十分に思えるかもしれません。しかしにはまだ足りないものがあります。
それが何なのか、次に進む前に考えてみてください。
(フォローしているユーザーが5,000人もいたらどうなるでしょうか?)。
OR user_id = 1で、自分自身のユーザーidを渡している点に注目。 (user_id IN (3,..,51) OR user_id = 1)
def feed Micropost.where("user_id = ?", following_ids, id) end ActiveRecord::PreparedStatementInvalid: wrong number of bind variables (2 for 1) in: user_id = ? test/models/user_test.rb:15:in `block (2 levels) in <class:UserTest>' test/models/user_test.rb:14:in `block in <class:UserTest>'
# ユーザーのステータスフィードを返す def feed Micropost.all end Expected true to be nil or false test/models/user_test.rb:23:in `block (2 levels) in <class:UserTest>' test/models/user_test.rb:22:in `block in <class:UserTest>' # フォローしていないユーザーの投稿を確認 archer.microposts.each do |post_unfollowed| assert_not michael.feed.include?(post_unfollowed) end
フォローしていないものが含まれているのはダメです
つまり、フォローしているユーザーが5,000人程度になるとWebサービス全体が遅くなる可能性があります。ステータスフィードを改善していきましょう。
following_idsでフォローしているすべてのユーザーをデータベースに
問い合わせし、さらに、フォローしているユーザーの完全な配列を作るために再度データベースに問い合わせしている点です。
SQLのサブセレクト (subselect) を使うと解決できます。
フィードをリファクタリングすることから始めましょう。
whereメソッド内の変数に、キーと値のペアを使う green
app/models/user.rb
class User < ApplicationRecord . . . # ユーザーのステータスフィードを返す def feed Micropost.where("user_id IN (:following_ids) OR user_id = :user_id", following_ids: following_ids, user_id: id) end . . . end
これまでのコード
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
を次のように置き換えました。
Micropost.where("user_id IN (:following_ids) OR user_id = :user_id",
following_ids: following_ids, user_id: id)
前者の疑問符を使った文法も便利ですが、同じ変数を複数の場所に挿入したい場合は、後者の置き換え後の文法を使う方がより便利です。
これからSQLクエリにもう1つのuser_idを追加します。特に、次のRubyコードは、
following_ids
このようなSQLに置き換えることができます。
following_ids = "SELECT followed_id FROM relationships WHERE follower_id = :user_id"
このコードをSQLのサブセレクトとして使います。つまり、「ユーザー1がフォローしているユーザーすべてを選択する」というSQLを既存のSQLに内包させる形になり、結果としてSQLは次のようになります。
SELECT * FROM microposts WHERE user_id IN (SELECT followed_id FROM relationships WHERE follower_id = 1) OR user_id = 1
集合のロジックを (Railsではなく) データベース内に保存するので、
より効率的にデータを取得することができます。
もっと効率的なフィードを実装する準備ができました。
(ここに記述されているコードは生のSQLを表す文字列であり、
following_idsという文字列はエスケープされているのではなく、
見やすさのために式展開しているだけだという点に注意してください。)
app/models/user.rb
class User < ApplicationRecord
.
.
.
# ユーザーのステータスフィードを返す
def feed
following_ids = "SELECT followed_id FROM relationships
WHERE follower_id = :user_id"
Micropost.where("user_id IN (#{following_ids})
OR user_id = :user_id", user_id: id)
end
.
.
.
end
RailsとRubyとSQLのコードが複雑に絡み合っていて厄介ですが、
ちゃんと動作します。
rails test
大規模なWebサービスでは、バックグラウンド処理を使ってフィードを非同期で生成するなどのさらなる改善が必要でしょう。
ステータスフィードの実装は完了です。
rails test
git add -A
git commit -m "Add user following"
git checkout master
git merge following-users
コードをリポジトリにpushして、本番環境にデプロイしてみましょう。
git push
git push heroku
heroku pg:reset DATABASE
heroku run rails db:migrate
heroku run rails db:seed
1:Homeページで表示される1ページ目のフィードに対して、
統合テストを書いてみましょう。
test "feed on Home page" do get root_path @user.feed.paginate(page: 1).each do |micropost| assert_match CGI.escapeHTML(micropost.content), response.body end end
2:期待されるHTMLをCGI.escapeHTMLメソッドでエスケープしています(このメソッドはCGI.escapeと同じ用途です)。
このコードでは、なぜHTMLをエスケープさせる必要があったのでしょうか?考えてみてください。
ヒント: 試しにエスケープ処理を外して、
得られるHTMLの内容を注意深く調べてください。
マイクロポストの内容が何かおかしいはずです。また、
ターミナルの検索機能 (Cmd-FもしくはCtrl-F) を使って
「sorry」を探すと原因の究明に役立つはずです。
contentをエスケープしている為、CGI.esapeHTMLを加える必要がある。
ステータスフィードが追加され、Ruby on Railsチュートリアルの
サンプルアプリケーションがとうとう完成しました。
このサンプルアプリケーションには、
Railsの主要な機能 (モデル、ビュー、コントローラ、テンプレート、パーシャル、beforeフィルター、バリデーション、コールバック、has_many/belongs_to/has_many through関連付け、セキュリティ、テスティング、デプロイ)が多数含まれています。
Railsアプリケーションに何らかの機能を実装していて困ったときは、RailsガイドやRails APIをチェックしてみてください。
いずれも1,000ページを超える大型の公式ドキュメントなので、
今自分がやろうとしていることに関連するトピックがあるかもしれません。
できるだけ念入りにGoogleで検索し、自分が調べようとしているトピックに言及しているブログやチュートリアルがないかどうか、よく探すことです。
Webアプリケーションの開発には常に困難がつきまといます。
他人の経験と失敗から学ぶことも重要です。
Twitterには、マイクロポスト入力中に@記号に続けてユーザーのログイン名を入力するとそのユーザーに返信できる機能があります。
このポストは、宛先のユーザーのフィードと、
自分をフォローしているユーザーにのみ表示されます。
この返信機能の簡単なバージョンを実装してみましょう。具体的には、@replyは受信者のフィードと送信者のフィードにのみ表示されるようにします。
これを実装するには、micropostsテーブルのin_reply_toカラムと、
追加のincluding_repliesスコープをMicropostモデルに追加する
必要があると思います。スコープの詳細については、
RailsガイドのActive Record クエリインターフェイスを参照してください。
このサンプルアプリケーションではユーザー名が重なり得るので、
ユーザー名を一意に表す方法も考えなければならないでしょう。
1つの方法は、idと名前を組み合わせて@1-michael-hartlのようにすることです
もう1つの方法は、ユーザー登録の項目に一意のユーザー名を追加し、@replyで使えるようにすることです。
Twitterでは、ダイレクトメッセージを行える機能がサポートされています。この機能をサンプルアプリケーションに実装してみましょう
(ヒント: Messageモデルと、新規マイクロポストにマッチする正規表現が必要になるでしょう)。
ユーザーに新しくフォロワーが増えたときにメールで通知する機能を
実装してみましょう。続いて、メールでの通知機能をオプションとして選択可能にし、不要な場合は通知をオフにできるようにしてみましょう。
メール周りで分からないことがあったら、
RailsガイドのAction Mailerの基礎にヒントがないか調べてみましょう。
ユーザーごとのマイクロポストをRSSフィードする機能を実装してください。次にステータスフィードをRSSフィードする機能も実装し、
余裕があればフィードに認証スキームも追加してアクセスを制限してみてください。
多くのWebサイトはAPI (Application Programmer Interface)を
公開しており、第三者のアプリケーションからリソースのget/post/put/deleteが行えるようになっています。
サンプルアプリケーションにもこのようなREST APIを実装してください。解決のヒントは、respond_toブロックをコントローラーの多くのアクションに追加することです。
このブロックはXMLをリクエストされたときに応答します。
セキュリティには十分注意してください。
認可されたユーザーにのみAPIアクセスを許可する必要があります。
現在のサンプルアプリケーションには、ユーザーの一覧ページを端から探す、もしくは他のユーザーのフィードを表示する以外に他のユーザーを検索する手段がありません。
この点を強化するために、検索機能を実装してください。
続いて、マイクロポストを検索する機能も追加してください
(ヒント: まずは自分自身で検索機能に関する情報を探してみましょう。難しければ、@budougumi0617 さんの簡単な検索フォームの実装例を参考にしてください)。
上記の他にも、「いいね機能」「シェア機能」「minitestの代わりにRSpecで書き直す」「erbの代わりにHamlで書き直す」
「エラーメッセージをI18nで日本語化する」「オートコンプリート機能」といったアイデアがありそうです。
has_many through
多対多の関係性を定義する関連付けメソッド。
source
has_manyに対してパラメータを与えるオプション。
sourceオブションで与えた値は配列の元を表しているので、実際の配列のインデックスは変わらない。
collection
コレクションルーティングを追加するメソッド。
idを指定せずに全てのメンバーを表示したりできる
フォローしているユーザーを表示するページと、フォロワーを表示するページは、
いずれもプロフィールページとユーザー一覧ページを合わせたような作りになるという点で似ています。
どちらにもフォローの統計情報などのユーザー情報を
表示するサイドバーと、ユーザーのリストがあります。
さらに、サイドバーには小さめのユーザープロフィール画像のリンクを格子状に並べて表示する予定です。
フォローしているユーザーのリンクとフォロワーのリンクを
動くようにすることでTwitterに倣って、
どちらのページでもユーザーのログインを要求するようにします。
フォローしているユーザーのリンクとフォロワーのリンクを動くようにすることです。 Twitterに倣って、どちらのページでもユーザーのログインを要求するようにします。そこで前回のアクセス制御と同様に、まずはテストから書いていきましょう。
フォロー/フォロワーページの認可をテストする red
test/controllers/users_controller_test.rb
require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other_user = users(:archer) end . . . test "should redirect following when not logged in" do get following_user_path(@user) assert_redirected_to login_url end test "should redirect followers when not logged in" do get followers_user_path(@user) assert_redirected_to login_url end end
この実装には1つだけトリッキーな部分があります。
それはUsersコントローラに2つの新しいアクションを追加する必要があるということです。
定義した2つのルーティングにもとづいており、これらはそれぞれfollowingおよびfollowersと呼ぶ必要があります。
それぞれのアクションでは、タイトルを設定し、ユーザーを検索し、@user.followingまたは@user.followersからデータを取り出し、
ページネーションを行なって、ページを出力する必要があります。
followingアクションとfollowersアクション red
app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update, :destroy, :following, :followers] . . . def following @title = "Following" @user = User.find(params[:id]) @users = @user.following.paginate(page: params[:page]) render 'show_follow' end def followers @title = "Followers" @user = User.find(params[:id]) @users = @user.followers.paginate(page: params[:page]) render 'show_follow' end private . . . end
Railsは慣習に従って、アクションに対応するビューを暗黙的に呼び出します。
例えば、showアクションの最後でshow.html.erbを呼び出す、といった具合です。
renderを明示的に呼び出し、show_followという同じビューを出力しています。つまり、作成が必要なビューはこれ1つです。
renderで呼び出しているビューが同じである理由は、
ERbはどちらの場合でもほぼ同じであり、で両方の場合をカバーできるためです。
↓フォローしているユーザーとフォロワーの両方を表示するshow_followビュー green
app/views/users/show_follow.html.erb
<% provide(:title, @title) %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> <%= gravatar_for @user %> <h1><%= @user.name %></h1> <span><%= link_to "view my profile", @user %></span> <span><b>Microposts:</b> <%= @user.microposts.count %></span> </section> <section class="stats"> <%= render 'shared/stats' %> <% if @users.any? %> <div class="user_avatars"> <% @users.each do |user| %> <%= link_to gravatar_for(user, size: 30), user %> <% end %> </div> <% end %> </section> </aside> <div class="col-md-8"> <h3><%= @title %></h3> <% if @users.any? %> <ul class="users follow"> <%= render @users %> </ul> <%= will_paginate %> <% end %> </div> </div>
14.2 FollowのWebインターフェイス
フォロー/フォロー解除の基本的なインターフェイスを実装します。また、
フォローしているユーザーと、フォロワーにそれぞれ表示用のページを作成します。
ユーザーのステータスフィードを追加して、サンプルアプリケーションを完成させます。
サンプルデータを自動作成するrails db:seedを使って、
データベースにサンプルデータを登録できるとやはり便利です。
先にサンプルデータを自動作成できるようにしておけば、
Webページの見た目のデザインから先にとりかかることができ、
バックエンド機能の実装を後に回すことができます。
最初のユーザーにユーザー3からユーザー51までをフォローさせ、
それから逆にユーザー4からユーザー41に最初のユーザーをフォローさせます。
ソースを見るとわかるように、このような設定を自由に行うことができます。こうしてリレーションシップを作成しておけば、
アプリケーションのインターフェイスを開発するには十分です。
サンプルデータにfollowing/followerの関係性を追加する
db/seeds.rb
# ユーザー User.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar", admin: true, activated: true, activated_at: Time.zone.now) 99.times do |n| name = Faker::Name.name email = "example-#{n+1}@railstutorial.org" password = "password" User.create!(name: name, email: email, password: password, password_confirmation: password, activated: true, activated_at: Time.zone.now) end # マイクロポスト users = User.order(:created_at).take(6) 50.times do content = Faker::Lorem.sentence(5) users.each { |user| user.microposts.create!(content: content) } end # リレーションシップ users = User.all user = users.first following = users[2..50] followers = users[3..40] following.each { |followed| user.follow(followed) } followers.each { |follower| follower.follow(user) }
rails db:migrate:reset rails db:seed
User.first.followers.count User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] (0.4ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]] => 38
User.first.following.count User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] (0.2ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? [["follower_id", 1]] => 49
これでサンプルユーザーに、フォローしているユーザーとフォロワーができました。プロフィールページとHomeページを更新して、
これを反映しましょう。
最初に、プロフィールページとHomeページに、
フォローしているユーザーとフォロワーの統計情報を表示するための
パーシャルを作成します。次に、フォロー用とフォロー解除用のフォームを作成します。
それから、フォローしているユーザーの一覧 (“following”)と
フォロワーの一覧 (“followers”) を表示する専用のページを作成します。
resourcesブロックの内側で:memberメソッドを使っています。
これは初登場のメソッドですが、まずはどんな動作をするのか推測してみてください
Usersコントローラにfollowingアクションとfollowersアクションを追加する
config/routes.rb
Rails.application.routes.draw do root 'static_pages#home' get '/help', to: 'static_pages#help' get '/about', to: 'static_pages#about' get '/contact', to: 'static_pages#contact' get '/signup', to: 'users#new' get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' resources :users do member do get :following, :followers end end resources :account_activations, only: [:edit] resources :password_resets, only: [:new, :create, :edit, :update] resources :microposts, only: [:create, :destroy] end
どちらもデータを表示するページなので、
適切なHTTPメソッドはGETリクエストになります。
したがって、getメソッドを使って適切なレスポンスを返すようにします。
他のユーザーをフォロー(およびフォロー解除)できるソーシャルな仕組みの追加と、
フォローしているユーザーの投稿をステータスフィードに表示する機能を追加します。
ここで学んだデータモデルは、今後自分用のWebアプリケーションを開発するときに必ず役に立ちます。
ユーザーをフォローする機能を実装する第一歩は、データモデルを構成することです。ただし、これは見た目ほど単純ではありません。
素朴に考えれば、has_many
(1対多) の関連付けを用いて「1人のユーザーが複数のユーザーをhas_many
としてフォローし、1人のユーザーに複数のフォロワーがいることをhas_many
で表す」といった方法でも実装できそうです。しかし後ほど説明しますが、この方法ではたちまち壁に突き当たってしまいます。これを解決するためのhas_many through
についてもこの後で説明します。
あるユーザーをフォローしているすべてのユーザーの集合はfollowersとなりuser.followersはそれらのユーザーの配列を表すことになります。
Twitterの慣習にならい、本チュートリアルではfollowingという
呼称を採用します (例: “50 following, 75 followers”)。
したがって、あるユーザーがフォローしているすべてのユーザーの集合はcalvin.followingとなります。
followingテーブルと has_many関連付けを使って、
フォローしているユーザーのモデリングができます。
user.followingはユーザーの集合でなければならないため、
followingテーブルのそれぞれの行は、followed_idで識別可能なユーザーでなければなりません (これはfollower_idの関連付けについても同様です)。
さらに、それぞれの行はユーザーなので、
これらのユーザーに名前やパスワードなどの属性も追加する必要があるでしょう。
2つの疑問が生じます。
1. あるユーザーが別のユーザーをフォローするとき、何が作成される? 2. あるユーザーが別のユーザーをフォロー解除するとき、何が削除される?。
1人のユーザーは1対多の関係を持つことができ、
さらにユーザーはリレーションシップを経由して多くのfollowing
(またはfollowers) と関係を持つことができるということです。
Facebookのような友好関係 (Friendships) では本質的に
左右対称のデータモデルが成り立ちますが、
Twitterのようなフォロー関係では左右非対称の性質があります。
すなわち、CalvinはHobbesをフォローしていても、
HobbesはCalvinをフォローしていないといった関係性が成り立つのです。
左右非対称な関係性を見分けるために、
それぞれを能動的関係 (Active Relationship)と
受動的関係 (Passive Relationship)と呼ぶことにします
CalvinがHobbesをフォローしているが、
HobbesはCalvinをフォローしていない場合では、
CalvinはHobbesに対して「能動的関係」を持っていることになります。
つまりアイドルとファンの関係のようなものでCalvinはアイドルなわけよ
逆に、HobbesはCalvinに対して「受動的関係」を持っていることになります。
つまりアイドルとファンの関係のようなものでHobbesはファンなわけよ
フォローしているユーザーを生成するために、能動的関係に焦点を当てていきます
フォローしているユーザーはfollowed_idがあれば識別することができるので、
先ほどのfollowingテーブルをactive_relationshipsテーブル
と見立ててみましょう。
ただしユーザー情報は無駄なので、ユーザーid以外の情報は削除します。そして、followed_idを通して、
usersテーブルのフォローされているユーザーを見つけるようにします。
テーブル名にはこの「関係」を表す「relationships」を使いましょう。モデル名はRailsの慣習にならって、Relationshipとします。
アップローダーも悪くはありませんが、いくつかの目立つ欠点があります。例えば、アップロードされた画像に対する制限がないため、
もしユーザーが巨大なファイルを上げたり、無効なファイルを上げると問題が発生してしまいます。
この欠点を直すために、画像サイズやフォーマットに対するバリデーションを実装し、サーバー用とクライアント (ブラウザ)用の両方に追加しましょう。
画像のファイル名から有効な拡張子 (PNG/GIF/JPEGなど) を検証する
画像フォーマットのバリデーション
app/uploaders/picture_uploader.rb
class PictureUploader < CarrierWave::Uploader::Base storage :file # アップロードファイルの保存先ディレクトリは上書き可能 # 下記はデフォルトの保存先 def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end # アップロード可能な拡張子のリスト def extension_whitelist %w(jpg jpeg gif png) end end
2つ目のバリデーションでは、画像のサイズを制御します。
これはMicropostモデルに書き足していきます。
今回は手動でpicture_sizeという独自のバリデーションを定義します。
今まで使っていたvalidatesメソッドではなく、
validateメソッドを使っている点に注目してください。
app/models/micropost.rb
class Micropost < ApplicationRecord
belongs_to :user
default_scope -> { order(created_at: :desc) }
mount_uploader :picture, PictureUploader
validates :user_id, presence: true
validates :content, presence: true, length: { maximum: 140 }
validate :picture_size
private
# アップロードされた画像のサイズをバリデーションする
def picture_size
if picture.size > 5.megabytes
errors.add(:picture, "should be less than 5MB")
end
end
validateメソッドでは、引数にシンボル (:picture_size) を取り、
そのシンボル名に対応したメソッドを呼び出します。
また、呼び出されたpicture_sizeメソッドでは、5MBを上限とし、
それを超えた場合はカスタマイズした
エラーメッセージをerrorsコレクションに追加しています。
定義した画像のバリデーションをビューに組み込むために、
クライアント側に2つの処理を追加しましょう。
まずはフォーマットのバリデーションを反映するためには、
file_fieldタグにacceptパラメータを付与して使います。
<%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
このときacceptパラメータでは、リスト 13.64で許可したファイル形式を、MIMEタイプで指定するようにします。
次に、大きすぎるファイルサイズに対して警告を出すために、
ちょっとしたJavaScript (正確にはjQuery) を書き加えます。
こうすることで、長すぎるアップロード時間を防いだり、
サーバーへの負荷を抑えたりすることに繋がります。
$('#micropost_picture').bind('change', function() { var size_in_megabytes = this.files[0].size/1024/1024; if (size_in_megabytes > 5) { alert('Maximum file size is 5MB. Please choose a smaller file.'); } });
上のコードでは(ハッシュマーク#から分かるように)
CSS idのmicropost_pictureを含んだ要素を見つけ出し、
この要素を監視しています。
そしてこのidを持った要素とは、マイクロポストのフォームを指します(なお、ブラウザ上で画面を右クリックし、インスペクターで要素を調べることで確認できます)。
つまり、このCSS idを持つ要素が変化したとき、このjQueryの関数が動き出します。そして、もしファイルサイズが大きすぎた場合、
alertメソッドで警告を出すといった仕組みです。
これらの追加的なチェック機能をまとめると、↓のようになります。
ファイルサイズをjQueryでチェックする
app/views/shared/_micropost_form.html.erb
<%= form_for(@micropost) do |f| %> <%= render 'shared/error_messages', object: f.object %> <div class="field"> <%= f.text_area :content, placeholder: "Compose new micropost..." %> </div> <%= f.submit "Post", class: "btn btn-primary" %> <span class="picture"> <%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %> </span> <% end %> <script type="text/javascript"> $('#micropost_picture').bind('change', function() { var size_in_megabytes = this.files[0].size/1024/1024; if (size_in_megabytes > 5) { alert('Maximum file size is 5MB. Please choose a smaller file.'); } }); </script>
実装はまだ不完全です。
仮に送信フォームを使った投稿をうまく制限できても、ブラウザのインスペクタ機能でJavaScriptをいじったり、curl
などを使って直接POST
リクエストを送信する場合には対応しきれません
エラーメッセージが以下のようになる。
画像は5MB未満である必要があります
画像「xmind」ファイルのアップロードは許可されていません。
許可されているタイプ:jpg、jpeg、gif、png
画像を表示させる前にサイズを変更する
画像をリサイズするためには、画像を操作するプログラムが必要になります。今回はImageMagickという
プログラムを使うので、これを開発環境にインストールします。
本番環境がHerokuであれば、既に本番環境でImageMagickが使えるようになっています。クラウドIDEでは、
次のコマンドでこのプログラムをインストールできます。
sudo yum install -y ImageMagick
もしローカル環境で開発している場合、それぞれの環境に応じてImagiMagickをインストールする手順が異なります。
例えばMacの場合であれば、Homebrewを導入し、
brew install imagemagick
コマンドを使ってインストールします。
次に、MiniMagickというImageMagickとRubyを繋ぐgemを使って、
画像をリサイズしてみましょう。
MiniMagickのドキュメントを見ると
様々な方法でリサイズできることがわかりますが、
今回はresize_to_limit: [400, 400]という方法を使います。これは、
縦横どちらかが400pxを超えていた場合、適切なサイズに縮小するオプションです(ただし小さい画像であっても拡大はしません)。
画像をリサイズするために画像アップローダーを修正する
app/uploaders/picture_uploader.rb
class PictureUploader < CarrierWave::Uploader::Base include CarrierWave::MiniMagick process resize_to_limit: [400, 400] storage :file # アップロードファイルの保存先ディレクトリは上書き可能 # 下記はデフォルトの保存先 def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end # アップロード可能な拡張子のリスト def extension_whitelist %w(jpg jpeg gif png) end end
うまく出来る。バリデーション実装前のはもちろんリサイズされない
2:テストを追加していた場合、この時点でテストスイートを走らせると
紛らわしいエラーメッセージが表示されることがあります。
このエラーを取り除いてみましょう。
設定ファイルを修正し、テスト時はCarrierWaveに画像のリサイズを
させないようにしてみましょう。
config/initializers/skip_image_resizing.rb
if Rails.env.test?
CarrierWave.configure do |config|
config.enable_processing = false
end
end
storage :fileという行によって、
ローカルのファイルシステムに画像を保存するようになっているからです。
本番環境では、ファイルシステムではなくクラウドストレージサービスに画像を保存するようにしてみましょう。
本番環境でクラウドストレージに保存するためにはfog gemを使うと簡単です。
本番環境での画像アップロードを調整する
app/uploaders/picture_uploader.rb
class PictureUploader < CarrierWave::Uploader::Base include CarrierWave::MiniMagick process resize_to_limit: [400, 400] if Rails.env.production? storage :fog else storage :file end # アップロードファイルの保存先ディレクトリは上書き可能 # 下記はデフォルトの保存先 def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end # アップロード可能な拡張子のリスト def extension_whitelist %w(jpg jpeg gif png) end end
世の中には多くのクラウドストレージサービスがありますが、今回は有名で信頼性も高いアマゾンの「Simple Storage Service (S3) 」を使います。
Amazon Web Servicesアカウントにサインアップする
AWS Identity and Access Management (IAM)でユーザーを
作成し、AccessキーとSecretキーをメモする
AWS ConsoleからS3 bucketを作成し(bucketの名前はなんでも大丈夫です)作成したユーザーに対してRead権限とWrite権限を付与する
fogでリージョンを指定する場合は
:region => ENV[‘S3_REGION’] といったパラメータを渡し、
heroku config:set S3_REGION=”リージョン名”
といったコマンドを実行することで設定できます。
なお、東京のリージョン名は “ap-northeast-1” です。
config/initializers/carrier_wave.rb
if Rails.env.production?
CarrierWave.configure do |config|
config.fog_credentials = {
# Amazon S3用の設定
:provider => 'AWS',
:region => ENV['S3_REGION'], # 例: 'ap-northeast-1'
:aws_access_key_id => ENV['S3_ACCESS_KEY'],
:aws_secret_access_key => ENV['S3_SECRET_KEY']
}
config.fog_directory = ENV['S3_BUCKET']
end
end
今回は手動で設定する必要があります。heroku config:setコマンドを使って、次のようにHeroku上の環境変数を設定してください。
heroku config:set S3_ACCESS_KEY="ココに先ほどメモしたAccessキーを入力" heroku config:set S3_SECRET_KEY="同様に、Secretキーを入力" heroku config:set S3_BUCKET="Bucketの名前を入力" heroku config:set S3_REGION="Regionの名前を入力"
.gitignoreファイルにアップロード用ディレクトリを追加する
# アップロードされたテスト画像を無視する /public/uploads
それでは、これまでの変更をトピックブランチにコミットし、masterブランチにマージしていきましょう。
rails test git add -A git commit -m "Add user microposts" git checkout master git merge user-microposts git push
次に、Herokuへのデプロイ、データベースのリセット、サンプルデータの生成を順に実行していきます。
git push heroku heroku pg:reset DATABASE heroku run rails db:migrate heroku run rails db:seed
最終状態のGemfile
source 'https://rubygems.org' gem 'rails', '5.1.6' gem 'bcrypt', '3.1.12' gem 'faker', '1.7.3' gem 'carrierwave', '1.2.2' gem 'mini_magick', '4.7.0' gem 'will_paginate', '3.1.6' gem 'bootstrap-will_paginate', '1.0.0' gem 'bootstrap-sass', '3.3.7' gem 'puma', '3.9.1' gem 'sass-rails', '5.0.6' gem 'uglifier', '3.2.0' gem 'coffee-rails', '4.2.2' gem 'jquery-rails', '4.3.1' gem 'turbolinks', '5.0.1' gem 'jbuilder', '2.7.0' group :development, :test do gem 'sqlite3', '1.3.13' gem 'byebug', '9.0.6', platform: :mri end group :development do gem 'web-console', '3.5.1' gem 'listen', '3.1.5' gem 'spring', '2.0.2' gem 'spring-watcher-listen', '2.0.1' end group :test do gem 'rails-controller-testing', '1.0.2' gem 'minitest', '5.10.3' gem 'minitest-reporters', '1.1.14' gem 'guard', '2.14.1' gem 'guard-minitest', '2.4.6' end group :production do gem 'pg', '0.20.0' gem 'fog', '1.42' end # Windows環境ではtzinfo-dataというgemを含める必要があります gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]