rails-active-record-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRails Active Record Patterns
Rails Active Record 模式
Master Active Record patterns for building robust Rails models with proper
associations, validations, scopes, and query optimization.
掌握Active Record模式,构建具备合理关联、验证、作用域及查询优化的健壮Rails模型。
Overview
概述
Active Record is Rails' Object-Relational Mapping (ORM) layer that connects
model classes to database tables. It implements the Active Record pattern,
where each object instance represents a row in the database and includes both
data and behavior.
Active Record是Rails的对象关系映射(ORM)层,用于将模型类与数据库表关联起来。它实现了Active Record模式,其中每个对象实例代表数据库中的一行记录,同时包含数据与行为。
Installation and Setup
安装与配置
Creating Models
创建模型
bash
undefinedbash
undefinedGenerate a model with migrations
Generate a model with migrations
rails generate model User name:string email:string:uniq
rails generate model User name:string email:string:uniq
Generate model with associations
Generate model with associations
rails generate model Post title:string body:text user:references
rails generate model Post title:string body:text user:references
Run migrations
Run migrations
rails db:migrate
undefinedrails db:migrate
undefinedDatabase Configuration
数据库配置
yaml
undefinedyaml
undefinedconfig/database.yml
config/database.yml
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
development:
<<: *default
database: myapp_development
test:
<<: *default
database: myapp_test
production:
<<: *default
database: myapp_production
username: myapp
password: <%= ENV['MYAPP_DATABASE_PASSWORD'] %>
undefineddefault: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
development:
<<: *default
database: myapp_development
test:
<<: *default
database: myapp_test
production:
<<: *default
database: myapp_production
username: myapp
password: <%= ENV['MYAPP_DATABASE_PASSWORD'] %>
undefinedCore Patterns
核心模式
1. Basic Model Definition
1. 基础模型定义
ruby
undefinedruby
undefinedapp/models/user.rb
app/models/user.rb
class User < ApplicationRecord
Validations
validates :email, presence: true, uniqueness: true,
format: { with: URI::MailTo::EMAIL_REGEXP }
validates :name, presence: true, length: { minimum: 2, maximum: 50 }
Callbacks
before_save :normalize_email
after_create :send_welcome_email
Scopes
scope :active, -> { where(active: true) }
scope :recent, -> { order(created_at: :desc).limit(10) }
private
def normalize_email
self.email = email.downcase.strip
end
def send_welcome_email
UserMailer.welcome(self).deliver_later
end
end
undefinedclass User < ApplicationRecord
Validations
validates :email, presence: true, uniqueness: true,
format: { with: URI::MailTo::EMAIL_REGEXP }
validates :name, presence: true, length: { minimum: 2, maximum: 50 }
Callbacks
before_save :normalize_email
after_create :send_welcome_email
Scopes
scope :active, -> { where(active: true) }
scope :recent, -> { order(created_at: :desc).limit(10) }
private
def normalize_email
self.email = email.downcase.strip
end
def send_welcome_email
UserMailer.welcome(self).deliver_later
end
end
undefined2. Associations
2. 关联关系
ruby
undefinedruby
undefinedapp/models/user.rb
app/models/user.rb
class User < ApplicationRecord
One-to-many
has_many :posts, dependent: :destroy
has_many :comments, dependent: :destroy
Many-to-many through join table
has_many :memberships, dependent: :destroy
has_many :organizations, through: :memberships
Has-one
has_one :profile, dependent: :destroy
Polymorphic association
has_many :images, as: :imageable, dependent: :destroy
end
class User < ApplicationRecord
One-to-many
has_many :posts, dependent: :destroy
has_many :comments, dependent: :destroy
Many-to-many through join table
has_many :memberships, dependent: :destroy
has_many :organizations, through: :memberships
Has-one
has_one :profile, dependent: :destroy
Polymorphic association
has_many :images, as: :imageable, dependent: :destroy
end
app/models/post.rb
app/models/post.rb
class Post < ApplicationRecord
belongs_to :user
has_many :comments, dependent: :destroy
has_many :commenters, through: :comments, source: :user
Counter cache
belongs_to :user, counter_cache: true
end
class Post < ApplicationRecord
belongs_to :user
has_many :comments, dependent: :destroy
has_many :commenters, through: :comments, source: :user
Counter cache
belongs_to :user, counter_cache: true
end
app/models/organization.rb
app/models/organization.rb
class Organization < ApplicationRecord
has_many :memberships, dependent: :destroy
has_many :users, through: :memberships
end
class Organization < ApplicationRecord
has_many :memberships, dependent: :destroy
has_many :users, through: :memberships
end
app/models/membership.rb
app/models/membership.rb
class Membership < ApplicationRecord
belongs_to :user
belongs_to :organization
enum role: { member: 0, admin: 1, owner: 2 }
end
undefinedclass Membership < ApplicationRecord
belongs_to :user
belongs_to :organization
enum role: { member: 0, admin: 1, owner: 2 }
end
undefined3. Advanced Queries
3. 高级查询
ruby
undefinedruby
undefinedapp/models/post.rb
app/models/post.rb
class Post < ApplicationRecord
Scopes with arguments
scope :by_author, ->(user_id) { where(user_id: user_id) }
scope :published_after, ->(date) { where('published_at > ?', date) }
scope :with_tag, ->(tag) { joins(:tags).where(tags: { name: tag }) }
Class methods for complex queries
def self.popular(threshold = 100)
where('views_count >= ?', threshold)
.order(views_count: :desc)
end
def self.search(query)
where('title ILIKE ? OR body ILIKE ?', "%#{query}%", "%#{query}%")
end
Query with joins and includes
def self.with_user_and_comments
includes(:user, comments: :user)
.order(created_at: :desc)
end
end
class Post < ApplicationRecord
Scopes with arguments
scope :by_author, ->(user_id) { where(user_id: user_id) }
scope :published_after, ->(date) { where('published_at > ?', date) }
scope :with_tag, ->(tag) { joins(:tags).where(tags: { name: tag }) }
Class methods for complex queries
def self.popular(threshold = 100)
where('views_count >= ?', threshold)
.order(views_count: :desc)
end
def self.search(query)
where('title ILIKE ? OR body ILIKE ?', "%#{query}%", "%#{query}%")
end
Query with joins and includes
def self.with_user_and_comments
includes(:user, comments: :user)
.order(created_at: :desc)
end
end
Usage
Usage
Post.published_after(1.week.ago)
.by_author(current_user.id)
.with_tag('rails')
.popular(50)
undefinedPost.published_after(1.week.ago)
.by_author(current_user.id)
.with_tag('rails')
.popular(50)
undefined4. Validations
4. 验证机制
ruby
undefinedruby
undefinedapp/models/user.rb
app/models/user.rb
class User < ApplicationRecord
Presence validation
validates :email, :name, presence: true
Uniqueness validation
validates :email, uniqueness: { case_sensitive: false }
Format validation
validates :username, format: {
with: /\A[a-z0-9_]+\z/,
message: "only allows lowercase letters, numbers, and underscores"
}
Length validation
validates :bio, length: { maximum: 500 }
validates :password, length: { minimum: 8 }, if: :password_required?
Numericality validation
validates :age, numericality: {
only_integer: true,
greater_than_or_equal_to: 18,
less_than: 120
}
Custom validation
validate :email_domain_allowed
private
def email_domain_allowed
return if email.blank?
domain = email.split('@').last
unless ALLOWED_DOMAINS.include?(domain)
errors.add(:email, "domain #{domain} is not allowed")
endend
def password_required?
new_record? || password.present?
end
end
undefinedclass User < ApplicationRecord
Presence validation
validates :email, :name, presence: true
Uniqueness validation
validates :email, uniqueness: { case_sensitive: false }
Format validation
validates :username, format: {
with: /\A[a-z0-9_]+\z/,
message: "only allows lowercase letters, numbers, and underscores"
}
Length validation
validates :bio, length: { maximum: 500 }
validates :password, length: { minimum: 8 }, if: :password_required?
Numericality validation
validates :age, numericality: {
only_integer: true,
greater_than_or_equal_to: 18,
less_than: 120
}
Custom validation
validate :email_domain_allowed
private
def email_domain_allowed
return if email.blank?
domain = email.split('@').last
unless ALLOWED_DOMAINS.include?(domain)
errors.add(:email, "domain #{domain} is not allowed")
endend
def password_required?
new_record? || password.present?
end
end
undefined5. Callbacks
5. 回调函数
ruby
undefinedruby
undefinedapp/models/post.rb
app/models/post.rb
class Post < ApplicationRecord
Before callbacks
before_validation :normalize_title
before_save :calculate_reading_time
before_create :generate_slug
After callbacks
after_create :notify_followers
after_update :clear_cache, if: :saved_change_to_body?
after_destroy :cleanup_attachments
Around callbacks
around_save :log_save_time
private
def normalize_title
self.title = title.strip.titleize if title.present?
end
def calculate_reading_time
return unless body_changed?
words = body.split.size
self.reading_time = (words / 200.0).ceil
end
def generate_slug
self.slug = title.parameterize
end
def notify_followers
NotifyFollowersJob.perform_later(self)
end
def clear_cache
Rails.cache.delete("post/#{id}")
end
def cleanup_attachments
attachments.purge_later
end
def log_save_time
start = Time.current
yield
duration = Time.current - start
Rails.logger.info "Post #{id} saved in #{duration}s"
end
end
undefinedclass Post < ApplicationRecord
Before callbacks
before_validation :normalize_title
before_save :calculate_reading_time
before_create :generate_slug
After callbacks
after_create :notify_followers
after_update :clear_cache, if: :saved_change_to_body?
after_destroy :cleanup_attachments
Around callbacks
around_save :log_save_time
private
def normalize_title
self.title = title.strip.titleize if title.present?
end
def calculate_reading_time
return unless body_changed?
words = body.split.size
self.reading_time = (words / 200.0).ceil
end
def generate_slug
self.slug = title.parameterize
end
def notify_followers
NotifyFollowersJob.perform_later(self)
end
def clear_cache
Rails.cache.delete("post/#{id}")
end
def cleanup_attachments
attachments.purge_later
end
def log_save_time
start = Time.current
yield
duration = Time.current - start
Rails.logger.info "Post #{id} saved in #{duration}s"
end
end
undefined6. Enum Patterns
6. 枚举模式
ruby
undefinedruby
undefinedapp/models/post.rb
app/models/post.rb
class Post < ApplicationRecord
Basic enum
enum status: {
draft: 0,
published: 1,
archived: 2
}
Enum with prefix/suffix
enum visibility: {
public: 0,
private: 1,
unlisted: 2
}, _prefix: :visibility
Multiple enums
enum content_type: {
article: 0,
video: 1,
podcast: 2
}, _suffix: :content
Scopes automatically created
Post.draft, Post.published, Post.archived
Post.visibility_public, Post.visibility_private
Post.article_content, Post.video_content
Query methods
post.draft?, post.published?, post.archived?
post.visibility_public?, post.visibility_private?
State transitions
def publish!
published! if draft?
end
end
undefinedclass Post < ApplicationRecord
Basic enum
enum status: {
draft: 0,
published: 1,
archived: 2
}
Enum with prefix/suffix
enum visibility: {
public: 0,
private: 1,
unlisted: 2
}, _prefix: :visibility
Multiple enums
enum content_type: {
article: 0,
video: 1,
podcast: 2
}, _suffix: :content
Scopes automatically created
Post.draft, Post.published, Post.archived
Post.visibility_public, Post.visibility_private
Post.article_content, Post.video_content
Query methods
post.draft?, post.published?, post.archived?
post.visibility_public?, post.visibility_private?
State transitions
def publish!
published! if draft?
end
end
undefined7. Query Optimization
7. 查询优化
ruby
undefinedruby
undefinedapp/models/post.rb
app/models/post.rb
class Post < ApplicationRecord
Eager loading to avoid N+1
scope :with_associations, -> {
includes(:user, :tags, comments: :user)
}
Select specific columns
scope :title_and_author, -> {
select('posts.id, posts.title, users.name as author_name')
.joins(:user)
}
Batch processing
def self.process_in_batches
find_each(batch_size: 1000) do |post|
post.process
end
end
Pluck for arrays
def self.recent_titles
order(created_at: :desc)
.limit(10)
.pluck(:title)
end
Exists check (efficient)
def self.has_recent_posts?(user_id)
where(user_id: user_id)
.where('created_at > ?', 1.day.ago)
.exists?
end
Count with joins
def self.popular_authors
joins(:user)
.group('users.id', 'users.name')
.select('users.id, users.name, COUNT(posts.id) as posts_count')
.having('COUNT(posts.id) >= ?', 10)
.order('posts_count DESC')
end
end
undefinedclass Post < ApplicationRecord
Eager loading to avoid N+1
scope :with_associations, -> {
includes(:user, :tags, comments: :user)
}
Select specific columns
scope :title_and_author, -> {
select('posts.id, posts.title, users.name as author_name')
.joins(:user)
}
Batch processing
def self.process_in_batches
find_each(batch_size: 1000) do |post|
post.process
end
end
Pluck for arrays
def self.recent_titles
order(created_at: :desc)
.limit(10)
.pluck(:title)
end
Exists check (efficient)
def self.has_recent_posts?(user_id)
where(user_id: user_id)
.where('created_at > ?', 1.day.ago)
.exists?
end
Count with joins
def self.popular_authors
joins(:user)
.group('users.id', 'users.name')
.select('users.id, users.name, COUNT(posts.id) as posts_count')
.having('COUNT(posts.id) >= ?', 10)
.order('posts_count DESC')
end
end
undefined8. Transactions
8. 事务处理
ruby
undefinedruby
undefinedapp/services/post_publisher.rb
app/services/post_publisher.rb
class PostPublisher
def self.publish(post, user)
ActiveRecord::Base.transaction do
post.update!(status: :published, published_at: Time.current)
user.increment!(:posts_count)
NotificationService.notify_followers(post)
# If any operation fails, entire transaction is rolled back
endrescue ActiveRecord::RecordInvalid => e
Rails.logger.error "Failed to publish post: #{e.message}"
false
end
Nested transactions with savepoints
def self.complex_operation(post)
ActiveRecord::Base.transaction do
post.update!(featured: true)
ActiveRecord::Base.transaction(requires_new: true) do
# This creates a savepoint
post.tags.create!(name: 'featured')
end
endend
end
undefinedclass PostPublisher
def self.publish(post, user)
ActiveRecord::Base.transaction do
post.update!(status: :published, published_at: Time.current)
user.increment!(:posts_count)
NotificationService.notify_followers(post)
# If any operation fails, entire transaction is rolled back
endrescue ActiveRecord::RecordInvalid => e
Rails.logger.error "Failed to publish post: #{e.message}"
false
end
Nested transactions with savepoints
def self.complex_operation(post)
ActiveRecord::Base.transaction do
post.update!(featured: true)
ActiveRecord::Base.transaction(requires_new: true) do
# This creates a savepoint
post.tags.create!(name: 'featured')
end
endend
end
undefined9. STI (Single Table Inheritance)
9. 单表继承(STI)
ruby
undefinedruby
undefinedapp/models/vehicle.rb
app/models/vehicle.rb
class Vehicle < ApplicationRecord
validates :make, :model, presence: true
def max_speed
raise NotImplementedError
end
end
class Vehicle < ApplicationRecord
validates :make, :model, presence: true
def max_speed
raise NotImplementedError
end
end
app/models/car.rb
app/models/car.rb
class Car < Vehicle
validates :doors, presence: true
def max_speed
120
end
end
class Car < Vehicle
validates :doors, presence: true
def max_speed
120
end
end
app/models/motorcycle.rb
app/models/motorcycle.rb
class Motorcycle < Vehicle
validates :engine_size, presence: true
def max_speed
180
end
end
class Motorcycle < Vehicle
validates :engine_size, presence: true
def max_speed
180
end
end
Usage
Usage
car = Car.create(make: 'Toyota', model: 'Camry', doors: 4)
car.type # => "Car"
Vehicle.all # Returns both cars and motorcycles
Car.all # Returns only cars
undefinedcar = Car.create(make: 'Toyota', model: 'Camry', doors: 4)
car.type # => "Car"
Vehicle.all # Returns both cars and motorcycles
Car.all # Returns only cars
undefined10. Concerns
10. 关注点(Concerns)
ruby
undefinedruby
undefinedapp/models/concerns/sluggable.rb
app/models/concerns/sluggable.rb
module Sluggable
extend ActiveSupport::Concern
included do
before_validation :generate_slug
validates :slug, presence: true, uniqueness: true
end
class_methods do
def find_by_slug(slug)
find_by(slug: slug)
end
end
private
def generate_slug
return if slug.present?
base_slug = title.parameterize
self.slug = unique_slug(base_slug)
end
def unique_slug(base_slug)
slug_candidate = base_slug
counter = 1
while self.class.exists?(slug: slug_candidate)
slug_candidate = "#{base_slug}-#{counter}"
counter += 1
end
slug_candidateend
end
module Sluggable
extend ActiveSupport::Concern
included do
before_validation :generate_slug
validates :slug, presence: true, uniqueness: true
end
class_methods do
def find_by_slug(slug)
find_by(slug: slug)
end
end
private
def generate_slug
return if slug.present?
base_slug = title.parameterize
self.slug = unique_slug(base_slug)
end
def unique_slug(base_slug)
slug_candidate = base_slug
counter = 1
while self.class.exists?(slug: slug_candidate)
slug_candidate = "#{base_slug}-#{counter}"
counter += 1
end
slug_candidateend
end
app/models/post.rb
app/models/post.rb
class Post < ApplicationRecord
include Sluggable
end
undefinedclass Post < ApplicationRecord
include Sluggable
end
undefinedBest Practices
最佳实践
- Use scopes for reusable queries - Keep query logic in the model
- Eager load associations - Prevent N+1 queries with includes/preload
- Add database indexes - Index foreign keys and frequently queried columns
- Use counter caches - Optimize count queries for associations
- Validate at model level - Ensure data integrity with validations
- Keep callbacks simple - Extract complex logic to service objects
- Use transactions - Ensure data consistency for multi-step operations
- Leverage concerns - Share common behavior across models
- Use enums for state - Type-safe state management with enums
- Write efficient queries - Use select, pluck, and exists appropriately
- 使用作用域实现可复用查询 - 将查询逻辑放在模型中
- 预加载关联关系 - 使用includes/preload避免N+1查询问题
- 添加数据库索引 - 为外键和频繁查询的列添加索引
- 使用计数器缓存 - 优化关联关系的计数查询
- 在模型层进行验证 - 通过验证确保数据完整性
- 简化回调函数 - 将复杂逻辑提取到服务对象中
- 使用事务处理 - 确保多步骤操作的数据一致性
- 利用关注点 - 在多个模型间共享通用行为
- 使用枚举管理状态 - 实现类型安全的状态管理
- 编写高效查询 - 合理使用select、pluck和exists
Common Pitfalls
常见陷阱
- N+1 queries - Forgetting to eager load associations
- Callback hell - Too many callbacks making flow hard to follow
- Fat models - Putting too much business logic in models
- Missing indexes - Slow queries due to unindexed columns
- Unsafe updates - Not using transactions for related operations
- Validation bypass - Using update_attribute or save(validate: false)
- Memory bloat - Loading all records instead of batching
- SQL injection - Using string interpolation in where clauses
- Counter cache mismatches - Manual updates breaking counter caches
- Ignoring database constraints - Not adding DB-level validations
- N+1查询问题 - 忘记预加载关联关系
- 回调地狱 - 过多回调导致流程难以追踪
- 胖模型 - 在模型中放入过多业务逻辑
- 缺失索引 - 未索引的列导致查询缓慢
- 不安全的更新 - 未使用事务处理相关操作
- 绕过验证 - 使用update_attribute或save(validate: false)
- 内存膨胀 - 加载所有记录而非分批处理
- SQL注入 - 在where子句中使用字符串插值
- 计数器缓存不匹配 - 手动更新破坏计数器缓存
- 忽略数据库约束 - 未添加数据库级别的验证
When to Use
适用场景
- Building data-backed Rails applications
- Implementing business logic tied to database models
- Creating REST APIs with Rails
- Developing CRUD interfaces
- Managing complex data relationships
- Building multi-tenant applications
- Creating admin interfaces with Active Admin
- Implementing soft deletes and audit trails
- Building reporting and analytics features
- Creating content management systems
- 构建基于数据的Rails应用
- 实现与数据库模型绑定的业务逻辑
- 使用Rails创建REST API
- 开发CRUD界面
- 管理复杂的数据关系
- 构建多租户应用
- 使用Active Admin创建管理界面
- 实现软删除和审计追踪
- 构建报表与分析功能
- 创建内容管理系统