rails-active-record-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Rails 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
undefined
bash
undefined

Generate 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
undefined
rails db:migrate
undefined

Database Configuration

数据库配置

yaml
undefined
yaml
undefined

config/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'] %>
undefined
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'] %>
undefined

Core Patterns

核心模式

1. Basic Model Definition

1. 基础模型定义

ruby
undefined
ruby
undefined

app/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
undefined
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
undefined

2. Associations

2. 关联关系

ruby
undefined
ruby
undefined

app/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
undefined
class Membership < ApplicationRecord belongs_to :user belongs_to :organization
enum role: { member: 0, admin: 1, owner: 2 } end
undefined

3. Advanced Queries

3. 高级查询

ruby
undefined
ruby
undefined

app/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)
undefined
Post.published_after(1.week.ago) .by_author(current_user.id) .with_tag('rails') .popular(50)
undefined

4. Validations

4. 验证机制

ruby
undefined
ruby
undefined

app/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")
end
end
def password_required? new_record? || password.present? end end
undefined
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")
end
end
def password_required? new_record? || password.present? end end
undefined

5. Callbacks

5. 回调函数

ruby
undefined
ruby
undefined

app/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
undefined
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
undefined

6. Enum Patterns

6. 枚举模式

ruby
undefined
ruby
undefined

app/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
undefined
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
undefined

7. Query Optimization

7. 查询优化

ruby
undefined
ruby
undefined

app/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
undefined
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
undefined

8. Transactions

8. 事务处理

ruby
undefined
ruby
undefined

app/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
end
rescue 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
end
end end
undefined
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
end
rescue 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
end
end end
undefined

9. STI (Single Table Inheritance)

9. 单表继承(STI)

ruby
undefined
ruby
undefined

app/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
undefined
car = Car.create(make: 'Toyota', model: 'Camry', doors: 4) car.type # => "Car" Vehicle.all # Returns both cars and motorcycles Car.all # Returns only cars
undefined

10. Concerns

10. 关注点(Concerns)

ruby
undefined
ruby
undefined

app/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_candidate
end 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_candidate
end end

app/models/post.rb

app/models/post.rb

class Post < ApplicationRecord include Sluggable end
undefined
class Post < ApplicationRecord include Sluggable end
undefined

Best Practices

最佳实践

  1. Use scopes for reusable queries - Keep query logic in the model
  2. Eager load associations - Prevent N+1 queries with includes/preload
  3. Add database indexes - Index foreign keys and frequently queried columns
  4. Use counter caches - Optimize count queries for associations
  5. Validate at model level - Ensure data integrity with validations
  6. Keep callbacks simple - Extract complex logic to service objects
  7. Use transactions - Ensure data consistency for multi-step operations
  8. Leverage concerns - Share common behavior across models
  9. Use enums for state - Type-safe state management with enums
  10. Write efficient queries - Use select, pluck, and exists appropriately
  1. 使用作用域实现可复用查询 - 将查询逻辑放在模型中
  2. 预加载关联关系 - 使用includes/preload避免N+1查询问题
  3. 添加数据库索引 - 为外键和频繁查询的列添加索引
  4. 使用计数器缓存 - 优化关联关系的计数查询
  5. 在模型层进行验证 - 通过验证确保数据完整性
  6. 简化回调函数 - 将复杂逻辑提取到服务对象中
  7. 使用事务处理 - 确保多步骤操作的数据一致性
  8. 利用关注点 - 在多个模型间共享通用行为
  9. 使用枚举管理状态 - 实现类型安全的状态管理
  10. 编写高效查询 - 合理使用select、pluck和exists

Common Pitfalls

常见陷阱

  1. N+1 queries - Forgetting to eager load associations
  2. Callback hell - Too many callbacks making flow hard to follow
  3. Fat models - Putting too much business logic in models
  4. Missing indexes - Slow queries due to unindexed columns
  5. Unsafe updates - Not using transactions for related operations
  6. Validation bypass - Using update_attribute or save(validate: false)
  7. Memory bloat - Loading all records instead of batching
  8. SQL injection - Using string interpolation in where clauses
  9. Counter cache mismatches - Manual updates breaking counter caches
  10. Ignoring database constraints - Not adding DB-level validations
  1. N+1查询问题 - 忘记预加载关联关系
  2. 回调地狱 - 过多回调导致流程难以追踪
  3. 胖模型 - 在模型中放入过多业务逻辑
  4. 缺失索引 - 未索引的列导致查询缓慢
  5. 不安全的更新 - 未使用事务处理相关操作
  6. 绕过验证 - 使用update_attribute或save(validate: false)
  7. 内存膨胀 - 加载所有记录而非分批处理
  8. SQL注入 - 在where子句中使用字符串插值
  9. 计数器缓存不匹配 - 手动更新破坏计数器缓存
  10. 忽略数据库约束 - 未添加数据库级别的验证

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创建管理界面
  • 实现软删除和审计追踪
  • 构建报表与分析功能
  • 创建内容管理系统

Resources

参考资源