Ruby on Rails

「設定より規約」を重視したフルスタックWebフレームワーク。Hotwireによりモダンなフロントエンド開発も可能になった。

RubyフレームワークMVCWeb開発フルスタック

GitHub概要

rails/rails

Ruby on Rails

スター57,276
ウォッチ2,311
フォーク21,912
作成日:2008年4月11日
言語:Ruby
ライセンス:MIT License

トピックス

activejobactiverecordframeworkhtmlmvcrailsruby

スター履歴

rails/rails Star History
データ取得日時: 2025/8/13 01:43

フレームワーク

Ruby on Rails

概要

Ruby on Railsは、Ruby言語で書かれたオープンソースのWebアプリケーションフレームワークです。

詳細

Ruby on Rails(Rails)は、2004年にDavid Heinemeier Hanssonによって作成された、Web開発における的命的なフレームワークです。「Convention over Configuration(設定よりも規約)」、「Don't Repeat Yourself(DRY)」、「RESTful設計」などの哲学を中核とし、開発者が簡単に高品質なWebアプリケーションを構築できるように設計されています。MVC(Model-View-Controller)アーキテクチャを採用し、データベース操作のActive Record、ルーティング管理のAction Controller、ビューレンダリングのAction Viewなど、包括的な機能を提供しています。ジェネレーターやスキャフォールディング機能により、コードの自動生成と高速なプロトタイピングが可能です。豊富なGemエコシステムにより、認証、ファイルアップロード、決済処理など、様々な機能を簡単に組み込めます。テストドリブン開発(TDD)や行動駆動開発(BDD)に必要なテストフレームワークやツールが最初から組み込まれており、保守性が高く反復可能なコードを書くことができます。GitHub、Shopify、Airbnb、Basecampなど多くの有名サービスで採用され、特にスタートアップやプロトタイプ開発、コンテンツ管理システムの構築に優れています。

メリット・デメリット

メリット

  • 高い生産性: Convention over Configurationによる高速開発
  • 豊富なGemエコシステム: 15万以上のライブラリが利用可能
  • MVCアーキテクチャ: 整理されたコード構造と関心の分離
  • Active Record: 直感的で強力なORM機能
  • ジェネレーター: コードの自動生成とスキャフォールディング
  • テスト統合: ビルトインのテストフレームワーク
  • 大きなコミュニティ: 豊富なドキュメントと情報源
  • 柔軟性: 様々な用途に対応できる拡張性

デメリット

  • パフォーマンス: コンパイル言語と比較して実行速度が遅い
  • メモリ使用量: リソース消費が大きい
  • コンベンションの制約: 規約に従わない場合の複雑さ
  • マジックの存在: 抽象化によるブラックボックス化
  • バージョン互換性: メジャーアップデート時の互換性問題
  • スケーリングの課題: 大規模アプリケーションでのスケール難易度

主要リンク

書き方の例

Hello World

# Gemfile
source 'https://rubygems.org'
gem 'rails', '~> 8.0.0'

# config/routes.rb
Rails.application.routes.draw do
  root 'application#hello'
end

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  def hello
    render plain: "Hello, Rails!"
  end
end
# 新しいRailsアプリケーションの作成
rails new myapp
cd myapp
rails server

MVC構造とルーティング

# config/routes.rb
Rails.application.routes.draw do
  root 'home#index'
  
  resources :users do
    member do
      get :profile
    end
    collection do
      get :search
    end
  end
  
  namespace :api do
    namespace :v1 do
      resources :posts, only: [:index, :show, :create, :update, :destroy]
    end
  end
end

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy, :profile]
  
  def index
    @users = User.all
  end
  
  def show
  end
  
  def new
    @user = User.new
  end
  
  def create
    @user = User.new(user_params)
    
    if @user.save
      redirect_to @user, notice: 'ユーザーが正常に作成されました。'
    else
      render :new, status: :unprocessable_entity
    end
  end
  
  def update
    if @user.update(user_params)
      redirect_to @user, notice: 'ユーザーが正常に更新されました。'
    else
      render :edit, status: :unprocessable_entity
    end
  end
  
  private
  
  def set_user
    @user = User.find(params[:id])
  end
  
  def user_params
    params.require(:user).permit(:name, :email, :age)
  end
end

Active Recordモデル

# app/models/user.rb
class User < ApplicationRecord
  has_many :posts, dependent: :destroy
  has_many :comments, through: :posts
  has_one_attached :avatar
  
  validates :name, presence: true, length: { minimum: 2, maximum: 50 }
  validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :age, presence: true, numericality: { greater_than: 0, less_than: 120 }
  
  scope :adults, -> { where('age >= ?', 18) }
  scope :by_name, ->(name) { where('name ILIKE ?', "%#{name}%") }
  
  before_save :normalize_email
  after_create :send_welcome_email
  
  def full_name
    "#{first_name} #{last_name}"
  end
  
  def adult?
    age >= 18
  end
  
  private
  
  def normalize_email
    self.email = email.downcase.strip
  end
  
  def send_welcome_email
    UserMailer.welcome(self).deliver_later
  end
end

# app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy
  has_rich_text :content
  has_many_attached :images
  
  validates :title, presence: true, length: { maximum: 100 }
  validates :content, presence: true
  
  scope :published, -> { where(published: true) }
  scope :recent, -> { order(created_at: :desc) }
  
  def excerpt(limit = 100)
    content.to_plain_text.truncate(limit)
  end
end

ビューとヘルパー

<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
  <head>
    <title>My Rails App</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    
    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
  </head>

  <body>
    <nav class="navbar">
      <%= link_to "Home", root_path, class: "nav-link" %>
      <%= link_to "Users", users_path, class: "nav-link" %>
      <% if user_signed_in? %>
        <%= link_to "Profile", current_user, class: "nav-link" %>
        <%= link_to "Logout", destroy_user_session_path, 
                    method: :delete, class: "nav-link" %>
      <% else %>
        <%= link_to "Login", new_user_session_path, class: "nav-link" %>
      <% end %>
    </nav>
    
    <main>
      <% flash.each do |type, message| %>
        <div class="alert alert-<%= type %>"><%= message %></div>
      <% end %>
      
      <%= yield %>
    </main>
  </body>
</html>

<!-- app/views/users/index.html.erb -->
<h1>ユーザー一覧</h1>

<%= link_to '新しいユーザー', new_user_path, class: 'btn btn-primary' %>

<div class="users-grid">
  <% @users.each do |user| %>
    <div class="user-card">
      <%= image_tag user.avatar, alt: user.name if user.avatar.attached? %>
      <h3><%= link_to user.name, user %></h3>
      <p><%= user.email %></p>
      <p>年齢: <%= user.age %>歳</p>
      
      <div class="actions">
        <%= link_to '詳細', user, class: 'btn btn-info' %>
        <%= link_to '編集', edit_user_path(user), class: 'btn btn-warning' %>
        <%= link_to '削除', user, method: :delete, 
                    confirm: '本当に削除しますか?', 
                    class: 'btn btn-danger' %>
      </div>
    </div>
  <% end %>
</div>

JSON APIとシリアライザー

# app/controllers/api/v1/posts_controller.rb
class Api::V1::PostsController < ApplicationController
  before_action :set_post, only: [:show, :update, :destroy]
  before_action :authenticate_user!, except: [:index, :show]
  
  def index
    @posts = Post.published.includes(:user).recent.page(params[:page])
    render json: @posts, include: [:user], meta: pagination_meta(@posts)
  end
  
  def show
    render json: @post, include: [:user, :comments]
  end
  
  def create
    @post = current_user.posts.build(post_params)
    
    if @post.save
      render json: @post, status: :created
    else
      render json: { errors: @post.errors }, status: :unprocessable_entity
    end
  end
  
  def update
    if @post.update(post_params)
      render json: @post
    else
      render json: { errors: @post.errors }, status: :unprocessable_entity
    end
  end
  
  def destroy
    @post.destroy
    head :no_content
  end
  
  private
  
  def set_post
    @post = Post.find(params[:id])
  end
  
  def post_params
    params.require(:post).permit(:title, :content, :published, images: [])
  end
  
  def pagination_meta(collection)
    {
      current_page: collection.current_page,
      total_pages: collection.total_pages,
      total_count: collection.total_count
    }
  end
end

# app/serializers/post_serializer.rb
class PostSerializer < ActiveModel::Serializer
  attributes :id, :title, :content, :published, :created_at, :updated_at
  
  belongs_to :user
  has_many :comments
  
  def content
    object.excerpt(200)
  end
end

バックグラウンドジョブ(Active Job)

# app/jobs/send_email_job.rb
class SendEmailJob < ApplicationJob
  queue_as :default
  
  def perform(user_id, email_type)
    user = User.find(user_id)
    
    case email_type
    when 'welcome'
      UserMailer.welcome(user).deliver_now
    when 'newsletter'
      UserMailer.newsletter(user).deliver_now
    else
      raise ArgumentError, "Unknown email type: #{email_type}"
    end
  end
end

# app/jobs/process_image_job.rb
class ProcessImageJob < ApplicationJob
  queue_as :high_priority
  
  retry_on StandardError, wait: 5.seconds, attempts: 3
  
  def perform(post_id)
    post = Post.find(post_id)
    
    post.images.each do |image|
      # 画像処理ロジック
      process_image(image)
    end
    
    post.update(processed: true)
  end
  
  private
  
  def process_image(image)
    # リサイズ、最適化などの処理
    puts "Processing image: #{image.filename}"
  end
end

# ジョブの実行
SendEmailJob.perform_later(user.id, 'welcome')
ProcessImageJob.set(wait: 5.minutes).perform_later(post.id)

テスト(RSpec)

# spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  describe 'validations' do
    it { should validate_presence_of(:name) }
    it { should validate_presence_of(:email) }
    it { should validate_uniqueness_of(:email) }
    it { should validate_numericality_of(:age).is_greater_than(0) }
  end
  
  describe 'associations' do
    it { should have_many(:posts).dependent(:destroy) }
    it { should have_one_attached(:avatar) }
  end
  
  describe 'scopes' do
    let!(:adult_user) { create(:user, age: 25) }
    let!(:minor_user) { create(:user, age: 16) }
    
    it 'returns only adult users' do
      expect(User.adults).to include(adult_user)
      expect(User.adults).not_to include(minor_user)
    end
  end
  
  describe '#adult?' do
    it 'returns true for users 18 and older' do
      user = build(:user, age: 18)
      expect(user.adult?).to be true
    end
    
    it 'returns false for users under 18' do
      user = build(:user, age: 17)
      expect(user.adult?).to be false
    end
  end
end

# spec/controllers/users_controller_spec.rb
require 'rails_helper'

RSpec.describe UsersController, type: :controller do
  describe 'GET #index' do
    it 'returns a success response' do
      get :index
      expect(response).to be_successful
    end
    
    it 'assigns all users to @users' do
      user = create(:user)
      get :index
      expect(assigns(:users)).to eq([user])
    end
  end
  
  describe 'POST #create' do
    context 'with valid parameters' do
      let(:valid_attributes) do
        { name: 'Test User', email: '[email protected]', age: 25 }
      end
      
      it 'creates a new User' do
        expect {
          post :create, params: { user: valid_attributes }
        }.to change(User, :count).by(1)
      end
      
      it 'redirects to the created user' do
        post :create, params: { user: valid_attributes }
        expect(response).to redirect_to(User.last)
      end
    end
    
    context 'with invalid parameters' do
      let(:invalid_attributes) { { name: '', email: 'invalid' } }
      
      it 'does not create a new User' do
        expect {
          post :create, params: { user: invalid_attributes }
        }.not_to change(User, :count)
      end
      
      it 'renders the new template' do
        post :create, params: { user: invalid_attributes }
        expect(response).to render_template(:new)
      end
    end
  end
end