ruby-rails-application

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Ruby Rails Application

Ruby on Rails 应用程序

Overview

概述

Build comprehensive Ruby on Rails applications with proper model associations, RESTful controllers, Active Record queries, authentication systems, middleware chains, and view rendering following Rails conventions.
遵循Rails约定,构建包含完善模型关联、RESTful控制器、Active Record查询、身份验证系统、中间件链和视图渲染的Ruby on Rails应用程序。

When to Use

适用场景

  • Building Rails web applications
  • Implementing Active Record models with associations
  • Creating RESTful controllers and actions
  • Integrating authentication and authorization
  • Building complex database relationships
  • Implementing Rails middleware and filters
  • 构建Rails Web应用程序
  • 实现带有关联关系的Active Record模型
  • 创建RESTful控制器及动作
  • 集成身份验证与授权功能
  • 构建复杂的数据库关系
  • 实现Rails中间件与过滤器

Instructions

操作步骤

1. Rails Project Setup

1. Rails 项目搭建

bash
rails new myapp --api --database=postgresql
cd myapp
rails db:create
bash
rails new myapp --api --database=postgresql
cd myapp
rails db:create

2. Models with Active Record

2. 基于Active Record的模型

ruby
undefined
ruby
undefined

app/models/user.rb

app/models/user.rb

class User < ApplicationRecord has_many :posts, dependent: :destroy has_many :comments, dependent: :destroy
enum role: { user: 0, admin: 1 }
validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP } validates :password, presence: true, length: { minimum: 8 }, if: :new_record? validates :first_name, :last_name, presence: true
has_secure_password
before_save :downcase_email
def full_name "#{first_name} #{last_name}" end
def active? is_active end
private
def downcase_email self.email = email.downcase end end
class User < ApplicationRecord has_many :posts, dependent: :destroy has_many :comments, dependent: :destroy
enum role: { user: 0, admin: 1 }
validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP } validates :password, presence: true, length: { minimum: 8 }, if: :new_record? validates :first_name, :last_name, presence: true
has_secure_password
before_save :downcase_email
def full_name "#{first_name} #{last_name}" end
def active? is_active end
private
def downcase_email self.email = email.downcase end end

app/models/post.rb

app/models/post.rb

class Post < ApplicationRecord belongs_to :user has_many :comments, dependent: :destroy
enum status: { draft: 0, published: 1, archived: 2 }
validates :title, presence: true, length: { minimum: 1, maximum: 255 } validates :content, presence: true, length: { minimum: 1 } validates :user_id, presence: true
scope :published, -> { where(status: :published) } scope :recent, -> { order(created_at: :desc) } scope :by_author, ->(user_id) { where(user_id: user_id) }
def publish! update(status: :published) end
def unpublish! update(status: :draft) end end
class Post < ApplicationRecord belongs_to :user has_many :comments, dependent: :destroy
enum status: { draft: 0, published: 1, archived: 2 }
validates :title, presence: true, length: { minimum: 1, maximum: 255 } validates :content, presence: true, length: { minimum: 1 } validates :user_id, presence: true
scope :published, -> { where(status: :published) } scope :recent, -> { order(created_at: :desc) } scope :by_author, ->(user_id) { where(user_id: user_id) }
def publish! update(status: :published) end
def unpublish! update(status: :draft) end end

app/models/comment.rb

app/models/comment.rb

class Comment < ApplicationRecord belongs_to :user belongs_to :post
validates :content, presence: true, length: { minimum: 1 } validates :user_id, :post_id, presence: true
scope :recent, -> { order(created_at: :desc) } scope :by_author, ->(user_id) { where(user_id: user_id) } end
undefined
class Comment < ApplicationRecord belongs_to :user belongs_to :post
validates :content, presence: true, length: { minimum: 1 } validates :user_id, :post_id, presence: true
scope :recent, -> { order(created_at: :desc) } scope :by_author, ->(user_id) { where(user_id: user_id) } end
undefined

3. Database Migrations

3. 数据库迁移

ruby
undefined
ruby
undefined

db/migrate/20240101120000_create_users.rb

db/migrate/20240101120000_create_users.rb

class CreateUsers < ActiveRecord::Migration[7.0] def change create_table :users do |t| t.string :email, null: false t.string :password_digest, null: false t.string :first_name, null: false t.string :last_name, null: false t.integer :role, default: 0 t.boolean :is_active, default: true t.timestamps end
add_index :users, :email, unique: true
add_index :users, :role
end end
class CreateUsers < ActiveRecord::Migration[7.0] def change create_table :users do |t| t.string :email, null: false t.string :password_digest, null: false t.string :first_name, null: false t.string :last_name, null: false t.integer :role, default: 0 t.boolean :is_active, default: true t.timestamps end
add_index :users, :email, unique: true
add_index :users, :role
end end

db/migrate/20240101120001_create_posts.rb

db/migrate/20240101120001_create_posts.rb

class CreatePosts < ActiveRecord::Migration[7.0] def change create_table :posts do |t| t.string :title, null: false t.text :content, null: false t.integer :status, default: 0 t.references :user, null: false, foreign_key: true t.timestamps end
add_index :posts, :status
add_index :posts, [:user_id, :status]
end end
class CreatePosts < ActiveRecord::Migration[7.0] def change create_table :posts do |t| t.string :title, null: false t.text :content, null: false t.integer :status, default: 0 t.references :user, null: false, foreign_key: true t.timestamps end
add_index :posts, :status
add_index :posts, [:user_id, :status]
end end

db/migrate/20240101120002_create_comments.rb

db/migrate/20240101120002_create_comments.rb

class CreateComments < ActiveRecord::Migration[7.0] def change create_table :comments do |t| t.text :content, null: false t.references :user, null: false, foreign_key: true t.references :post, null: false, foreign_key: true t.timestamps end
add_index :comments, [:post_id, :created_at]
add_index :comments, [:user_id, :created_at]
end end
undefined
class CreateComments < ActiveRecord::Migration[7.0] def change create_table :comments do |t| t.text :content, null: false t.references :user, null: false, foreign_key: true t.references :post, null: false, foreign_key: true t.timestamps end
add_index :comments, [:post_id, :created_at]
add_index :comments, [:user_id, :created_at]
end end
undefined

4. Controllers with RESTful Actions

4. 带RESTful动作的控制器

ruby
undefined
ruby
undefined

app/controllers/api/v1/users_controller.rb

app/controllers/api/v1/users_controller.rb

module Api module V1 class UsersController < ApplicationController before_action :authenticate_request, except: [:create] before_action :set_user, only: [:show, :update, :destroy] before_action :authorize_user!, only: [:update, :destroy]
  def index
    users = User.all
    users = users.where("email ILIKE ?", "%#{params[:q]}%") if params[:q].present?
    users = users.page(params[:page]).per(params[:limit] || 20)

    render json: {
      data: users,
      pagination: pagination_data(users)
    }
  end

  def show
    render json: @user
  end

  def create
    user = User.new(user_params)

    if user.save
      token = encode_token(user.id)
      render json: {
        user: user,
        token: token
      }, status: :created
    else
      render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
    end
  end

  def update
    if @user.update(user_params)
      render json: @user
    else
      render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity
    end
  end

  def destroy
    @user.destroy
    head :no_content
  end

  private

  def set_user
    @user = User.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    render json: { error: 'User not found' }, status: :not_found
  end

  def authorize_user!
    unless current_user.id == @user.id || current_user.admin?
      render json: { error: 'Unauthorized' }, status: :forbidden
    end
  end

  def user_params
    params.require(:user).permit(:email, :password, :first_name, :last_name)
  end

  def pagination_data(collection)
    {
      page: collection.current_page,
      per_page: collection.limit_value,
      total: collection.total_count,
      total_pages: collection.total_pages
    }
  end
end
end end
module Api module V1 class UsersController < ApplicationController before_action :authenticate_request, except: [:create] before_action :set_user, only: [:show, :update, :destroy] before_action :authorize_user!, only: [:update, :destroy]
  def index
    users = User.all
    users = users.where("email ILIKE ?", "%#{params[:q]}%") if params[:q].present?
    users = users.page(params[:page]).per(params[:limit] || 20)

    render json: {
      data: users,
      pagination: pagination_data(users)
    }
  end

  def show
    render json: @user
  end

  def create
    user = User.new(user_params)

    if user.save
      token = encode_token(user.id)
      render json: {
        user: user,
        token: token
      }, status: :created
    else
      render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
    end
  end

  def update
    if @user.update(user_params)
      render json: @user
    else
      render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity
    end
  end

  def destroy
    @user.destroy
    head :no_content
  end

  private

  def set_user
    @user = User.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    render json: { error: 'User not found' }, status: :not_found
  end

  def authorize_user!
    unless current_user.id == @user.id || current_user.admin?
      render json: { error: 'Unauthorized' }, status: :forbidden
    end
  end

  def user_params
    params.require(:user).permit(:email, :password, :first_name, :last_name)
  end

  def pagination_data(collection)
    {
      page: collection.current_page,
      per_page: collection.limit_value,
      total: collection.total_count,
      total_pages: collection.total_pages
    }
  end
end
end end

app/controllers/api/v1/posts_controller.rb

app/controllers/api/v1/posts_controller.rb

module Api module V1 class PostsController < ApplicationController before_action :authenticate_request, except: [:index, :show] before_action :set_post, only: [:show, :update, :destroy, :publish] before_action :authorize_post_owner!, only: [:update, :destroy, :publish]
  def index
    posts = Post.published.recent
    posts = posts.by_author(params[:author_id]) if params[:author_id].present?
    posts = posts.where("title ILIKE ?", "%#{params[:q]}%") if params[:q].present?
    posts = posts.page(params[:page]).per(params[:limit] || 20)

    render json: {
      data: posts,
      pagination: pagination_data(posts)
    }
  end

  def show
    if @post.published? || current_user&.id == @post.user_id
      render json: @post
    else
      render json: { error: 'Post not found' }, status: :not_found
    end
  end

  def create
    @post = current_user.posts.build(post_params)

    if @post.save
      render json: @post, status: :created
    else
      render json: { errors: @post.errors.full_messages }, status: :unprocessable_entity
    end
  end

  def update
    if @post.update(post_params)
      render json: @post
    else
      render json: { errors: @post.errors.full_messages }, status: :unprocessable_entity
    end
  end

  def destroy
    @post.destroy
    head :no_content
  end

  def publish
    @post.publish!
    render json: @post
  end

  private

  def set_post
    @post = Post.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    render json: { error: 'Post not found' }, status: :not_found
  end

  def authorize_post_owner!
    unless current_user.id == @post.user_id || current_user.admin?
      render json: { error: 'Unauthorized' }, status: :forbidden
    end
  end

  def post_params
    params.require(:post).permit(:title, :content, :status)
  end

  def pagination_data(collection)
    {
      page: collection.current_page,
      per_page: collection.limit_value,
      total: collection.total_count
    }
  end
end
end end
undefined
module Api module V1 class PostsController < ApplicationController before_action :authenticate_request, except: [:index, :show] before_action :set_post, only: [:show, :update, :destroy, :publish] before_action :authorize_post_owner!, only: [:update, :destroy, :publish]
  def index
    posts = Post.published.recent
    posts = posts.by_author(params[:author_id]) if params[:author_id].present?
    posts = posts.where("title ILIKE ?", "%#{params[:q]}%") if params[:q].present?
    posts = posts.page(params[:page]).per(params[:limit] || 20)

    render json: {
      data: posts,
      pagination: pagination_data(posts)
    }
  end

  def show
    if @post.published? || current_user&.id == @post.user_id
      render json: @post
    else
      render json: { error: 'Post not found' }, status: :not_found
    end
  end

  def create
    @post = current_user.posts.build(post_params)

    if @post.save
      render json: @post, status: :created
    else
      render json: { errors: @post.errors.full_messages }, status: :unprocessable_entity
    end
  end

  def update
    if @post.update(post_params)
      render json: @post
    else
      render json: { errors: @post.errors.full_messages }, status: :unprocessable_entity
    end
  end

  def destroy
    @post.destroy
    head :no_content
  end

  def publish
    @post.publish!
    render json: @post
  end

  private

  def set_post
    @post = Post.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    render json: { error: 'Post not found' }, status: :not_found
  end

  def authorize_post_owner!
    unless current_user.id == @post.user_id || current_user.admin?
      render json: { error: 'Unauthorized' }, status: :forbidden
    end
  end

  def post_params
    params.require(:post).permit(:title, :content, :status)
  end

  def pagination_data(collection)
    {
      page: collection.current_page,
      per_page: collection.limit_value,
      total: collection.total_count
    }
  end
end
end end
undefined

5. Authentication with JWT

5. 基于JWT的身份验证

ruby
undefined
ruby
undefined

app/controllers/application_controller.rb

app/controllers/application_controller.rb

class ApplicationController < ActionController::API include ActionController::Cookies
SECRET_KEY = Rails.application.secrets.secret_key_base
def encode_token(user_id) payload = { user_id: user_id, exp: 24.hours.from_now.to_i } JWT.encode(payload, SECRET_KEY, 'HS256') end
def decode_token(token) begin JWT.decode(token, SECRET_KEY, true, { algorithm: 'HS256' }) rescue JWT::ExpiredSignature, JWT::DecodeError nil end end
def authenticate_request header = request.headers['Authorization'] token = header.split(' ').last if header.present?
decoded = decode_token(token)
if decoded
  @current_user_id = decoded[0]['user_id']
  @current_user = User.find(@current_user_id)
else
  render json: { error: 'Unauthorized' }, status: :unauthorized
end
end
def current_user @current_user end
def logged_in? current_user.present? end end
class ApplicationController < ActionController::API include ActionController::Cookies
SECRET_KEY = Rails.application.secrets.secret_key_base
def encode_token(user_id) payload = { user_id: user_id, exp: 24.hours.from_now.to_i } JWT.encode(payload, SECRET_KEY, 'HS256') end
def decode_token(token) begin JWT.decode(token, SECRET_KEY, true, { algorithm: 'HS256' }) rescue JWT::ExpiredSignature, JWT::DecodeError nil end end
def authenticate_request header = request.headers['Authorization'] token = header.split(' ').last if header.present?
decoded = decode_token(token)
if decoded
  @current_user_id = decoded[0]['user_id']
  @current_user = User.find(@current_user_id)
else
  render json: { error: 'Unauthorized' }, status: :unauthorized
end
end
def current_user @current_user end
def logged_in? current_user.present? end end

config/routes.rb

config/routes.rb

Rails.application.routes.draw do namespace :api do namespace :v1 do post 'auth/login', to: 'auth#login' post 'auth/register', to: 'auth#register'
  resources :users
  resources :posts do
    member do
      patch :publish
    end
    resources :comments, only: [:index, :create, :destroy]
  end
end
end end
undefined
Rails.application.routes.draw do namespace :api do namespace :v1 do post 'auth/login', to: 'auth#login' post 'auth/register', to: 'auth#register'
  resources :users
  resources :posts do
    member do
      patch :publish
    end
    resources :comments, only: [:index, :create, :destroy]
  end
end
end end
undefined

6. Active Record Queries

6. Active Record 查询

ruby
undefined
ruby
undefined

app/services/post_service.rb

app/services/post_service.rb

class PostService def self.get_user_posts(user_id, status: nil) posts = Post.by_author(user_id) posts = posts.where(status: status) if status.present? posts.recent end
def self.trending_posts(limit: 10) Post.published .joins(:comments) .group('posts.id') .order('COUNT(comments.id) DESC') .limit(limit) end
def self.search_posts(query) Post.published .where("title ILIKE ? OR content ILIKE ?", "%#{query}%", "%#{query}%") .recent end
def self.archive_old_drafts(days: 30) Post.where(status: :draft) .where('created_at < ?', days.days.ago) .update_all(status: :archived) end end
class PostService def self.get_user_posts(user_id, status: nil) posts = Post.by_author(user_id) posts = posts.where(status: status) if status.present? posts.recent end
def self.trending_posts(limit: 10) Post.published .joins(:comments) .group('posts.id') .order('COUNT(comments.id) DESC') .limit(limit) end
def self.search_posts(query) Post.published .where("title ILIKE ? OR content ILIKE ?", "%#{query}%", "%#{query}%") .recent end
def self.archive_old_drafts(days: 30) Post.where(status: :draft) .where('created_at < ?', days.days.ago) .update_all(status: :archived) end end

Usage

Usage

posts = Post.includes(:user).recent.limit(10) recent_comments = Comment.where(post_id: post.id).order(created_at: :desc).limit(5)
undefined
posts = Post.includes(:user).recent.limit(10) recent_comments = Comment.where(post_id: post.id).order(created_at: :desc).limit(5)
undefined

7. Serializers

7. 序列化器

ruby
undefined
ruby
undefined

app/serializers/user_serializer.rb

app/serializers/user_serializer.rb

class UserSerializer def initialize(user) @user = user end
def to_json { id: @user.id, email: @user.email, first_name: @user.first_name, last_name: @user.last_name, full_name: @user.full_name, role: @user.role, is_active: @user.is_active, created_at: @user.created_at.iso8601, updated_at: @user.updated_at.iso8601 } end end
class UserSerializer def initialize(user) @user = user end
def to_json { id: @user.id, email: @user.email, first_name: @user.first_name, last_name: @user.last_name, full_name: @user.full_name, role: @user.role, is_active: @user.is_active, created_at: @user.created_at.iso8601, updated_at: @user.updated_at.iso8601 } end end

In controller

In controller

def show render json: UserSerializer.new(@user).to_json end
undefined
def show render json: UserSerializer.new(@user).to_json end
undefined

Best Practices

最佳实践

✅ DO

✅ 建议

  • Use conventions over configuration
  • Leverage Active Record associations
  • Implement proper scopes for queries
  • Use strong parameters for security
  • Implement authentication in ApplicationController
  • Use services for complex business logic
  • Implement proper error handling
  • Use database migrations for schema changes
  • Validate all inputs at model level
  • Use before_action filters appropriately
  • 遵循"约定优于配置"原则
  • 充分利用Active Record关联
  • 为查询实现合适的作用域
  • 使用强参数保障安全
  • 在ApplicationController中实现身份验证
  • 使用服务层处理复杂业务逻辑
  • 实现完善的错误处理
  • 使用数据库迁移管理Schema变更
  • 在模型层验证所有输入
  • 合理使用before_action过滤器

❌ DON'T

❌ 禁忌

  • Use raw SQL without parameterization
  • Implement business logic in controllers
  • Trust user input without validation
  • Store secrets in code
  • Use select * without specifying columns
  • Forget N+1 query problems (use includes/joins)
  • Implement authentication in each controller
  • Use global variables
  • Ignore database constraints
  • 使用未参数化的原生SQL
  • 在控制器中实现业务逻辑
  • 信任未经过验证的用户输入
  • 在代码中存储敏感信息
  • 使用select *而不指定列
  • 忽略N+1查询问题(使用includes/joins解决)
  • 在每个控制器中单独实现身份验证
  • 使用全局变量
  • 忽略数据库约束

Complete Example

完整示例

ruby
undefined
ruby
undefined

Gemfile

Gemfile

source 'https://rubygems.org' gem 'rails', '> 7.0.0' gem 'pg', '> 1.1' gem 'bcrypt', '~> 3.1.7' gem 'jwt' gem 'kaminari'
source 'https://rubygems.org' gem 'rails', '> 7.0.0' gem 'pg', '> 1.1' gem 'bcrypt', '~> 3.1.7' gem 'jwt' gem 'kaminari'

models.rb + controllers.rb (see sections above)

models.rb + controllers.rb (see sections above)

routes.rb and migrations (see sections above)

routes.rb and migrations (see sections above)

undefined
",