この記事の基本的な方針
メッセージと画像を投稿できるアプリはよくありますが、それが複数の画像となると難易度が格段に上がります。
ここでは、『一度にメッセージと複数画像を投稿できるRailsアプリ』を、極めて丁寧に、誰も置いてきぼりにならないように、プログラミングの基礎ががっちり固まるように、説明いたします。
この記事は、すでに以下の簡単な4画面アプリができている前提です。ログイン機能があり、ログインしていれば登録でき、全ての投稿がログインしていればTOP画面に表示されます。
【TOP画面(ログイン前)】 【TOP画面(ログイン後)】
【登録画面】
【ログイン画面】
手を動かしながら読みたいようでしたら、以下でこの4画面アプリを手に入れてください。
$ 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の導入は慣れたもんですね?
gem 'carrierwave'
を追記して、
$ bundle install
します。はい!bundle install
した後はなんでしたか?サーバの再起動ですね。
次に画像自体とデータベースに保存する情報の架け橋となるアップローダというファイルを作ります。
$ rails g uploader image
作成されたファイルは何も編集する必要はありませんが、一度は開いて目を通すクセをつけましょう。
続いてテーブルを作ります。投稿する画像が一つであればメッセージテーブルにカラムを増やして保存しますが、今回は複数なので別テーブルを作ります。なぜ、複数だと別テーブルでないとならないのでしょうか?テーブルが第一正規形ですらない非正規形になってしまうからです。テーブルは第一より第二より厳しい第三正規形にするのが一般的ですが、ここでは詳しく説明はしません。
テーブルを作ります。
$ rails g model image
今回は画像情報とその画像がどのメッセージと一緒に投稿されたかの情報を保存できるようにしようと思うので、マイグレーションファイルにt.string :image
とt.references :message, foreign_key: true
を追記し,
$ rails db:migrate
します。
もうわかっていると思いますが、テーブル間の関係が1対多である場合は多の側のテーブルに1の側のidを載せます。なぜ?逆パタンを考えてみればわかりますね?メッセージ側に画像のidを載せようとしたら、画像の数だけカラムを増やさなければならないです。それなら初めから画像を別テーブルにせずにメッセージテーブルに画像情報を保存しますよね?
続いて
mount_uploader :image, ImageUploader
を追記します。なぜ?私はこれがどんな機能を持っているか完全にはわかっていません。公式の情報であるCarrierWaveのGitHubにこうしろと書いてあるからです。道具をブラックボックスとして扱うことは悪いことではありません。中身まで全部理解把握するくらいなら一から自分で作った方が早いので、すでに用意してある道具を使う意味がありません。ただ、正しく使うためになるべく一次ソースに触れるクセをつけましょう。
(ブラックボックス…中身の構造はわからないが、入力に対する出力がわかっている道具)
続いて、モデルにテーブル同士の関係を書きましょう。
belongs_to :message
has_many :images
普通です。
次は、formで複数の値をとるために必要になる特別なコードです。
以下を追記します。
accepts_nested_attributes_for :images
accepts_nested_attributes_forメソッドは、1対多のアソシエーションを組んでいて、さらにそれらをformから一度にデータベースに保存したい場合に、1の側に書きます。
続いてコントローラを整えましょう。
まず、app/controllers/messages_controller.rb
のnewアクション内でインスタンス生成のあとに@message.images.build
を追記します。
さて、画像データをコントローラのcreateアクションでどのように受け取ればいいでしょうか?先ほどはbinding.pry
を使いましたが、今回は検証ツールで確認します。
取り急ぎ、ファイルフィールドである以下をフォームのならびに追記して、(複数画像投稿用になっています)
<%= f.fields_for :images do |i| %>
<%= i.file_field :image %>
<% end %>
ブラウザで投稿画面を読み込み検証ツールで確認してみます。
見るべきはinput要素ごとのname属性です。
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])
となります。修正してください。
これについてはこの記事(英語です)が秀逸です。
ここでやっとメッセージと画像が一つ投稿できるようになります。
念のためにビューとコントローラの全体を再掲しておきます。
<%= 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 %>
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]
の数字を増やしていけば複数画像を投稿できるようになります。
<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を使えるようにします。
gem 'jquery-rails'
でbundle install
でサーバ再起動し、
//= require jquery
//= require jquery_ujs
を//= require_tree .
より上に追記し、app/assets/javascripts
にmessages.js
を作ります。app/assets/javascripts/messages.coffee
があると作成したファイルが機能しないので削除します。
$(function () {
console.log("OK")
});
を書いて、ブラウザをどの画面でもいいのでリロードします。
コンソールにOKが表示されたら成功です。
ここでturbolinksというgemがJQueryの動きを阻害する可能性があるので削除しておきます。(リロードすればjsが動作するのに、リンクで移動すると動作しない、等)
Gemfile
のgem 'turbolinks'
をコメントアウトするか消しbundle install
、app/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_for
とfile_field
がネストで組まれているのでfile_fieldのHTML出力であるinput要素がaaaのdiv要素の孫要素に思えるかもしれませんが、fields_for
は複数ファイルフィールドを生み出すのに必要なコードで、実際はaaaのdiv要素のなかにはinput要素のみが子要素である状態になります。検証ツールで確認すればわかります。
さて今の目標は、aaaのdiv要素のなかにあるinput要素の後に続けて(弟要素として)3個ファイルフィールドを入れこむことです。まず小さく試してみましょう。
$(function () {
$(".aaa").append(`<div>ハロー!</div>`);
})
まずはこれで行きます。append
は指定の要素の子要素の一番最後に引数の値を入れ込むメソッドです。類似メソッドのprepend
、before
、after
とセットで捉えておきましょう。
これで<%= i.file_field :image %>
の弟要素として<div>ハロー!</div>
が入るはずです。
ブラウザでlocalhost:3000/messages/new
に行って確認しましょう。ハロー!
が確認できますね。divタグで囲ったので改行されましたね。
念のために検証ツールでも見ておきます。
このようになります。
さて、本当に入れたかったのはファイルフィールドでした。これももはや簡単ですね。
$(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しているだけです。ただ、同じようなことが並ぶのはよくないのでプログラミングらしい方法をとります。以下です。
$(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にのってますね。
<% 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 %>
これで全て完成です。
ここからはおまけです。
もしこのように目的のメソッドがすぐに見つけることが出来なかった場合、少し力技ですが私ならこうします。
class MessagesController < ApplicationController
〜省略〜
def index
@messages = Message.all
binding.pry
end
〜省略〜
end
このようにし、ブラウザでTOP画面にアクセスして停止させます。
@messages
に全てのメッセージの情報が入っていますが、その中から画像も一緒に投稿しているメッセージを一つ選び入っている順番を指定して取り出します。
> @messages[5]
例えばこんな具合に。次に画像テーブルとアソシエーションを組んでいるので
> @messages[5].images
画像は複数なので複数形ですね。
すると
=> [#<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>,]
こんな具合で画像の情報が配列で取れるので、試しに一つ選んでみます。
> @messages[5].images[1]
ここからがわからないので、
> @messages[5].images[1].methods
とします。これは今このオブジェクトに対して使えるメソッドを全て出す方法です。びっくりするほど沢山でてきますね。
この中からそれっぽいものを探して打ってみます。ありました、:image_url
。
> @messages[5].images[1].image_url
結果は、
=> "/uploads/image/image/10/images_-_2020-02-24T000659.895.jpeg"
それっぽいです。
Railsプロジェクト内に画像を格納する場合、assetsディレクトリ配下である場合とpablicディレクトリ配下である場合があり、URLの頭が/
である場合は後者です。実際に見に行って本当にあるか見てみます。あります。
あと確証がほしければ、<インスタンス>.method(:<メソッド>).source_location
と打てばそのメソッドがどこで定義しているかを教えてくれます。
> @messages[5].images[1].method(:image_url).source_location
=>["/Users/xxxxxxxxxxxx/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/carrierwave-2.1.0/lib/carrierwave/mount.rb", 149]
あとはここをcatコマンドで見にいけば書いてあります…複雑に継承してあったりして一筋縄には行きませんが。
話がずれました。
完成はこれでもいいですね。(.
と_
だけの違いですが)
<% 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ではなくこの記事のタイトルと同名のブランチなので注意して下さい)
現状ではメッセージも画像も空でも投稿できてしまいますし、ファイルフィールドで画像以外のファイルも選択できてしまいますし、できれば投稿前にファイルフィールドで選んだ画像が見れるて、複数のファイルフィールドが最初から出ておらず画像を選んだら次のファイルフィールドが出るようになると嬉しいですね。
今後この続きを書く予定です。