はじめに
RoRの学習をするために、尊敬するYouTuberの一人である以下の方の動画を参考に、ブログアプリを作成しました。このアプリに画像投稿機能を実装したので、自分の個人メモとして記事に残すことにしました。
開発環境
フロントエンド
- Next.js 15.0.2 (App Router)
- react 18.2.0 (TypeScript)
バックエンド
- ruby 3.1.6
- Rails 7.2.2
データベース
- sqlite3
実装手順
1. Rails側: APIの設定
Railsで画像投稿を処理するために、Active Storage
を使用します。
1-1. 必要なGemのインストール
image_processing
のGemが必要です。Gemfile
に追加してインストールします。
gem 'image_processing', '~> 1.2'
その後、以下を実行:
bundle install
rails active_storage:install
rails db:migrate
コードの内容
1. bundle install
役割:
-
Gemfile
に記述されたGem(ライブラリ)をインストールします。 - 必要なGemをプロジェクト内の環境に追加し、それらを動作可能な状態にします。
詳細:
-
Gemfile
はRailsアプリケーションで使用するGem(ライブラリ)のリストを記述したファイルです。 -
bundle install
は、Gemfile
を読み取り、指定されたGemとその依存関係を解決してインストールします。 - Gemは通常、プロジェクトごとに管理されるため、プロジェクトの
Gemfile.lock
にインストールされたバージョンが記録されます。
例:
-
image_processing
やactive_storage
に関連するGemがインストールされます。 - このコマンドが成功することで、
rails active_storage:install
を実行するための環境が整います。
2. rails active_storage:install
役割:
- Active Storage 機能をRailsアプリケーションに追加します。
- Active Storageは、画像やPDF、動画などのファイルをアプリケーションで簡単に管理・操作できるようにするRails標準の機能です。
具体的な動作:
- このコマンドを実行すると、Active Storageを使用するためのマイグレーションファイルが作成されます。
- 作成される主なテーブル:
-
active_storage_blobs
: ファイルのメタデータを保存するテーブル。 -
active_storage_attachments
: モデル(例: Post)とファイルの関連付けを管理するテーブル。
-
- 作成される主なテーブル:
ファイルの例:
実行後、db/migrate/
ディレクトリに次のようなマイグレーションファイルが生成されます:
class CreateActiveStorageTables < ActiveRecord::Migration[6.0]
def change
create_table :active_storage_blobs do |t|
t.string :key, null: false
t.string :filename, null: false
t.string :content_type
t.text :metadata
t.string :service_name, null: false
t.bigint :byte_size, null: false
t.string :checksum, null: false
t.datetime :created_at, null: false
end
create_table :active_storage_attachments do |t|
t.string :name, null: false
t.references :record, null: false, polymorphic: true, index: false
t.references :blob, null: false
t.datetime :created_at, null: false
end
end
end
次に実行する必要があるコマンド:
このコマンドで生成されたマイグレーションをデータベースに反映させるために、rails db:migrate
を実行します。
3. rails db:migrate
役割:
- マイグレーションファイルに基づいて、データベース構造を更新します。
- ここでは、
rails active_storage:install
で作成されたActive Storage用のテーブルをデータベースに適用します。
具体的な動作:
-
db/migrate/
ディレクトリ内の未実行のマイグレーションファイルを探します。 - 各マイグレーションを順番に実行し、データベースのスキーマを最新の状態に更新します。
- 更新されたスキーマ情報は
db/schema.rb
に記録されます。
実行後の効果:
- Active Storageでファイルを保存するために必要なデータベースの準備が整います。
- ファイルのメタ情報を保存する
active_storage_blobs
テーブル。 - モデルとファイルの関連付けを管理する
active_storage_attachments
テーブル。
- ファイルのメタ情報を保存する
これらのコマンドを実行する順序と効果
-
bundle install
:- 必要なGemをインストールしてActive Storageを利用可能にします。
-
rails active_storage:install
:- Active Storageを設定するためのマイグレーションファイルを生成します。
-
rails db:migrate
:- データベースにActive Storage用のテーブルを作成し、設定を反映します。
1-2. モデルに画像添付機能を追加
Post
モデルで画像を添付可能にします。
app/models/post.rb
:
class Post < ApplicationRecord
has_one_attached :image
include Rails.application.routes.url_helpers
def as_json(options = {})
super(options).merge(
image_url: image.attached? ? Rails.application.routes.url_helpers.url_for(image) : nil
)
end
end
コードの内容
1. class Post < ApplicationRecord
- このコードは、
Post
という名前の Rails モデルを定義しています。 -
ApplicationRecord
を継承しており、これにより Active Record の機能を利用できます。 - このモデルはデータベースの
posts
テーブルと関連付けられます。
2. include Rails.application.routes.url_helpers
- この行は、Rails のルートヘルパー(例:
url_for
,rails_blob_url
)をこのモデル内で利用可能にするためのものです。 - 通常、
url_for
などのルート生成メソッドはコントローラーやビューでのみ利用できます。この行を追加することで、モデル内でもそれらを使用できるようになります。
3. has_one_attached :image
- Active Storage を使って、このモデルに1つの画像(
image
)を添付できるようにしています。 - Active Storage を利用することで、画像やファイルのアップロード、保存、関連付けが可能になります。
機能概要
- 画像が添付されると、Active Storage は以下の3つを管理します:
- ファイル自体(デフォルトでは
storage
ディレクトリに保存されます)。 - メタデータ(ファイル名やタイプなど)。
- モデル(
Post
)との関連付け情報。
- ファイル自体(デフォルトでは
利用例
post = Post.new(title: "Sample Post")
post.image.attach(io: File.open("path/to/image.jpg"), filename: "image.jpg")
post.save
これにより、image
フィールドに画像が添付されます。
4. def as_json(options = {})
- このメソッドは、
Post
モデルを JSON 形式で返すときのカスタマイズを行います。 - Rails では、モデルを JSON に変換する際に
as_json
メソッドが呼び出されます。このメソッドをオーバーライドすることで、出力される JSON に追加情報を含めることができます。
5. super(options)
-
super(options)
は、親クラス(ApplicationRecord
)のas_json
メソッドを呼び出しています。 -
super
により、通常のPost
モデルの属性(例:id
,title
,content
など)が JSON として返されます。
6. merge(image_url: ...)
-
merge
を使って、生成された JSON にimage_url
という新しいキーを追加しています。 - この
image_url
は、画像が添付されている場合にその画像へのアクセス URL を含みます。
7. image.attached?
-
image.attached?
は、Active Storage で画像が添付されているかを判定します。-
true
の場合: 画像が添付されている。 -
false
の場合: 画像が添付されていない。
-
例
post.image.attached? # => true or false
8. Rails.application.routes.url_helpers.url_for(image)
- 画像が添付されている場合、
url_for(image)
を使ってその画像への完全な URL を生成します。 - 例えば、ローカル開発環境で以下のような URL が生成されます:
http://localhost:3001/rails/active_storage/blobs/redirect/:signed_id/:filename
url_for
の仕組み
-
url_for
は、Rails のルーティング情報を基にして適切な URL を生成します。 - Active Storage のファイルには署名付き ID(
signed_id
)が付与され、セキュリティが確保されています。
9. else nil
- 画像が添付されていない場合、
image_url
の値としてnil
を返します。 - これにより、画像がない場合に
image_url
が空になることを示します。
まとめ
このコードの動作
-
Post
モデルに Active Storage の画像添付機能を設定(has_one_attached :image
)。 -
as_json
メソッドをカスタマイズして、以下を JSON 出力に含める:- 画像の添付状況に応じて、
image_url
を追加。 - 添付されている場合は画像の URL を生成。
- 添付されていない場合は
nil
を設定。
- 画像の添付状況に応じて、
このコードの利点
- Active Storage による画像添付を簡単に扱える。
- API レスポンスに画像の URL を自動的に含められるため、フロントエンドでの処理が容易になる。
- 画像が存在しない場合の対応(
nil
を返す)も含まれているため、堅牢性が高い。
1-3. APIエンドポイントを更新
画像を含む投稿を処理するエンドポイントを作成します。
app/controllers/posts_controller.rb
:
class PostsController < ApplicationController
def create
post = Post.new(post_params)
if post.save
render json: { post: post, message: 'Post created successfully' }, status: :created
else
render json: { errors: post.errors.full_messages }, status: :unprocessable_entity
end
end
def update
post = Post.find(params[:id])
if post.update(post_params)
render json: { post: post, message: 'Post updated successfully' }
else
render json: { errors: post.errors.full_messages }, status: :unprocessable_entity
end
end
private
def post_params
params.require(:post).permit(:title, :content, :image) # imageを許可
end
end
2. Next.js側: フロントエンドの設定
Next.jsで画像を含むフォームを作成し、Rails APIにリクエストを送信します。
2-1. フォームを作成
pages/posts/create-post/page.tsx
:
"use client";
import { useState } from "react";
import axios from "axios";
export default function CreatePost() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [image, setImage] = useState<File | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData();
formData.append("post[title]", title);
formData.append("post[content]", content);
if (image) {
formData.append("post[image]", image);
}
try {
const res = await axios.post("http://localhost:3001/api/v1/posts", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
console.log(res.data);
alert("投稿が作成されました!");
} catch (err) {
console.error(err);
alert("投稿の作成に失敗しました。");
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>タイトル:</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>
<div>
<label>本文:</label>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
required
/>
</div>
<div>
<label>画像:</label>
<input
type="file"
onChange={(e) => setImage(e.target.files ? e.target.files[0] : null)}
/>
</div>
<button type="submit">投稿</button>
</form>
);
}
2-2. APIへのリクエストを確認
-
axios
を使用して、Rails
のAPIに画像を含むリクエストを送信しています。 - フォームデータ (
FormData
) を利用することで、画像ファイルを含むデータを送信できます。
3. Next.jsで画像を表示
投稿詳細ページで画像を表示します。
投稿詳細ページ
pages/posts/[id]/page.tsx
:
"use client";
import { useEffect, useState } from "react";
import axios from "axios";
export default function PostDetail({ params }: { params: { id: string } }) {
const [post, setPost] = useState<any>(null);
useEffect(() => {
const fetchPost = async () => {
const res = await axios.get(`http://localhost:3001/api/v1/posts/${params.id}`);
setPost(res.data);
};
fetchPost();
}, [params.id]);
if (!post) return <div>Loading...</div>;
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
{cleanImageUrl && (
<Image
src={cleanImageUrl}
alt={post.title}
width={500} // 適切な幅を指定
height={450} // 適切な高さを指定
/>
)}
</div>
);
}
4. Railsで画像URLを返す
Rails側で画像のURLを含めたレスポンスを返します。
app/models/post.rb
:
class Post < ApplicationRecord
has_one_attached :image
include Rails.application.routes.url_helpers
def as_json(options = {})
super(options).merge(
image_url: image.attached? ? Rails.application.routes.url_helpers.url_for(image) : nil
)
end
end
config/environments/development.rb
:
Rails.application.routes.default_url_options = { host: "localhost", port: 3001 }
5. 動作確認
-
Railsサーバーを起動:
rails server
-
Next.jsサーバーを起動:
npm run dev
-
ブラウザでアクセス:
- 投稿フォームページ:
http://localhost:3000/posts/create-post
- 投稿詳細ページ:
http://localhost:3000/posts/[id]
- 投稿フォームページ:
こんな感じで表示できました⇩(画像はGPTが作ったうちの娘のアバターですw)
おわりに
今回は詳細画面での表示までですが、更新画面でも表示できるようにしたいと思います。
もっとスマートに実装できる方法があれば、ご教示ください。誤りがある場合も、ご指摘いただけるとありがたいです。最後までお読みいただきまして、ありがとうございます!