Railsチュートリアル 第13章 ユーザーのマイクロポスト
Railsチュートリアルもいよいよ終わりに近づいてきて、ワクワクが止まりません。
今まではユーザーのモデルや、ログイン機構や、そのセッション管理だったのですが、いよいよ投稿機能の実装に入っていきwebサービスの体を成していきます。
13.1.1 基本的なモデル
Micropostモデルは、本文を収納しておくcontent属性と、投稿したユーザーとマイクロポストを関連付けるuser_id属性の2つの属性だけを持ちます。
contentはtext型、user_idはinteger型です。
今まででよく出てきたstrings型とtext型の違いですが、strings型は255文字まで制限があり、text型はより多くの文章量を収納でき、また表現豊かなマイクロポストを実現できたり、投稿フォームにString用のテキストフィールドではなくてText用の広いテキストエリアを使うため、より自然な投稿フォームが実現できたりメリットが多いので、こう言った本文の情報を扱う時はtext型を使います。
Micropostモデルの生成
rails generate model Micropost content:text user:references
Micropostはid、content、user_idからなるモデルですので、作成時はcontentと、user_idを指定します。
user:refarence型となっていますが、この型を指定すると自動的にインデックスと外部キー参照付きのuser_idカラムが追加され、Userのid情報とMicropostのuser_id情報を関連付けする下準備をしてくれます
上のモデルの生成の書き方だと user_idは1つのマイクロポストは1人のユーザーにのみ属するという言う意味で下記のファイルが自動生成されます
リスト 13.2: 自動生成されたMicropostモデル app/models/micropost.rb
class Micropost < ApplicationRecord belongs_to :user end
また、以下のマイグレーションファイルにインデックスを張ります
リスト 13.3: インデックスが付与されたMicropostのマイグレーション db/migrate/[timestamp]_create_microposts.rb
class CreateMicroposts < ActiveRecord::Migration[5.0] def change create_table :microposts do |t| t.text :content t.references :user, foreign_key: true t.timestamps end add_index :microposts, [:user_id, :created_at] #ここにインデックスを追加 end end
user_idとcreated_atを以降使うので、DBの検索効率を上げる目的でindexを追加するのですが、
[:user_id, :created_at]と、両方を1つの配列に含めているのは両方のキーを同時に扱う複合キーインデックスとして作成しているからです。
こうすることで、user_idに関連付けられたすべてのマイクロポストを作成時刻の逆順で取り出しやすくなります。
複合キーインデックスとは、複数の並び替え条件でインデックスを作ること。
つまり、user_idとcreated_at両方使った検索の速度を上げることができます。
13.1.3 User/Micropostの関連付け
UserモデルとMicropostsの関連付けを行います
app/models/micropost.rbに belongs_to :userを
app/models/user.rbに has_many :micropostsを
それぞれ追加することで2つのモデルを関連付けて扱うことができるようになります。
具体的には
表 13.1: user/micropost関連メソッドのまとめ
|メソッド| 用途|
|:---|:---|
|micropost.user| Micropostに紐付いたUserオブジェクトを返す|
|user.microposts| Userのマイクロポストの集合をかえす|
|user.microposts.create(arg)| userに紐付いたマイクロポストを作成する|
|user.microposts.create!(arg)| userに紐付いたマイクロポストを作成する (失敗時に例外を発生)|
|user.microposts.build(arg)| userに紐付いた新しいMicropostオブジェクトを返す|
|user.microposts.find_by(id: 1)| userに紐付いていて、idが1であるマイクロポストを検索する|
13.1.4 マイクロポストを改良する
今回のwebサービスでは最新の投稿を一番上に表示したい。 そこで使われるのがデフォルトスコープ。
ググって調べてみると
デフォルトスコープとは、モデルからデータを取得する際に常に特定の検索条件を指定することができる機能みたいですね。
モデルに検索条件とともに指定します。 リスト 13.17: default_scopeでマイクロポストを順序付ける green app/models/micropost.rb
class Micropost < ApplicationRecord belongs_to :user default_scope -> { order(created_at: :desc) } #こうするとcreated_atについて常に降順にデータを取り出せる validates :user_id, presence: true validates :content, presence: true, length: { maximum: 140 } end
ただし、ググった感じ、結構スパゲッティコードの原因になったり、保守性が悪くなったりする傾向にあるので使う時には注意が必要な部分かもしれません。
13.3.3 フィードの原型
Micropost.where("user_id = ?", id)
"user_id=?",idという書き方はセキュリティ上強く推奨されているとの事ですが、ちょっとわからなかったためググったところ。
Railsガイドによると
例 Client.where("orders_count = ?", params[:orders]) Active Recordは最初の引数を、文字列で表された条件として受け取ります。その後に続く引数は、文字列内にある疑問符 ? と置き換えられます。 以下の書き方は危険であり、避ける必要があります。 Client.where("orders_count = #{params[:orders]}") 条件文字列の中に変数を直接置くと、その変数はデータベースに そのまま 渡されてしまいます。これは、悪意のある人物がエスケープされていない危険な変数を渡すことができるということです。このようなコードがあると、悪意のある人物がデータベースを意のままにすることができ、データベース全体が危険にさらされます。くれぐれも、条件文字列の中に引数を直接置くことはしないでください。
との事。 検索条件を変数にしたい時、文字列の中に直接変数を置くとSQLインジェクションの危険性が高まるので、?でエスケープした書き方にしたほうが良い、とのことです。
13.3.3 フィードの原型
以下の部分がよくわからなかったので、自分なりに考察
ただしささいなことではありますが、マイクロポストの投稿が失敗すると、 Homeページは@feed_itemsインスタンス変数を期待しているため、現状では壊れてしまいます。最も簡単な解決方法は、リスト 13.50のように空の配列を渡しておくことです
リスト 13.50: createアクションに空の@feed_itemsインスタンス変数を追加する app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController before_action :logged_in_user, only: [:create, :destroy] def create @micropost = current_user.microposts.build(micropost_params) if @micropost.save flash[:success] = "Micropost created!" redirect_to root_url else @feed_items = [] #ここに空の@feed_itemsを追加 render 'static_pages/home' end end def destroy end private def micropost_params params.require(:micropost).permit(:content) end end
投稿に失敗すると、elseによりrender 'static_pages/home'でstatic_pagesフォルダ下のhome.html.erbが直接描画される。
renderによる再描画により、@feed_itemsなどのインスタンス変数は無くなってしまう。
また、このrenderによる再描画ではURLアクセス無しのため、homeアクションも実行されないので、インスタンス変数も生成されない。
しかし、このhome.html.erbは<%= render 'shared/feed' %>部分の描画に@feed_itemsインスタンス変数を必要としていて、
描画しようとしても@feed_itemsがない状態のため、エラーが出てしまう。
なので、再描画前に空の@feed_itemsを無理やり生成して対処している、という事かな?
13.3.4 マイクロポストを削除する
リスト 13.52: Micropostsコントローラのdestroyアクション
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController before_action :logged_in_user, only: [:create, :destroy] before_action :correct_user, only: :destroy . . . def destroy @micropost.destroy flash[:success] = "Micropost deleted" redirect_to request.referrer || root_url #request.referrerは直前のurlを返す。Homeページ、プロフィールページどちらから削除された場合でも対応するため end private def micropost_params params.require(:micropost).permit(:content) end def correct_user @micropost = current_user.microposts.find_by(id: params[:id]) #user.microposts.find_by(id)でuserに紐付いていて、そのidのmicropostを検索 redirect_to root_url if @micropost.nil? end end
request.referrerというメソッドフレンドリーフォワーディングのrequest.url変数 と似ていて、直前のページのURLを返します (今回の場合、Homeページになります)。 このため、マイクロポストがHomeページから削除された場合でもプロフィールページから削除された場合でも、request.referrerを使うことでDELETEリクエストが発行されたページに戻すことができます。
13.4.4 本番環境での画像アップロード
ここで、躓いてしまったのですが、
以下のサイトを参考にさせて頂き、
パブリックアクセスコントロールリスト (ACL) を管理する」項目2つのチェックを外して、無事にAWS S3による画像アップロードに成功しました!
13.5.1 本章のまとめ
Active Recordモデルの力によって、マイクロポストも (ユーザーと同じで) リソースとして扱える
Railsは複数のキーインデックスをサポートしている
Userは複数のMicropostsを持っていて (has_many)、Micropostは1人のUserに依存している (belongs_to) といった関係性をモデル化した
has_manyやbelongs_toを利用することで、関連付けを通して多くのメソッドが使えるようになった
user.microposts.build(...)というコードは、引数で与えたユーザーに関連付けされたマイクロポストを返す
default_scopeを使うとデフォルトの順序を変更できる
default_scopeは引数に無名関数 (->) を取る
dependent: :destroyオプションを使うと、関連付けされたオブジェクトと自分自身を同時に削除する
paginateメソッドやcountメソッドは、どちらも関連付けを通して実行され、効率的にデータベースに問い合わせしている
fixtureは、関連付けを使ったオブジェクトの作成もサポートしている
パーシャルを呼び出すときに、一緒に変数を渡すことができる
whereメソッドを使うと、Active Recordを通して選択 (部分集合を取り出すこと) ができる
依存しているオブジェクトを作成/削除するときは、常に関連付けを通すようにすることで、よりセキュアな操作が実現できる
CarrierWaveを使うと画像アップロードや画像リサイズができる