Rails ActiveRecord Store 使用介绍

helapu · 2017年03月07日 · 最后由 jasl 回复于 2017年03月10日 · 6470 次阅读
本帖已被管理员设置为精华贴

ActiveRecord Store

http://api.rubyonrails.org/classes/ActiveRecord/Store.html

阅读 http://api.rubyonrails.org 相关的笔记

使用 Model 里面的一个字段作为一个序列化的封装,用来存储一个 key/value

文档里面提到,对应的存储字段的类型最好是 text,以便确保有足够的存储空间

Make sure that you declare the database column used for the serialized store as a text, so there's plenty of room.

假设 Model 里面有一个字段 body

class CreatePosts < ActiveRecord::Migration[5.0]
  def change
    create_table :posts do |t|
      t.string :title
      t.text :body  # 作为store序列化的字段
      t.boolean :published
      t.integer :status

      t.timestamps
    end
  end
end

接着设置对应的序列化属性

class Post < ApplicationRecord

  # enum status: [ :active, :archived ] # 这里使用数组 与之对应的数字从0依次增加
  enum status: { active: 10, archived: 20 } # 明确指定对应的数字

  store :body, accessors: [ :color, :homepage, :email ], coder: JSON # 序列化属性

end

这样设置后,在 body 这一个字段上就可以存储多个 key/value 了

irb(main):001:0> p = Post.create
   (0.1ms)  begin transaction
  SQL (1.2ms)  INSERT INTO "posts" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", 2017-02-16 07:32:44 UTC], ["updated_at", 2017-02-16 07:32:44 UTC]]
   (1.9ms)  commit transaction
=> #<Post id: 4, title: nil, body: {}, published: nil, status: nil, created_at: "2017-02-16 07:32:44", updated_at: "2017-02-16 07:32:44">
irb(main):002:0> p.body
=> {}
irb(main):003:0> p.body.class
=> ActiveSupport::HashWithIndifferentAccess
irb(main):004:0> p.body[:color] = "red"
=> "red"
irb(main):005:0> p.body[:email] = "[email protected]"
=> "[email protected]"
irb(main):006:0> p.color
=> "red"
irb(main):007:0> p.email
=> "[email protected]"
irb(main):008:0> p.body[:no_set] = "这个属性没有在model声明"
=> "这个属性没有在model声明"
irb(main):009:0> p.body[:no_set]
=> "这个属性没有在model声明"
irb(main):010:0> p.no_set #这个会报错
huacnlee 将本帖设为了精华贴。 03月07日 17:22

曾经的 serialize

pathbox 回复

是 rails4 才修改的吧?历史版本的不是很清楚

helapu 回复

serialize 现在也有,其实你看看 store 的源码就知道怎么回事了

def store(store_attribute, options = {})
  serialize store_attribute, IndifferentCoder.new(options[:coder])
  store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors
end

简单场景下的 serialize 的封装

这个和 Postresql 里面的 JSON 类型很类似吧?

chenzeyu 回复

postgre 原生的数据类型支持索引,支持数据库级别的操作,能用原生就用原生的 active record 的方案是在数据库不支持的前提下没办法的方案。

jasl 回复

看来复杂的场景 还是需要使用 serialize 的

dy1901 回复

store 和利用 hstore 之类的数据库特性不冲突的,序列化和反序列化是由 coder 完成,可以自己定制

jasl 回复

是不冲突,效率低功能少呀

dy1901 回复

效率可能低,但是功能非常强大,serialize 只是一种标准实践,封装了形如

class Role < ApplicationRecord
  def permissions
    @_permissions ||= Permissions.new read_attribute(:permissions)
  end
end

这种将某字段作为虚拟模型的模式,那能做到多强大就看需要和想象力了

Rails 只是为一些简单场景(比如数组、哈希)做了开箱即用的实现,Rails 推崇 PG,还有 PG 支持 Array 这些数据类型比 Rails 提供这方面支持可晚了一些,这里有点历史原因在的

在 serialize Hash 有个小坑。普通的 hash 和 ActionController::Parameters 或 Hashie::Mash 等类型都能存。这就会导致了,如果代码中原本使用 symbol 方式取值正确的逻辑,当该字段值用 hash 类型,并且 hash 对应的 key 用字符串形式存了,原本原本使用 symbol 方式取值正确的逻辑取值就变为空了。所以,推荐当取值的时候都用字符串形式。第二个是,如果数据库存了 ActionController::Parameters 类型的值,在 rails 5 中取这个值的时候就报错,,直接导致用这个 model 的时候就报错。因为 ActionController::Parameters 在 rails 5 不再继承于 Hash,而进行 serialize hash 的时候就不行了。在 rails 4 中把这个数据用普通 hash 更改一下,在 rails 5 项目中就恢复正常

jasl 回复

这个功能只要 orm 层可以反射为 hash 就可以实现了,使用 hstore 或者 serialize 在这个场景并没有啥区别,但是我的意思是 hstore 的好处是,它是 pg 原生支持的类型,可以做

SELECT
 sum (attrs -> 'like_count') As total_likes
FROM
 posts;
GROUP BY xxx

这种级别的操作,所以如果数据库层面原生支持 json,最好直接用原生类型,而不是 serialize。

dy1901 回复

这个不冲突的呀,serialize 只决定把某个字段映射成什么类,最后持久化成什么样是 Coder 来控制的,存成 hstore 没有任何问题

况且,serialize 可以把字段映射成一个具体的类,可以有效的封装状态行为验证等等

想它的价值得把存储数据和应用数据分成两件事来看待

jasl 回复

哦,这个用法没试过,回头试试。我想当然以为 serialize 要求数据库类型必须为 string 了

不过搜了下 5.0 的 doc,貌似不推荐这种情况用 serialize 了

http://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html

If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, then specify the name of that attribute using this method and it will be handled automatically. The serialization is done through YAML. If class_name is specified, the serialized object must be of that class on assignment and retrieval. Otherwise SerializationTypeMismatch will be raised.

Empty objects as {}, in the case of Hash, or [], in the case of Array, will always be persisted as null.

Keep in mind that database adapters handle certain serialization tasks for you. For instance: json and jsonb types in PostgreSQL will be converted between JSON object/array syntax and Ruby Hash or Array objects transparently. There is no need to use serialize in this case.

For more complex cases, such as conversion to or from your application domain objects, consider using the ActiveRecord::Attributes API.

dy1901 回复

默认是 YAML,你发的文档有提的。

他不是不推荐,这里确实有坑,举个例子,你可以持久化成 JSON,但是 JSON 和 Ruby 的数据类型没法一一对应,没有考虑到这点(主要是说 Coder 要处理好类型转换,包括字段为 nil 之类种种边界情况),就会出问题,所以在使用的时候要小心。

dy1901 回复

总之是个比较高级的特性了,用好很强大,提另一种用法,来自 Redmine: https://github.com/edavis10/redmine/blob/master/app/models/role.rb#L77

但是需要注意的问题也很多,这里有抽象泄露,得对 ActiveModel 有一些了解才能正确实现,提一种坑

class Role < ApplicationRecord
  def permissions
    @_permissions ||= Permissions.new read_attribute(:permissions)
  end
end

role = Role.new
role.permissions.class #=> Permissions 这是我们期望的
role.permissions = {read: false} # 或者 role.update_attributes permissions: {} 前端表单提交可能会这么写
role.permissions.class #=> Hash 注意!
jasl 回复

👍 ,的确很坑,得定义 permissions= 的行为才行

dy1901 回复

是的,从另一方面来讲,role.permissions = {read: false} 这样做改变了 permissions 的类型是很合乎逻辑的行为,Ruby 鸭子类型嘛!

但是,write_attributes 等行为也会改变 permissions 的数据类型,这就涉及到 ActiveModel 的相关的原理了,他底层是用 public_send "#{attribute}=", val 来实现更新数据的行为,所以这里有抽象泄露。

cqgsm 回复

haha

深恶痛绝 滥用这种设计!!

这东西外加自定义 Coder,用于实现 Event, Notification,在个别场景需要存储奇怪数据的时候很合适。

Notification.create({
  event_type: 'block_topic'
  target_type: 'Topic',
  target_id: @topic.id,
  params: Notification::BlockTopicParam.new(message: 'this reason of this block') 
})

瑕不掩瑜

哇 看了你们的讨论 才感觉 一个知识点 往往只是冰山一角 需要更多的去思考

helapu 回复

继续抛砖引玉哇 😂

需要 登录 后方可回复, 如果你还没有账号请 注册新账号