rails-developer
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRuby on Rails Developer
Ruby on Rails 开发者
Expert in Ruby on Rails development with focus on Rails 8, Hotwire, and the Solid Trifecta stack.
专注于Rails 8、Hotwire及Solid Trifecta技术栈的Ruby on Rails开发专家。
Core Principles
核心原则
yaml
stack_philosophy:
framework: "Rails 8"
database: "SQLite3 (production-ready)"
background_jobs: "SolidQueue"
websockets: "SolidCable"
caching: "SolidCache"
frontend: "Hotwire (Turbo + Stimulus)"
styling: "Tailwind CSS"
code_conventions:
naming:
files: "snake_case"
methods: "snake_case"
classes: "CamelCase"
constants: "SCREAMING_SNAKE_CASE"
structure:
- "Follow Rails conventions"
- "RESTful routing"
- "Thin controllers, fat models"
- "Service objects for complex logic"yaml
stack_philosophy:
framework: "Rails 8"
database: "SQLite3 (production-ready)"
background_jobs: "SolidQueue"
websockets: "SolidCable"
caching: "SolidCache"
frontend: "Hotwire (Turbo + Stimulus)"
styling: "Tailwind CSS"
code_conventions:
naming:
files: "snake_case"
methods: "snake_case"
classes: "CamelCase"
constants: "SCREAMING_SNAKE_CASE"
structure:
- "Follow Rails conventions"
- "RESTful routing"
- "Thin controllers, fat models"
- "Service objects for complex logic"Rails 8 Features
Rails 8 特性
Authentication
身份验证
ruby
undefinedruby
undefinedGenerate built-in authentication
Generate built-in authentication
rails g authentication
rails g authentication
app/models/user.rb
app/models/user.rb
class User < ApplicationRecord
has_secure_password
normalizes :email, with: ->(email) { email.strip.downcase }
validates :email, presence: true,
uniqueness: true,
format: { with: URI::MailTo::EMAIL_REGEXP }
end
class User < ApplicationRecord
has_secure_password
normalizes :email, with: ->(email) { email.strip.downcase }
validates :email, presence: true,
uniqueness: true,
format: { with: URI::MailTo::EMAIL_REGEXP }
end
app/controllers/sessions_controller.rb
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
session[:user_id] = user.id
redirect_to root_path, notice: "Logged in!"
else
flash.now[:alert] = "Invalid email or password"
render :new, status: :unprocessable_entity
endend
def destroy
session[:user_id] = nil
redirect_to root_path, notice: "Logged out!"
end
end
undefinedclass SessionsController < ApplicationController
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
session[:user_id] = user.id
redirect_to root_path, notice: "Logged in!"
else
flash.now[:alert] = "Invalid email or password"
render :new, status: :unprocessable_entity
endend
def destroy
session[:user_id] = nil
redirect_to root_path, notice: "Logged out!"
end
end
undefinedSolid Trifecta
Solid Trifecta
ruby
undefinedruby
undefinedconfig/queue.yml - SolidQueue configuration
config/queue.yml - SolidQueue configuration
production:
workers:
- queues: "*"
threads: 5
polling_interval: 0.1
dispatchers:
- polling_interval: 1
batch_size: 500
production:
workers:
- queues: "*"
threads: 5
polling_interval: 0.1
dispatchers:
- polling_interval: 1
batch_size: 500
app/jobs/process_order_job.rb
app/jobs/process_order_job.rb
class ProcessOrderJob < ApplicationJob
queue_as :default
retry_on StandardError, wait: :polynomially_longer, attempts: 5
def perform(order_id)
order = Order.find(order_id)
OrderProcessor.new(order).process!
end
end
class ProcessOrderJob < ApplicationJob
queue_as :default
retry_on StandardError, wait: :polynomially_longer, attempts: 5
def perform(order_id)
order = Order.find(order_id)
OrderProcessor.new(order).process!
end
end
Using SolidCable for ActionCable
Using SolidCable for ActionCable
config/cable.yml
config/cable.yml
production:
adapter: solid_cable
polling_interval: 0.1
production:
adapter: solid_cable
polling_interval: 0.1
Using SolidCache
Using SolidCache
config/cache.yml
config/cache.yml
production:
store: solid_cache
size: 256.megabytes
undefinedproduction:
store: solid_cache
size: 256.megabytes
undefinedActiveRecord Patterns
ActiveRecord 模式
Models & Validations
模型与验证
ruby
undefinedruby
undefinedapp/models/product.rb
app/models/product.rb
class Product < ApplicationRecord
belongs_to :category
has_many :order_items, dependent: :restrict_with_error
has_many :orders, through: :order_items
has_one_attached :image
has_rich_text :description
enum :status, { draft: 0, published: 1, archived: 2 }
validates :name, presence: true, length: { maximum: 255 }
validates :price, presence: true, numericality: { greater_than: 0 }
validates :sku, presence: true, uniqueness: true
scope :available, -> { published.where("stock > 0") }
scope :featured, -> { where(featured: true).order(created_at: :desc) }
before_validation :generate_sku, on: :create
after_commit :update_search_index, on: [:create, :update]
private
def generate_sku
self.sku ||= "SKU-#{SecureRandom.hex(4).upcase}"
end
def update_search_index
SearchIndexJob.perform_later(self)
end
end
undefinedclass Product < ApplicationRecord
belongs_to :category
has_many :order_items, dependent: :restrict_with_error
has_many :orders, through: :order_items
has_one_attached :image
has_rich_text :description
enum :status, { draft: 0, published: 1, archived: 2 }
validates :name, presence: true, length: { maximum: 255 }
validates :price, presence: true, numericality: { greater_than: 0 }
validates :sku, presence: true, uniqueness: true
scope :available, -> { published.where("stock > 0") }
scope :featured, -> { where(featured: true).order(created_at: :desc) }
before_validation :generate_sku, on: :create
after_commit :update_search_index, on: [:create, :update]
private
def generate_sku
self.sku ||= "SKU-#{SecureRandom.hex(4).upcase}"
end
def update_search_index
SearchIndexJob.perform_later(self)
end
end
undefinedQuery Interface
查询接口
ruby
undefinedruby
undefinedEfficient queries
Efficient queries
class ProductQuery
def initialize(relation = Product.all)
@relation = relation
end
def search(term)
return self if term.blank?
@relation = @relation.where("name ILIKE :term OR description ILIKE :term",
term: "%#{term}%")
self
end
def by_category(category_id)
return self if category_id.blank?
@relation = @relation.where(category_id: category_id)
self
end
def price_range(min:, max:)
@relation = @relation.where(price: min..max) if min && max
self
end
def results
@relation
end
end
class ProductQuery
def initialize(relation = Product.all)
@relation = relation
end
def search(term)
return self if term.blank?
@relation = @relation.where("name ILIKE :term OR description ILIKE :term",
term: "%#{term}%")
self
end
def by_category(category_id)
return self if category_id.blank?
@relation = @relation.where(category_id: category_id)
self
end
def price_range(min:, max:)
@relation = @relation.where(price: min..max) if min && max
self
end
def results
@relation
end
end
Usage
Usage
products = ProductQuery.new
.search(params[:q])
.by_category(params[:category_id])
.price_range(min: 10, max: 100)
.results
.includes(:category, image_attachment: :blob)
.page(params[:page])
undefinedproducts = ProductQuery.new
.search(params[:q])
.by_category(params[:category_id])
.price_range(min: 10, max: 100)
.results
.includes(:category, image_attachment: :blob)
.page(params[:page])
undefinedMigrations
迁移
ruby
undefinedruby
undefineddb/migrate/20241201000000_create_orders.rb
db/migrate/20241201000000_create_orders.rb
class CreateOrders < ActiveRecord::Migration[8.0]
def change
create_table :orders do |t|
t.references :user, null: false, foreign_key: true
t.string :number, null: false, index: { unique: true }
t.integer :status, null: false, default: 0
t.decimal :total, precision: 10, scale: 2, null: false, default: 0
t.jsonb :metadata, null: false, default: {}
t.datetime :completed_at
t.timestamps
end
add_index :orders, :status
add_index :orders, :completed_at
add_index :orders, [:user_id, :status]end
end
undefinedclass CreateOrders < ActiveRecord::Migration[8.0]
def change
create_table :orders do |t|
t.references :user, null: false, foreign_key: true
t.string :number, null: false, index: { unique: true }
t.integer :status, null: false, default: 0
t.decimal :total, precision: 10, scale: 2, null: false, default: 0
t.jsonb :metadata, null: false, default: {}
t.datetime :completed_at
t.timestamps
end
add_index :orders, :status
add_index :orders, :completed_at
add_index :orders, [:user_id, :status]end
end
undefinedHotwire
Hotwire
Turbo Frames
Turbo Frames
erb
<!-- app/views/products/index.html.erb -->
<%= turbo_frame_tag "products" do %>
<div class="grid grid-cols-3 gap-4">
<% @products.each do |product| %>
<%= render product %>
<% end %>
</div>
<%= paginate @products %>
<% end %>
<!-- app/views/products/_product.html.erb -->
<%= turbo_frame_tag dom_id(product) do %>
<div class="card" data-controller="product">
<h3><%= product.name %></h3>
<p><%= number_to_currency product.price %></p>
<%= link_to "Edit", edit_product_path(product),
data: { turbo_frame: "modal" } %>
</div>
<% end %>erb
<!-- app/views/products/index.html.erb -->
<%= turbo_frame_tag "products" do %>
<div class="grid grid-cols-3 gap-4">
<% @products.each do |product| %>
<%= render product %>
<% end %>
</div>
<%= paginate @products %>
<% end %>
<!-- app/views/products/_product.html.erb -->
<%= turbo_frame_tag dom_id(product) do %>
<div class="card" data-controller="product">
<h3><%= product.name %></h3>
<p><%= number_to_currency product.price %></p>
<%= link_to "Edit", edit_product_path(product),
data: { turbo_frame: "modal" } %>
</div>
<% end %>Turbo Streams
Turbo Streams
ruby
undefinedruby
undefinedapp/controllers/comments_controller.rb
app/controllers/comments_controller.rb
class CommentsController < ApplicationController
def create
@comment = @post.comments.build(comment_params)
@comment.user = current_user
if @comment.save
respond_to do |format|
format.turbo_stream
format.html { redirect_to @post }
end
else
render :new, status: :unprocessable_entity
endend
end
class CommentsController < ApplicationController
def create
@comment = @post.comments.build(comment_params)
@comment.user = current_user
if @comment.save
respond_to do |format|
format.turbo_stream
format.html { redirect_to @post }
end
else
render :new, status: :unprocessable_entity
endend
end
app/views/comments/create.turbo_stream.erb
app/views/comments/create.turbo_stream.erb
<%= turbo_stream.prepend "comments", @comment %>
<%= turbo_stream.update "new_comment" do %>
<%= render "form", comment: Comment.new %>
<% end %>
<%= turbo_stream.update "comments_count", @post.comments.count %>
undefined<%= turbo_stream.prepend "comments", @comment %>
<%= turbo_stream.update "new_comment" do %>
<%= render "form", comment: Comment.new %>
<% end %>
<%= turbo_stream.update "comments_count", @post.comments.count %>
undefinedStimulus Controllers
Stimulus 控制器
javascript
// app/javascript/controllers/form_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["input", "submit", "output"]
static values = {
url: String,
debounce: { type: Number, default: 300 }
}
connect() {
this.timeout = null
}
search() {
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.performSearch()
}, this.debounceValue)
}
async performSearch() {
const query = this.inputTarget.value
if (query.length < 2) return
this.submitTarget.disabled = true
try {
const response = await fetch(`${this.urlValue}?q=${encodeURIComponent(query)}`)
const html = await response.text()
this.outputTarget.innerHTML = html
} finally {
this.submitTarget.disabled = false
}
}
}javascript
// app/javascript/controllers/form_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["input", "submit", "output"]
static values = {
url: String,
debounce: { type: Number, default: 300 }
}
connect() {
this.timeout = null
}
search() {
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.performSearch()
}, this.debounceValue)
}
async performSearch() {
const query = this.inputTarget.value
if (query.length < 2) return
this.submitTarget.disabled = true
try {
const response = await fetch(`${this.urlValue}?q=${encodeURIComponent(query)}`)
const html = await response.text()
this.outputTarget.innerHTML = html
} finally {
this.submitTarget.disabled = false
}
}
}Testing with RSpec
使用 RSpec 测试
ruby
undefinedruby
undefinedspec/models/product_spec.rb
spec/models/product_spec.rb
RSpec.describe Product, type: :model do
describe "validations" do
subject { build(:product) }
it { should validate_presence_of(:name) }
it { should validate_presence_of(:price) }
it { should validate_uniqueness_of(:sku) }
it { should validate_numericality_of(:price).is_greater_than(0) }end
describe "associations" do
it { should belong_to(:category) }
it { should have_many(:order_items) }
end
describe "scopes" do
describe ".available" do
let!(:available) { create(:product, status: :published, stock: 10) }
let!(:out_of_stock) { create(:product, status: :published, stock: 0) }
let!(:draft) { create(:product, status: :draft, stock: 10) }
it "returns only published products with stock" do
expect(Product.available).to contain_exactly(available)
end
endend
end
RSpec.describe Product, type: :model do
describe "validations" do
subject { build(:product) }
it { should validate_presence_of(:name) }
it { should validate_presence_of(:price) }
it { should validate_uniqueness_of(:sku) }
it { should validate_numericality_of(:price).is_greater_than(0) }end
describe "associations" do
it { should belong_to(:category) }
it { should have_many(:order_items) }
end
describe "scopes" do
describe ".available" do
let!(:available) { create(:product, status: :published, stock: 10) }
let!(:out_of_stock) { create(:product, status: :published, stock: 0) }
let!(:draft) { create(:product, status: :draft, stock: 10) }
it "returns only published products with stock" do
expect(Product.available).to contain_exactly(available)
end
endend
end
spec/requests/products_spec.rb
spec/requests/products_spec.rb
RSpec.describe "Products", type: :request do
describe "GET /products" do
it "returns successful response" do
get products_path
expect(response).to have_http_status(:success)
end
end
describe "POST /products" do
let(:valid_params) { { product: attributes_for(:product) } }
context "with valid params" do
it "creates a new product" do
expect {
post products_path, params: valid_params
}.to change(Product, :count).by(1)
end
endend
end
RSpec.describe "Products", type: :request do
describe "GET /products" do
it "returns successful response" do
get products_path
expect(response).to have_http_status(:success)
end
end
describe "POST /products" do
let(:valid_params) { { product: attributes_for(:product) } }
context "with valid params" do
it "creates a new product" do
expect {
post products_path, params: valid_params
}.to change(Product, :count).by(1)
end
endend
end
spec/system/products_spec.rb
spec/system/products_spec.rb
RSpec.describe "Product management", type: :system do
before { driven_by(:selenium_chrome_headless) }
it "allows creating a new product" do
visit new_product_path
fill_in "Name", with: "Test Product"
fill_in "Price", with: "99.99"
click_button "Create Product"
expect(page).to have_content("Product was successfully created")
expect(page).to have_content("Test Product")end
end
undefinedRSpec.describe "Product management", type: :system do
before { driven_by(:selenium_chrome_headless) }
it "allows creating a new product" do
visit new_product_path
fill_in "Name", with: "Test Product"
fill_in "Price", with: "99.99"
click_button "Create Product"
expect(page).to have_content("Product was successfully created")
expect(page).to have_content("Test Product")end
end
undefinedPerformance Optimization
性能优化
Caching
缓存
ruby
undefinedruby
undefinedFragment caching with Russian Doll strategy
Fragment caching with Russian Doll strategy
app/views/products/_product.html.erb
app/views/products/_product.html.erb
<% cache product do %>
<div class="product">
<% cache [product, "details"] do %>
<h3><%= product.name %></h3>
<p><%= product.description %></p>
<% end %>
<% cache [product.category, "category"] do %>
<span class="category"><%= product.category.name %></span>
<% end %><% cache product do %>
<div class="product">
<% cache [product, "details"] do %>
<h3><%= product.name %></h3>
<p><%= product.description %></p>
<% end %>
<% cache [product.category, "category"] do %>
<span class="category"><%= product.category.name %></span>
<% end %>Low-level caching
Low-level caching
class Product < ApplicationRecord
def expensive_calculation
Rails.cache.fetch([cache_key_with_version, "calculation"], expires_in: 1.hour) do
# Complex computation
perform_expensive_operation
end
end
end
class Product < ApplicationRecord
def expensive_calculation
Rails.cache.fetch([cache_key_with_version, "calculation"], expires_in: 1.hour) do
# Complex computation
perform_expensive_operation
end
end
end
Counter caching
Counter caching
class Comment < ApplicationRecord
belongs_to :post, counter_cache: true
end
undefinedclass Comment < ApplicationRecord
belongs_to :post, counter_cache: true
end
undefinedN+1 Prevention
N+1 查询问题预防
ruby
undefinedruby
undefinedapp/controllers/posts_controller.rb
app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
@posts = Post
.includes(:author, :comments, :tags)
.with_attached_image
.order(created_at: :desc)
.page(params[:page])
end
end
class PostsController < ApplicationController
def index
@posts = Post
.includes(:author, :comments, :tags)
.with_attached_image
.order(created_at: :desc)
.page(params[:page])
end
end
Using strict loading in development
Using strict loading in development
config/environments/development.rb
config/environments/development.rb
config.active_record.strict_loading_by_default = true
undefinedconfig.active_record.strict_loading_by_default = true
undefinedDatabase Optimization
数据库优化
ruby
undefinedruby
undefinedAdd proper indexes
Add proper indexes
class AddIndexesToProducts < ActiveRecord::Migration[8.0]
def change
add_index :products, :category_id
add_index :products, [:status, :created_at]
add_index :products, :price
# Partial index
add_index :products, :featured, where: "featured = true"
# GIN index for JSONB
add_index :products, :metadata, using: :ginend
end
class AddIndexesToProducts < ActiveRecord::Migration[8.0]
def change
add_index :products, :category_id
add_index :products, [:status, :created_at]
add_index :products, :price
# Partial index
add_index :products, :featured, where: "featured = true"
# GIN index for JSONB
add_index :products, :metadata, using: :ginend
end
Bulk operations
Bulk operations
Product.insert_all([
{ name: "Product 1", price: 10 },
{ name: "Product 2", price: 20 }
])
Product.where(status: :draft).update_all(status: :archived)
undefinedProduct.insert_all([
{ name: "Product 1", price: 10 },
{ name: "Product 2", price: 20 }
])
Product.where(status: :draft).update_all(status: :archived)
undefinedSecurity
安全防护
ruby
undefinedruby
undefinedStrong parameters
Strong parameters
class ProductsController < ApplicationController
private
def product_params
params.require(:product).permit(:name, :price, :description,
:category_id, :image, tags: [])
end
end
class ProductsController < ApplicationController
private
def product_params
params.require(:product).permit(:name, :price, :description,
:category_id, :image, tags: [])
end
end
Authorization
Authorization
class ApplicationController < ActionController::Base
before_action :authenticate_user!
private
def authorize_admin!
redirect_to root_path, alert: "Not authorized" unless current_user.admin?
end
end
class ApplicationController < ActionController::Base
before_action :authenticate_user!
private
def authorize_admin!
redirect_to root_path, alert: "Not authorized" unless current_user.admin?
end
end
Content Security Policy
Content Security Policy
config/initializers/content_security_policy.rb
config/initializers/content_security_policy.rb
Rails.application.configure do
config.content_security_policy do |policy|
policy.default_src :self, :https
policy.font_src :self, :https, :data
policy.img_src :self, :https, :data
policy.object_src :none
policy.script_src :self, :https
policy.style_src :self, :https, :unsafe_inline
end
end
undefinedRails.application.configure do
config.content_security_policy do |policy|
policy.default_src :self, :https
policy.font_src :self, :https, :data
policy.img_src :self, :https, :data
policy.object_src :none
policy.script_src :self, :https
policy.style_src :self, :https, :unsafe_inline
end
end
undefinedAPI Mode
API 模式
ruby
undefinedruby
undefinedapp/controllers/api/v1/base_controller.rb
app/controllers/api/v1/base_controller.rb
module Api
module V1
class BaseController < ActionController::API
include ActionController::HttpAuthentication::Token::ControllerMethods
before_action :authenticate_api_user!
private
def authenticate_api_user!
authenticate_or_request_with_http_token do |token, options|
@current_api_user = User.find_by(api_token: token)
end
end
def current_api_user
@current_api_user
end
endend
end
module Api
module V1
class BaseController < ActionController::API
include ActionController::HttpAuthentication::Token::ControllerMethods
before_action :authenticate_api_user!
private
def authenticate_api_user!
authenticate_or_request_with_http_token do |token, options|
@current_api_user = User.find_by(api_token: token)
end
end
def current_api_user
@current_api_user
end
endend
end
app/controllers/api/v1/products_controller.rb
app/controllers/api/v1/products_controller.rb
module Api
module V1
class ProductsController < BaseController
def index
products = Product.available.page(params[:page]).per(25)
render json: {
products: products.as_json(only: [:id, :name, :price]),
meta: {
current_page: products.current_page,
total_pages: products.total_pages,
total_count: products.total_count
}
}
end
def show
product = Product.find(params[:id])
render json: ProductSerializer.new(product).as_json
end
endend
end
undefinedmodule Api
module V1
class ProductsController < BaseController
def index
products = Product.available.page(params[:page]).per(25)
render json: {
products: products.as_json(only: [:id, :name, :price]),
meta: {
current_page: products.current_page,
total_pages: products.total_pages,
total_count: products.total_count
}
}
end
def show
product = Product.find(params[:id])
render json: ProductSerializer.new(product).as_json
end
endend
end
undefinedЛучшие практики
最佳实践
- Convention over Configuration — следуй Rails conventions
- Fat Model, Skinny Controller — логика в моделях и сервисах
- Hotwire first — минимум JavaScript, максимум Turbo
- Test everything — RSpec для моделей, запросов и системных тестов
- Cache strategically — Russian Doll caching для производительности
- Secure by default — strong parameters, CSP, аутентификация
- 约定优于配置 — 遵循Rails约定
- 胖模型,瘦控制器 — 逻辑放在模型和服务中
- 优先使用Hotwire — 最少JavaScript,最多Turbo
- 全面测试 — 使用RSpec进行模型、请求和系统测试
- 策略性缓存 — 俄罗斯套娃缓存提升性能
- 默认安全 — 强参数、CSP、身份验证