手动初始化带自定义查询值的ActiveRecord对象 无需声明额外模型属性
ActiveRecord 自定义属性实例化的底层逻辑与手动实现
核心底层原理
拆解Foo.select('1 AS bar').first能正常工作的真实流程,完全没有黑魔法:
- AR的属性存储核心是实例内部的
@attributes属性集合(类型为ActiveModel::AttributeSet),所有属性的读写代理逻辑,都是基于“@attributes集合中存在某个key”自动生效的,只要key在集合里,就能自动生成对应的读、写方法,不需要提前在类上声明。 - 常规调用
Foo.new(attrs)抛出UnknownAttributeError,本质是公开的批量赋值接口(assign_attributes)做了前置安全校验:只会接受类已注册的属性(表列、attribute声明、attr_accessor定义的方法),否则直接拦截报错。这个校验是公开接口的防护机制,和底层属性存储的能力无关。 - AR在处理数据库查询结果、构建实例时,完全绕过了这个公开校验流程:查询出的每一行结果,不管是表原有列还是自定义SQL生成的别名,都会直接写入实例内部的
@attributes集合,根本不会触发前置校验。
手动实现带自定义属性的AR实例
按照和AR原生构建完全一致的逻辑,不需要在Foo类上加任何额外声明,就能生成携带自定义属性的实例,两种常用实现方式:
方式1:贴合AR原生构建流程的实现(最接近select查询的底层路径)
这种方式和AR从数据库加载记录的初始化步骤1:1对齐:
custom_attrs = { bar: 1, id: 1, name: "ABC" } # 1. 仅分配内存,不调用initialize方法,完全跳过公开的参数校验 foo = Foo.allocate # 2. 调用AR内部初始化方法,加载基础内部状态(属性集、新记录标记、回调缓存等) foo.send :init_internals # 3. 逐值写入内部属性集合,无前置校验 attr_set = foo.instance_variable_get(:@attributes) custom_attrs.each do |key, value| attr_set.write(key.to_s, value) end # 4. 标记实例为已从数据库加载的非新记录,和Foo.first返回的实例状态完全一致 foo.instance_variable_set(:@new_record, false) foo.instance_variable_set(:@previously_new_record, false)
执行后可以直接验证:
foo.bar返回自定义值1foo.id返回1,foo.name返回"ABC"- 实例上没有任何动态添加的类方法,和原生查询的行为完全一致
方式2:更简洁的写入方式
如果已经有正常初始化的合法AR实例(比如已经传入了id、name这类表定义的属性),不需要重复走allocate流程,直接调用AR的内部属性写入接口即可:
# 先正常初始化带合法属性的实例 foo = Foo.new(id: 1, name: "ABC") # 直接通过内部写入接口写入自定义属性,无公开层校验 foo.write_attribute(:bar, 1) foo.bar # => 1,可正常读取
行为一致性说明
这种方式生成的实例,和select查询生成的带自定义列的实例行为完全一致:
- 自定义属性的值只会存在于当前实例的属性集合中,不会影响同类型的其他实例,也不会在类层面留下任何永久修改
- 调用
attributes方法时,自定义属性会和表列属性一起出现在返回的哈希中 - 调用
save/update等持久化方法时,AR生成SQL语句只会提取表定义对应列的属性值,自定义属性不会被写入数据库,完全避免表结构不匹配的报错 - 不需要在类上添加
attribute声明或者attr_accessor,完全满足限制条件
内容的提问来源于stack exchange,提问作者hrdwdmrbl
相关产品推荐
相关产品推荐

