Camping是一个不到4KB代码的Ruby微型Web框架,虽然体积极小,但它借助ActiveRecord获得了完整的ORM能力。在Camping中定义模型时,我们需要在Camping::Models模块内部编写ActiveRecord的子类,而关联关系的声明方式和Rails几乎一致。不过由于Camping的模块结构与Rails不同,许多从Rails迁移过来的开发者会在命名、自动加载和关联声明上踩坑。本文将系统讲解如何在Camping::Models::Associations的结构下正确使用ActiveRecord的各类关联方法。

Camping中的模型基本结构
在Camping中,所有模型都定义在应用模块的Models子模块中,而不是像Rails那样分散在app/models目录下的独立文件里。Camping采用单文件组织方式,模型类直接嵌套在module YourApp::Models内部。这种结构决定了关联声明必须写在类定义体内,且需要注意类名的解析作用域。
一个典型的模型定义如下:
module Blog::Models
class Post < Base
belongs_to :user
has_many :comments, dependent: :destroy
end
class Comment < Base
belongs_to :post
belongs_to :user
end
class User < Base
has_many :posts
has_many :comments
end
end这里的Base是Camping为我们准备好的基类,通常在模块中定义:class Base < Camping::Models::Base; end。所有模型都继承自它,这样Camping会在应用启动时自动建立数据库连接并执行建表逻辑。声明关联时,belongs_to和has_many后面的符号参数会被ActiveRecord解析为对应的类名,例如belongs_to :user会在Blog::Models的命名空间下查找User类。
需要注意的一点是,Camping不会像Rails那样在启动时自动加载模型文件,因为所有代码都在同一个文件中。如果你在控制台或Rakefile中单独引用模型,必须确保整个Camping应用已经被加载,否则关联声明会因为找不到目标常量而抛出NameError。
三种常用关联类型的声明与使用
ActiveRecord提供了多种关联宏方法,在Camping中都可以直接使用。最常用的三种是一对一、一对多和多对多,下面分别说明它们的声明方式和底层机制。
一对一关联:has_one与belongs_to
一对一关系用一个has_one配一个belongs_to来声明,外键始终放在belongs_to一方对应的表中。例如每个用户拥有一份个人资料:
module Blog::Models
class User < Base
has_one :profile, dependent: :destroy
end
class Profile < Base
belongs_to :user
end
end
# 使用示例
user = User.create(name: "alice")
Profile.create(user_id: user.id, bio: "Ruby 爱好者")
puts user.profile.bio # 输出: Ruby 爱好者belongs_to会在模型上生成一个user方法返回关联对象,同时提供build_user和create_user等便捷方法。has_one则生成profile方法,查询时会加上LIMIT 1。如果profiles表中存在多条user_id相同的记录,has_one只取第一条,这是排查数据问题时常见的疑点。
一对多关联:has_many
has_many是最常用的关联,返回的是关联对象的集合代理对象,支持链式查询。例如:
class Post < Base
has_many :comments, -> { order(created_at: :desc) }, dependent: :delete_all
end
# 集合代理支持链式调用
post.comments.where(active: true).limit(10).each do |c|
puts c.content
end
# 直接创建并关联
comment = post.comments.create(content: "写得好")has_many的第二个参数可以是一个Lambda作用域,用来为所有关联查询附加默认条件,比如排序或过滤软删除数据。dependent选项决定删除主记录时对子记录的处理方式::destroy会逐条实例化并删除(触发回调),:delete_all则直接执行批量DELETE语句(速度快但跳过回调),:nullify只把外键置空。在Camping这种小应用里数据量不大,但如果模型定义了after_destroy回调,就必须用:destroy而不是:delete_all,否则回调不会执行。
多对多关联:has_and_belongs_to_many与has_many through
多对多关系有两种实现方式。传统的habtm只需要一张中间表,不需要为中间表建立模型:
class Article < Base has_and_belongs_to_many :tags end class Tag < Base has_and_belongs_to_many :articles end # 需要一张 articles_tags 表,包含 article_id 和 tag_id 两个整型列,无主键
而has_many :through则要求中间表有对应的模型,功能更强大,可以在关联上附加验证、回调和额外字段:
class Physician < Base has_many :appointments has_many :patients, through: :appointments end class Appointment < Base belongs_to :physician belongs_to :patient end class Patient < Base has_many :appointments has_many :physicians, through: :appointments end
一般建议优先使用has_many :through,因为它更灵活、更容易扩展。habtm适合纯粹的标签类场景,中间表永远不需要额外字段。两种方式在Camping中都可以正常工作,但要注意Camping的建表是靠Rakefile中的迁移任务完成的,中间表也必须在迁移中显式创建,Camping不会自动生成。
Camping与Rails在关联定义上的差异及常见坑
虽然关联声明语法一致,但Camping的运行机制与Rails有几个关键差异,直接影响关联关系的使用体验。首先是表命名约定:Camping默认将类名转换为蛇形复数形式作为表名,但如果你的表已经存在且命名不符,需要显式调用set_table_name(老版本)或self.table_name =来指定。
其次是事务和连接管理。Camping在开发模式下默认使用内存数据库(如SQLite3::Database),每次请求可能重建表结构,关联数据不会跨请求保留。要让数据持久化,需要在应用类上配置数据库路径:
module Blog
set :secret, "请替换为自己的随机字符串"
def Blog.create
Camping::Models::Session.create_schema
# 指定持久化数据库文件
Camping::Models::Base.establish_connection(
adapter: "sqlite3",
database: "blog.db"
)
Blog::Models.create_schema
end
end再一个常见坑是命名空间解析。假设Post模型声明了belongs_to :user,ActiveRecord会在Post所在的模块作用域中查找User常量,也就是Blog::Models::User。如果你在别处定义了同名的顶层User类,可能会引起混淆甚至加载错误。建议在Camping应用中避免定义与应用模型同名的顶层类。
最后是N+1查询问题,这在Camping中同样存在。关联查询默认是懒加载的,遍历集合并访问每条记录的关联对象会产生大量SQL。解决方法和Rails一致,使用includes预加载:
# 差:每篇文章额外执行一次查询
Post.all.each { |p| puts p.comments.count }
# 好:一次性预加载所有评论
Post.includes(:comments).each { |p| puts p.comments.size }注意includes后访问关联要用size而不是count,count会额外发起一次COUNT查询,抵消了预加载的效果。掌握这些细节后,即使在没有Rails全家桶的Camping里,也能构建出查询高效、结构清晰的模型关联体系。对于小型项目、原型验证或学习ActiveRecord本身,Camping加上完整关联支持是一个非常好的组合。
Camping框架ActiveRecord模型关联修改时间:2026-09-09 14:33:07