9
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

『メッセージと複数画像の投稿』ができる最低限のRailsアプリを丁寧に作る

Last updated at Posted at 2020-03-09

この記事の基本的な方針

メッセージと画像を投稿できるアプリはよくありますが、それが複数の画像となると難易度が格段に上がります。
ここでは、『一度にメッセージと複数画像を投稿できるRailsアプリ』を、極めて丁寧に、誰も置いてきぼりにならないように、プログラミングの基礎ががっちり固まるように、説明いたします。

この記事は、すでに以下の簡単な4画面アプリができている前提です。ログイン機能があり、ログインしていれば登録でき、全ての投稿がログインしていればTOP画面に表示されます。

【TOP画面(ログイン前)】     【TOP画面(ログイン後)】
a0.png a4.png
【登録画面】
a1.png
【ログイン画面】
a2.png

手を動かしながら読みたいようでしたら、以下でこの4画面アプリを手に入れてください。

Terminal
$ git clone -b 『メッセージを投稿』できる最低限のRailsアプリを丁寧に作る(これで初心者完全卒業!) https://github.com/annaPanda8170/minimum_rails_application.git
$ bundle install
$ bundle exec rake db:create
$ bundle exec rake db:migrate

これ自体の作り方は、こちら

想定する読み手

既に一度Railsアプリをチュートリアルやスクール等で作ったことがある方であり、JQueryの基本文法を理解している方を想定しております。
Mac使用で、パソコンの環境構築は完了していることが前提です。

具体的なコーディング手順

完成品GitHub(masterではなくこの記事のタイトルと同名のブランチなので注意して下さい)

①まずは画像テーブルを作り、メッセージと画像一つを投稿できるようにする

画像はデータベースに直接保存することもできなくないようですが値がかなり大きいので、(ローカル環境においては)Railsプロジェクトのディレクトリ内に格納してデータべースにはその情報のみを保存するのが一般的です。
ここではCarrierWaveというgemを使います。gemの導入は慣れたもんですね?

Gemfile
gem 'carrierwave'

を追記して、

Terminal
$ bundle install

します。はい!bundle installした後はなんでしたか?サーバの再起動ですね。
次に画像自体とデータベースに保存する情報の架け橋となるアップローダというファイルを作ります。

Terminal
$ rails g uploader image

作成されたファイルは何も編集する必要はありませんが、一度は開いて目を通すクセをつけましょう。
続いてテーブルを作ります。投稿する画像が一つであればメッセージテーブルにカラムを増やして保存しますが、今回は複数なので別テーブルを作ります。なぜ、複数だと別テーブルでないとならないのでしょうか?テーブルが第一正規形ですらない非正規形になってしまうからです。テーブルは第一より第二より厳しい第三正規形にするのが一般的ですが、ここでは詳しく説明はしません。
テーブルを作ります。

Terminal
$ rails g model image

今回は画像情報とその画像がどのメッセージと一緒に投稿されたかの情報を保存できるようにしようと思うので、マイグレーションファイルにt.string :imaget.references :message, foreign_key: trueを追記し,

Terminal
$ rails db:migrate

します。
もうわかっていると思いますが、テーブル間の関係が1対多である場合は多の側のテーブルに1の側のidを載せます。なぜ?逆パタンを考えてみればわかりますね?メッセージ側に画像のidを載せようとしたら、画像の数だけカラムを増やさなければならないです。それなら初めから画像を別テーブルにせずにメッセージテーブルに画像情報を保存しますよね?
続いて

app/models/image.rb
mount_uploader :image, ImageUploader

を追記します。なぜ?私はこれがどんな機能を持っているか完全にはわかっていません。公式の情報であるCarrierWaveのGitHubにこうしろと書いてあるからです。道具をブラックボックスとして扱うことは悪いことではありません。中身まで全部理解把握するくらいなら一から自分で作った方が早いので、すでに用意してある道具を使う意味がありません。ただ、正しく使うためになるべく一次ソースに触れるクセをつけましょう。

(ブラックボックス…中身の構造はわからないが、入力に対する出力がわかっている道具)

続いて、モデルにテーブル同士の関係を書きましょう。

app/models/image.rb
belongs_to :message
app/models/message.rb
has_many :images

普通です。
次は、formで複数の値をとるために必要になる特別なコードです。
以下を追記します。

app/models/message.rb
accepts_nested_attributes_for :images

accepts_nested_attributes_forメソッドは、1対多のアソシエーションを組んでいて、さらにそれらをformから一度にデータベースに保存したい場合に、1の側に書きます。

続いてコントローラを整えましょう。
まず、app/controllers/messages_controller.rbのnewアクション内でインスタンス生成のあとに@message.images.buildを追記します。

さて、画像データをコントローラのcreateアクションでどのように受け取ればいいでしょうか?先ほどはbinding.pryを使いましたが、今回は検証ツールで確認します。

取り急ぎ、ファイルフィールドである以下をフォームのならびに追記して、(複数画像投稿用になっています)

app/views/messages/new.html.erb
<%= f.fields_for :images do |i| %>
  <%= i.file_field :image %>
<% end %>

ブラウザで投稿画面を読み込み検証ツールで確認してみます。
見るべきはinput要素ごとのname属性です。
imageat.png
message[images_attributes][0][image]が見て取れます。あーなんか難しい!!と、アレルギーを起こしてはなりません。食物アレルギーはともかく、コードアレルギーは勘違いです!深呼吸して落ち着きましょう。
rrr[1]となっていればrrrは配列で2番目の値を示していて、hhh[iii]となっていればhhhはハッシュでキーのiiiに紐づいた値を示していることは大丈夫ですよね?
丁寧に値がどのように格納されるかを構築してみれば、message内に

{"images_attributes"=> [{"image"=> <画像情報> }]}

のように入っていることがわかります。
これがさらにparamsに入るので最終的には

{"message"=> {"images_attributes"=> [{"image"=> <画像情報> }]}}

となります。今1個目の画像のファイルフィールドを扱っているので[0]ですがこれが増えてゆくと、例えば3個の画像が投稿された場合、

{"message"=> {"images_attributes"=> [{"image"=> <1個目画像情報> }, {"image"=> <2個目画像情報> }, {"image"=> <3個目画像情報> }]}}

となるわけです。長いだけで、簡単ですね。
ちなみに、ターミナルで投稿時に見られるこれらの値は、配列も数字と紐づいたハッシュで表記されるので、(ついでにメッセージ内容も加えてみると)

{"message"=> {"message"=> "こんばんは", "images_attributes"=> {"0"=> {"image"=> <1個目画像情報> }, "1"=> {"image"=> <2個目画像情報> },  "2"=> {"image"=> <3個目画像情報> }}}}

のようになるわけです。
これをコントローラのストロングパラメータのpermitメソッドで取得しようとすると、詳しい説明は省きますが、
permit(:message, images_attributes: [:image])
となります。修正してください。
これについてはこの記事(英語です)が秀逸です。

ここでやっとメッセージと画像が一つ投稿できるようになります。

念のためにビューとコントローラの全体を再掲しておきます。

app/views/messages/new.html.erb
<%= form_with(model: @message, local: true) do |f| %>
  <%= f.text_field :message %>
  <%= f.fields_for :images do |i| %>
    <%= i.file_field :image %>
  <% end %>
  <%= f.submit "投稿" %>
<% end %>
app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  before_action :to_root, except: [:index]
  def index
    @messages = Message.all
  end
  def new
    @message = Message.new
    @message.images.build
  end
  def create
    @message = Message.new(message_params)
    @message.save
    redirect_to root_path
  end
  private
  def message_params
    params.require(:message).permit(:message, images_attributes: [:image]).merge(user_id: current_user.id)
  end
  def to_root
    redirect_to root_path unless user_signed_in?
  end
end

問題なさそうですか?

②複数画像を投稿できるようにする

まず、検証ツールで見れるものをそのままHTMLで書いて(コピペして)、[0]の数字を増やしていけば複数画像を投稿できるようになります。

app/views/messages/new.html.erb
<form enctype="multipart/form-data" action="/messages" accept-charset="UTF-8" method="post"><input name="utf8" type="hidden" value="✓"><input type="hidden" name="authenticity_token" value="tpwzi3p9k6T3GNSWOe/0u8sQC2KUfRPV2JjNSZaibSG3KxfHNHDlzaaIL+GP5oDfGKfx2EXaGIt1MO5Jrs4TOA==">
  <input type="text" name="message[message]" id="message_message">
  <input type="file" name="message[images_attributes][0][image]" id="message_images_attributes_0_image">
  <input type="file" name="message[images_attributes][1][image]" id="message_images_attributes_1_image">
  <input type="file" name="message[images_attributes][2][image]" id="message_images_attributes_2_image">
  <input type="file" name="message[images_attributes][3][image]" id="message_images_attributes_3_image">
  <input type="submit" name="commit" value="投稿" data-disable-with="投稿">
</form>

これを、ヘルパーメソッドでどう書けば良いかがわかりませんでした。わかる方教えてください(汗)

erbファイルだけで書くことはできませんでしたが、JQueryを使えばできます。
まずはJQueryを使えるようにします。

Gemfile
gem 'jquery-rails'

bundle installでサーバ再起動し、

app/assets/javascripts/application.js
//= require jquery
//= require jquery_ujs

//= require_tree .より上に追記し、app/assets/javascriptsmessages.jsを作ります。app/assets/javascripts/messages.coffeeがあると作成したファイルが機能しないので削除します。

app/assets/javascripts/messages.js
$(function () {
  console.log("OK")
});

を書いて、ブラウザをどの画面でもいいのでリロードします。
コンソールにOKが表示されたら成功です。

ここでturbolinksというgemがJQueryの動きを阻害する可能性があるので削除しておきます。(リロードすればjsが動作するのに、リンクで移動すると動作しない、等)
Gemfilegem 'turbolinks'をコメントアウトするか消しbundle installapp/assets/javascripts/application.js//= require turbolinksを消し、以下の変更もします。

```:app/views/layouts/application.html.erb`
× <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>

   |
   v

◯ <%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_include_tag 'application' %>



ここからファイルフィールドを4個に増やしてみます。
まず、`fields_for`を適当なクラス名をつけたdivタグで囲います(適当というのは「いいかげん」という意味ではなく「適切に」という意味です。私は`aaa`といいかげんにつけていますが…)。

```app/views/messages/new.html.erb
<%= form_with(model: @message, local: true) do |f| %>
  <%= f.text_field :message %>
  <div class="aaa">
    <%= f.fields_for :images do |i| %>
      <%= i.file_field :image %>
    <% end %>
  </div>
  <%= f.submit "投稿" %>
<% end %>

このerbファイルで見るとfields_forfile_fieldがネストで組まれているのでfile_fieldのHTML出力であるinput要素がaaaのdiv要素の孫要素に思えるかもしれませんが、fields_forは複数ファイルフィールドを生み出すのに必要なコードで、実際はaaaのdiv要素のなかにはinput要素のみが子要素である状態になります。検証ツールで確認すればわかります。

さて今の目標は、aaaのdiv要素のなかにあるinput要素の後に続けて(弟要素として)3個ファイルフィールドを入れこむことです。まず小さく試してみましょう。

app/assets/javascripts/messages.js
$(function () {
  $(".aaa").append(`<div>ハロー!</div>`);
})

まずはこれで行きます。appendは指定の要素の子要素の一番最後に引数の値を入れ込むメソッドです。類似メソッドのprependbeforeafterとセットで捉えておきましょう。
これで<%= i.file_field :image %>の弟要素として<div>ハロー!</div>が入るはずです。
ブラウザでlocalhost:3000/messages/newに行って確認しましょう。ハロー!が確認できますね。divタグで囲ったので改行されましたね。
念のために検証ツールでも見ておきます。

hello.png

このようになります。
さて、本当に入れたかったのはファイルフィールドでした。これももはや簡単ですね。

app/assets/javascripts/messages.js
$(function () {
  let html = `<input type="file" name="message[images_attributes][1][image]" id="message_images_attributes_1_image">
              <input type="file" name="message[images_attributes][2][image]" id="message_images_attributes_2_image">
              <input type="file" name="message[images_attributes][3][image]" id="message_images_attributes_3_image">`
  $(".aaa").append(html);
})

文字列を一度html変数に入れてそれをappendしているだけです。ただ、同じようなことが並ぶのはよくないのでプログラミングらしい方法をとります。以下です。

app/assets/javascripts/messages.js
$(function () {
  let html;
  for (let i = 1; i <= 3; i++){
    html = `<input type="file" name="message[images_attributes][${i}][image]" id="message_images_attributes_${i}_image">`
    $(".aaa").append(html);
  }
})

基本的なんで解説しません。これでファイルフィールドが100個でも1000個でもすぐに増やせるようになりました。
ひとまず投稿機能はこれで終わりです。

③TOP画面にメッセージ一覧に画像を表示する

メッセージによって画像の数が変わるので@messagesのeach文のmごとにまたeach文で画像を全部出せばいいのはわかりますが、URLをどのように取り出せばいいのかわかりません。CarrierWaveのGitHubにのってますね。

app/views/messages/new.html.erb
<% if user_signed_in? %>
  <%= current_user.email %>
  <%= link_to 'ログアウト', destroy_user_session_path, method: :delete %>
  <%= link_to '投稿', new_message_path %>
  <% @messages.each do |m| %>
    <div><span style="color: red;"><%= m.user.email %></span><%= m.message %></div>
    <% m.images.each do |i| %>
      <%= image_tag i.image.url %>   
    <% end %>
  <% end %>
<% else %>
  <%= link_to '新規登録', new_user_registration_path %>
  <%= link_to 'ログイン', new_user_session_path %>
<% end %>

これで全て完成です。

ここからはおまけです。
もしこのように目的のメソッドがすぐに見つけることが出来なかった場合、少し力技ですが私ならこうします。

app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  〜省略〜
  def index
    @messages = Message.all
    binding.pry
  end
  〜省略〜
end

このようにし、ブラウザでTOP画面にアクセスして停止させます。
@messagesに全てのメッセージの情報が入っていますが、その中から画像も一緒に投稿しているメッセージを一つ選び入っている順番を指定して取り出します。

Terminal
> @messages[5]

例えばこんな具合に。次に画像テーブルとアソシエーションを組んでいるので

Terminal
> @messages[5].images

画像は複数なので複数形ですね。

すると

Output
=> [#<Image:0x00007fb6e6a9c2f8 id: 9, image: "images_-_2020-02-24T000659.621.jpeg", message_id: 16, created_at: Sun, 08 Mar 2020 08:12:03 UTC +00:00, updated_at: Sun, 08 Mar 2020 08:12:03 UTC +00:00>,
 #<Image:0x00007fb6e6a9d1a8 id: 10, image: "images_-_2020-02-24T000659.895.jpeg", message_id: 16, created_at: Sun, 08 Mar 2020 08:12:03 UTC +00:00, updated_at: Sun, 08 Mar 2020 08:12:03 UTC +00:00>,
 #<Image:0x00007fb6e6a8fd50 id: 11, image: "images_-_2020-02-24T000700.430.jpeg", message_id: 16, created_at: Sun, 08 Mar 2020 08:12:03 UTC +00:00, updated_at: Sun, 08 Mar 2020 08:12:03 UTC +00:00>,]

こんな具合で画像の情報が配列で取れるので、試しに一つ選んでみます。

Terminal
> @messages[5].images[1]

ここからがわからないので、

Terminal
> @messages[5].images[1].methods

とします。これは今このオブジェクトに対して使えるメソッドを全て出す方法です。びっくりするほど沢山でてきますね。
この中からそれっぽいものを探して打ってみます。ありました、:image_url

Terminal
> @messages[5].images[1].image_url

結果は、

Output
=> "/uploads/image/image/10/images_-_2020-02-24T000659.895.jpeg"

それっぽいです。

Railsプロジェクト内に画像を格納する場合、assetsディレクトリ配下である場合とpablicディレクトリ配下である場合があり、URLの頭が/ である場合は後者です。実際に見に行って本当にあるか見てみます。あります。
あと確証がほしければ、<インスタンス>.method(:<メソッド>).source_locationと打てばそのメソッドがどこで定義しているかを教えてくれます。

Terminal
> @messages[5].images[1].method(:image_url).source_location
Output
=>["/Users/xxxxxxxxxxxx/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/carrierwave-2.1.0/lib/carrierwave/mount.rb", 149]

あとはここをcatコマンドで見にいけば書いてあります…複雑に継承してあったりして一筋縄には行きませんが。

話がずれました。
完成はこれでもいいですね。(._だけの違いですが)

app/views/messages/new.html.erb
<% if user_signed_in? %>
  <%= current_user.email %>
  <%= link_to 'ログアウト', destroy_user_session_path, method: :delete %>
  <%= link_to '投稿', new_message_path %>
  <% @messages.each do |m| %>
    <div><span style="color: red;"><%= m.user.email %></span><%= m.message %></div>
    <% m.images.each do |i| %>
      <%= image_tag i.image_url %>   
    <% end %>
  <% end %>
<% else %>
  <%= link_to '新規登録', new_user_registration_path %>
  <%= link_to 'ログイン', new_user_session_path %>
<% end %>

さて、あとはN+1問d…

疲れましたね。一度ここで締めましょう。

まとめ

長い記事を最後までありがとうございました。
ここまで粘り強く読んで理解されたあなたは本当に素晴らしいです。
"LGTM"をしてくれたらあなたは最高です。

完成品GitHub(masterではなくこの記事のタイトルと同名のブランチなので注意して下さい)

現状ではメッセージも画像も空でも投稿できてしまいますし、ファイルフィールドで画像以外のファイルも選択できてしまいますし、できれば投稿前にファイルフィールドで選んだ画像が見れるて、複数のファイルフィールドが最初から出ておらず画像を選んだら次のファイルフィールドが出るようになると嬉しいですね。
今後この続きを書く予定です。

9
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?