强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Ruby 入门指南 / 第 17 章:Rails 入门

第 17 章:Rails 入门

“Rails 让 Web 开发变得简单而愉快。” —— DHH


17.1 Rails 概述

17.1.1 Rails 是什么

Ruby on Rails(简称 Rails)是一个全栈 Web 框架,由 David Heinemeier Hansson(DHH)于 2004 年创建。

核心原则说明
约定优于配置合理的默认值,减少配置
DRYDon’t Repeat Yourself
REST资源导向的架构
约定路由resources 自动生成 RESTful 路由

17.1.2 MVC 架构

┌─────────────────────────────────────────┐
│                  Rails                  │
├─────────────────────────────────────────┤
│  浏览器请求 → 路由 → 控制器 → 模型 → 视图  │
│                                         │
│  Model(模型)      - ActiveRecord       │
│  View(视图)       - ERB / Haml         │
│  Controller(控制器)- Action Controller  │
└─────────────────────────────────────────┘

17.2 安装和创建

17.2.1 安装 Rails

# 安装 Rails
gem install rails

# 验证
rails --version

# 创建新项目
rails new myapp
rails new myapp --database=postgresql
rails new myapp --api  # API 模式
rails new myapp --skip-test --skip-system-test

17.2.2 项目结构

myapp/
├── app/               # 应用代码
│   ├── controllers/   # 控制器
│   ├── models/        # 模型
│   ├── views/         # 视图
│   ├── helpers/       # 辅助方法
│   ├── javascript/    # 前端代码
│   └── assets/        # 静态资源
├── config/            # 配置文件
│   ├── routes.rb      # 路由
│   ├── database.yml   # 数据库配置
│   └── environments/  # 环境配置
├── db/                # 数据库
│   └── migrate/       # 迁移文件
├── spec/ 或 test/     # 测试
├── Gemfile            # 依赖
└── bin/               # 脚本
    └── rails          # Rails 命令

17.2.3 常用命令

# 启动服务器
rails server
rails s

# 控制台
rails console
rails c

# 数据库
rails db:create
rails db:migrate
rails db:seed
rails db:rollback

# 生成器
rails generate model User name:string email:string
rails generate controller Users index show
rails generate migration AddAgeToUsers age:integer
rails generate scaffold Post title:string body:text

# 路由
rails routes
rails routes | grep users

# 测试
rails test
rails test:system

17.3 路由

17.3.1 基本路由

# config/routes.rb
Rails.application.routes.draw do
  # 基本路由
  get "/about", to: "pages#about"
  post "/contact", to: "pages#contact"
  
  # 多个 HTTP 方法
  match "/path", to: "controller#action", via: [:get, :post]
  
  # 资源路由(RESTful)
  resources :users
  
  # 有限资源
  resources :posts, only: [:index, :show, :create]
  resources :comments, except: [:destroy]
  
  # 嵌套资源
  resources :users do
    resources :posts
  end
  
  # 命名空间
  namespace :admin do
    resources :users
    resources :posts
  end
  
  # 自定义路由
  get "search", to: "search#index"
  post "login", to: "sessions#create"
  delete "logout", to: "sessions#destroy"
  
  # 根路由
  root "pages#home"
end

17.3.2 RESTful 路由

resources :users
# 生成以下路由:
# GET    /users          → users#index
# GET    /users/new      → users#new
# POST   /users          → users#create
# GET    /users/:id      → users#show
# GET    /users/:id/edit → users#edit
# PATCH  /users/:id      → users#update
# DELETE /users/:id      → users#destroy

# 辅助方法
# users_path        → /users
# user_path(@user)  → /users/1
# new_user_path     → /users/new
# edit_user_path(@user) → /users/1/edit

17.4 控制器

17.4.1 基本控制器

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]

  def index
    @users = User.all
    @users = @users.where(active: true) if params[:active]
    @users = @users.order(created_at: :desc).page(params[:page])
  end

  def show
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)

    if @user.save
      redirect_to @user, notice: "User created successfully."
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
  end

  def update
    if @user.update(user_params)
      redirect_to @user, notice: "User updated successfully."
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @user.destroy
    redirect_to users_path, notice: "User deleted."
  end

  private

  def set_user
    @user = User.find(params[:id])
  end

  def user_params
    params.require(:user).permit(:name, :email, :age, :active)
  end
end

17.4.2 渲染和重定向

class PagesController < ApplicationController
  def home
    # 默认渲染 app/views/pages/home.html.erb
  end

  def about
    render :about  # 显式渲染
  end

  def search
    @results = Search.perform(params[:q])
    
    respond_to do |format|
      format.html  # 渲染 HTML 模板
      format.json { render json: @results }
      format.xml  { render xml: @results }
    end
  end

  def redirect_example
    redirect_to root_path
    redirect_to user_path(@user), status: :moved_permanently
    redirect_back(fallback_location: root_path)
  end
end

17.5 ActiveRecord

17.5.1 模型基础

# app/models/user.rb
class User < ApplicationRecord
  # 关联
  has_many :posts, dependent: :destroy
  has_many :comments, through: :posts
  belongs_to :organization, optional: true

  # 验证
  validates :name, presence: true, length: { minimum: 2, maximum: 50 }
  validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :age, numericality: { greater_than: 0, less_than: 150 }, allow_nil: true

  # 回调
  before_save :normalize_email
  after_create :send_welcome_email

  # 作用域
  scope :active, -> { where(active: true) }
  scope :adults, -> { where("age >= ?", 18) }
  scope :recent, -> { where("created_at > ?", 1.week.ago) }

  # 类方法
  def self.search(query)
    where("name LIKE ? OR email LIKE ?", "%#{query}%", "%#{query}%")
  end

  # 实例方法
  def full_name
    "#{first_name} #{last_name}"
  end

  private

  def normalize_email
    self.email = email.downcase.strip
  end

  def send_welcome_email
    UserMailer.welcome(self).deliver_later
  end
end

17.5.2 查询

# 基本查询
User.all
User.find(1)
User.find_by(email: "alice@example.com")
User.where(active: true)
User.where("age > ?", 18)

# 链式查询
User.active.adults.recent.order(name: :asc).limit(10)

# 聚合
User.count
User.average(:age)
User.maximum(:age)
User.minimum(:age)
User.group(:role).count

# 复杂查询
User.includes(:posts).where(posts: { published: true })
User.joins(:posts).where("posts.created_at > ?", 1.month.ago)
User.left_joins(:comments).group("users.id").having("COUNT(comments.id) > ?", 5)

# 原始 SQL
User.find_by_sql("SELECT * FROM users WHERE age > 18")

# 分页
User.page(1).per(25)  # Kaminari
User.where(active: true).page(params[:page])

17.5.3 CRUD 操作

# 创建
user = User.new(name: "Alice", email: "alice@example.com")
user.save

User.create(name: "Alice", email: "alice@example.com")
User.create!(name: "Alice", email: "alice@example.com")  # 失败时抛异常

# 读取
User.find(1)
User.find_by(email: "alice@example.com")
User.first
User.last
User.where(active: true)

# 更新
user.update(name: "Bob")
user.update!(name: "Bob")
user.update_columns(name: "Bob")  # 跳过验证和回调
User.update_all(active: true)

# 删除
user.destroy
user.destroy!
User.destroy_all
User.delete_all  # 跳过回调

17.6 数据库迁移

17.6.1 生成迁移

# 生成迁移
rails generate migration CreateUsers name:string email:string age:integer
rails generate migration AddRoleToUsers role:string
rails generate migration AddIndexToUsersEmail

17.6.2 编写迁移

# db/migrate/20240115120000_create_users.rb
class CreateUsers < ActiveRecord::Migration[7.1]
  def change
    create_table :users do |t|
      t.string :name, null: false
      t.string :email, null: false
      t.integer :age
      t.string :role, default: "user"
      t.boolean :active, default: true
      t.timestamps
    end

    add_index :users, :email, unique: true
    add_index :users, :role
  end
end

# db/migrate/20240116120000_add_posts_table.rb
class AddPostsTable < ActiveRecord::Migration[7.1]
  def change
    create_table :posts do |t|
      t.references :user, null: false, foreign_key: true
      t.string :title, null: false
      t.text :body
      t.boolean :published, default: false
      t.datetime :published_at
      t.timestamps
    end

    add_index :posts, :published
    add_index :posts, [:user_id, :published]
  end
end

# 复杂迁移
class MigrateUserData < ActiveRecord::Migration[7.1]
  def up
    User.find_each do |user|
      user.update_columns(
        first_name: user.name.split(" ").first,
        last_name: user.name.split(" ").last
      )
    end
  end

  def down
    User.find_each do |user|
      user.update_columns(name: "#{user.first_name} #{user.last_name}")
    end
  end
end

17.6.3 迁移命令

# 运行迁移
rails db:migrate

# 回滚
rails db:rollback
rails db:rollback STEP=3  # 回滚 3 步

# 重置数据库
rails db:reset  # drop + create + migrate + seed

# 迁移状态
rails db:migrate:status

# 生成迁移时指定类型
rails generate migration AddIndex \
  field:type{options} \
  --no-test-framework

17.7 视图

17.7.1 ERB 模板

<!-- app/views/users/index.html.erb -->
<h1>Users</h1>

<%= link_to "New User", new_user_path, class: "btn btn-primary" %>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Email</th>
      <th>Actions</th>
    </tr>
  </thead>
  <tbody>
    <% @users.each do |user| %>
      <tr>
        <td><%= user.name %></td>
        <td><%= user.email %></td>
        <td>
          <%= link_to "Show", user_path(user) %>
          <%= link_to "Edit", edit_user_path(user) %>
          <%= button_to "Delete", user_path(user), method: :delete, 
              data: { confirm: "Are you sure?" } %>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

<%= paginate @users %>
<!-- app/views/users/_form.html.erb -->
<%= form_with(model: user) do |form| %>
  <% if user.errors.any? %>
    <div class="errors">
      <h2><%= pluralize(user.errors.count, "error") %>:</h2>
      <ul>
        <% user.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :name %>
    <%= form.text_field :name, class: "form-control" %>
  </div>

  <div class="field">
    <%= form.label :email %>
    <%= form.email_field :email, class: "form-control" %>
  </div>

  <div class="field">
    <%= form.label :age %>
    <%= form.number_field :age, class: "form-control" %>
  </div>

  <%= form.submit class: "btn btn-primary" %>
<% end %>

17.8 动手练习

  1. 创建博客系统
rails new blog
cd blog
rails generate scaffold Post title:string body:text
rails db:migrate
rails server
  1. 添加评论功能
rails generate model Comment post:references body:text
rails db:migrate
# 实现嵌套路由和表单
  1. 添加验证和测试
# 为 Post 模型添加验证
# 编写集成测试

17.9 本章小结

要点说明
MVCModel-View-Controller 架构模式
路由resources 生成 RESTful 路由
控制器处理请求、协调模型和视图
ActiveRecordORM 框架,映射数据库表
迁移版本控制数据库结构
视图ERB 模板渲染 HTML

📖 扩展阅读


上一章← 第 16 章:Gem 开发与管理 下一章第 18 章:Sinatra 轻量 Web →