1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Active Storageとurl_forメソッドを用いた画像添付機能の実装(Ruby on Rails7)

Last updated at Posted at 2024-11-14

はじめに

RoRの学習をするために、尊敬するYouTuberの一人である以下の方の動画を参考に、ブログアプリを作成しました。このアプリに画像投稿機能を実装したので、自分の個人メモとして記事に残すことにしました。

開発環境

フロントエンド

  • Next.js 15.0.2 (App Router)
  • react 18.2.0 (TypeScript)

バックエンド

  • ruby 3.1.6
  • Rails 7.2.2

データベース

  • sqlite3

Active Storageとは

Rails5.2から導入された、主にファイルをクラウドストレージサービスにアップロードして保持するためのフレームワークです。Active Storageを使用することで、Amazon S3、Google Cloud Storage、Microsoft Azure Storageなどのクラウドストレージサービスへ簡単にファイルをアップロードできるようになります。ファイルの形式は問われず、あらゆるファイルをアップロードできます。
Active Storageにアップロードされたファイルには個別のURLが付加されます。このようなURLには一般的に有効期間が設定されていますが、クラウドストレージサービスを利用している場合は、RailsからURLの生成をクラウドストレージサービスに依頼してくれます。

(引用:Ruby on Rails 7ポケットリファレンス)

url_forメソッドとは

指定されたオプション(ここではimageファイル)に対応するURLを生成して返すメソッドです。指定できるオプションは公式サイトをご覧ください。

実装手順

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_processingactive_storage に関連するGemがインストールされます。
  • このコマンドが成功することで、rails active_storage:install を実行するための環境が整います。

2. rails active_storage:install

役割:

  • Active Storage 機能をRailsアプリケーションに追加します。
  • Active Storageは、画像やPDF、動画などのファイルをアプリケーションで簡単に管理・操作できるようにするRails標準の機能です。

具体的な動作:

  • このコマンドを実行すると、Active Storageを使用するためのマイグレーションファイルが作成されます。
    • 作成される主なテーブル:
      1. active_storage_blobs: ファイルのメタデータを保存するテーブル。
      2. 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テーブル。

これらのコマンドを実行する順序と効果

  1. bundle install:

    • 必要なGemをインストールしてActive Storageを利用可能にします。
  2. rails active_storage:install:

    • Active Storageを設定するためのマイグレーションファイルを生成します。
  3. 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 が空になることを示します。

まとめ

このコードの動作

  1. Post モデルに Active Storage の画像添付機能を設定(has_one_attached :image)。
  2. 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) を利用することで、画像ファイルを含むデータを送信できます。

Thunder Clientで確認⇩
スクリーンショット 2024-11-14 10.39.56.png


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. 動作確認

  1. Railsサーバーを起動:

    rails server
    
  2. Next.jsサーバーを起動:

    npm run dev
    
  3. ブラウザでアクセス:

    • 投稿フォームページ: http://localhost:3000/posts/create-post
    • 投稿詳細ページ: http://localhost:3000/posts/[id]

こんな感じで表示できました⇩(画像はGPTが作ったうちの娘のアバターですw)
スクリーンショット 2024-11-14 15.50.10(2).png


おわりに

今回は詳細画面での表示までですが、更新画面でも表示できるようにしたいと思います。
もっとスマートに実装できる方法があれば、ご教示ください。誤りがある場合も、ご指摘いただけるとありがたいです。最後までお読みいただきまして、ありがとうございます!

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?