名称: rails-active-record-patterns 用户可调用: false 描述: 当使用Active Record模式时,包括模型、关联、查询、验证和回调。 允许的工具:
- 读取
- 写入
- 编辑
- Grep
- Glob
- Bash
Rails Active Record 模式
掌握 Active Record 模式,用于构建健壮的 Rails 模型,包括正确的关联、验证、作用域和查询优化。
概述
Active Record 是 Rails 的对象关系映射(ORM)层,它将模型类连接到数据库表。它实现了 Active Record 模式,其中每个对象实例代表数据库中的一行,并包含数据和行为。
安装和设置
创建模型
# 生成带有迁移的模型
rails generate model User name:string email:string:uniq
# 生成带有关联的模型
rails generate model Post title:string body:text user:references
# 运行迁移
rails db:migrate
数据库配置
# 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'] %>
核心模式
1. 基本模型定义
# app/models/user.rb
class User < ApplicationRecord
# 验证
validates :email, presence: true, uniqueness: true,
format: { with: URI::MailTo::EMAIL_REGEXP }
validates :name, presence: true, length: { minimum: 2, maximum: 50 }
# 回调
before_save :normalize_email
after_create :send_welcome_email
# 作用域
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
2. 关联
# app/models/user.rb
class User < ApplicationRecord
# 一对多
has_many :posts, dependent: :destroy
has_many :comments, dependent: :destroy
# 多对多通过连接表
has_many :memberships, dependent: :destroy
has_many :organizations, through: :memberships
# 一对一
has_one :profile, dependent: :destroy
# 多态关联
has_many :images, as: :imageable, dependent: :destroy
end
# app/models/post.rb
class Post < ApplicationRecord
belongs_to :user
has_many :comments, dependent: :destroy
has_many :commenters, through: :comments, source: :user
# 计数器缓存
belongs_to :user, counter_cache: true
end
# app/models/organization.rb
class Organization < ApplicationRecord
has_many :memberships, dependent: :destroy
has_many :users, through: :memberships
end
# app/models/membership.rb
class Membership < ApplicationRecord
belongs_to :user
belongs_to :organization
enum role: { member: 0, admin: 1, owner: 2 }
end
3. 高级查询
# app/models/post.rb
class Post < ApplicationRecord
# 带有参数的作用域
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 }) }
# 类方法用于复杂查询
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
# 查询与连接和包含
def self.with_user_and_comments
includes(:user, comments: :user)
.order(created_at: :desc)
end
end
# 用法
Post.published_after(1.week.ago)
.by_author(current_user.id)
.with_tag('rails')
.popular(50)
4. 验证
# app/models/user.rb
class User < ApplicationRecord
# 存在性验证
validates :email, :name, presence: true
# 唯一性验证
validates :email, uniqueness: { case_sensitive: false }
# 格式验证
validates :username, format: {
with: /\A[a-z0-9_]+\z/,
message: "只允许小写字母、数字和下划线"
}
# 长度验证
validates :bio, length: { maximum: 500 }
validates :password, length: { minimum: 8 }, if: :password_required?
# 数值性验证
validates :age, numericality: {
only_integer: true,
greater_than_or_equal_to: 18,
less_than: 120
}
# 自定义验证
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} 不被允许")
end
end
def password_required?
new_record? || password.present?
end
end
5. 回调
# app/models/post.rb
class Post < ApplicationRecord
# 前回调
before_validation :normalize_title
before_save :calculate_reading_time
before_create :generate_slug
# 后回调
after_create :notify_followers
after_update :clear_cache, if: :saved_change_to_body?
after_destroy :cleanup_attachments
# 环绕回调
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
6. 枚举模式
# app/models/post.rb
class Post < ApplicationRecord
# 基本枚举
enum status: {
draft: 0,
published: 1,
archived: 2
}
# 枚举带有前缀/后缀
enum visibility: {
public: 0,
private: 1,
unlisted: 2
}, _prefix: :visibility
# 多个枚举
enum content_type: {
article: 0,
video: 1,
podcast: 2
}, _suffix: :content
# 自动创建的作用域
# Post.draft, Post.published, Post.archived
# Post.visibility_public, Post.visibility_private
# Post.article_content, Post.video_content
# 查询方法
# post.draft?, post.published?, post.archived?
# post.visibility_public?, post.visibility_private?
# 状态转换
def publish!
published! if draft?
end
end
7. 查询优化
# app/models/post.rb
class Post < ApplicationRecord
# 预加载以避免 N+1
scope :with_associations, -> {
includes(:user, :tags, comments: :user)
}
# 选择特定列
scope :title_and_author, -> {
select('posts.id, posts.title, users.name as author_name')
.joins(:user)
}
# 批量处理
def self.process_in_batches
find_each(batch_size: 1000) do |post|
post.process
end
end
# 使用 pluck 获取数组
def self.recent_titles
order(created_at: :desc)
.limit(10)
.pluck(:title)
end
# 存在检查(高效)
def self.has_recent_posts?(user_id)
where(user_id: user_id)
.where('created_at > ?', 1.day.ago)
.exists?
end
# 带有连接的计数
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
8. 事务
# 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)
# 如果任何操作失败,整个事务将回滚
end
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error "Failed to publish post: #{e.message}"
false
end
# 嵌套事务带有保存点
def self.complex_operation(post)
ActiveRecord::Base.transaction do
post.update!(featured: true)
ActiveRecord::Base.transaction(requires_new: true) do
# 这将创建一个保存点
post.tags.create!(name: 'featured')
end
end
end
end
9. STI(单表继承)
# app/models/vehicle.rb
class Vehicle < ApplicationRecord
validates :make, :model, presence: true
def max_speed
raise NotImplementedError
end
end
# app/models/car.rb
class Car < Vehicle
validates :doors, presence: true
def max_speed
120
end
end
# app/models/motorcycle.rb
class Motorcycle < Vehicle
validates :engine_size, presence: true
def max_speed
180
end
end
# 用法
car = Car.create(make: 'Toyota', model: 'Camry', doors: 4)
car.type # => "Car"
Vehicle.all # 返回汽车和摩托车
Car.all # 只返回汽车
10. 关注点
# 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
# app/models/post.rb
class Post < ApplicationRecord
include Sluggable
end
最佳实践
- 使用作用域进行可重用查询 - 将查询逻辑保留在模型中
- 预加载关联 - 使用 includes/preload 避免 N+1 查询
- 添加数据库索引 - 索引外键和频繁查询的列
- 使用计数器缓存 - 优化关联的计数查询
- 在模型级别进行验证 - 使用验证确保数据完整性
- 保持回调简单 - 将复杂逻辑提取到服务对象
- 使用事务 - 确保多步骤操作的数据一致性
- 利用关注点 - 在模型之间共享常见行为
- 使用枚举管理状态 - 使用枚举进行类型安全的状态管理
- 编写高效查询 - 适当使用 select、pluck 和 exists
常见陷阱
- N+1 查询 - 忘记预加载关联
- 回调地狱 - 太多回调使流程难以跟踪
- 臃肿模型 - 在模型中放入太多业务逻辑
- 缺少索引 - 由于未索引列导致查询缓慢
- 不安全更新 - 未为相关操作使用事务
- 验证绕过 - 使用 update_attribute 或 save(validate: false)
- 内存膨胀 - 加载所有记录而不是批量处理
- SQL 注入 - 在 where 子句中使用字符串插值
- 计数器缓存不匹配 - 手动更新破坏计数器缓存
- 忽略数据库约束 - 未添加数据库级别的验证
何时使用
- 构建数据支持的 Rails 应用程序
- 实现与数据库模型相关的业务逻辑
- 创建带有 Rails 的 REST API
- 开发 CRUD 接口
- 管理复杂的数据关系
- 构建多租户应用程序
- 使用 Active Admin 创建管理界面
- 实现软删除和审计跟踪
- 构建报告和分析功能
- 创建内容管理系统