Rails ActiveRecord Store 使用介绍

helapu · 发布于 2017年3月07日 · 最后由 jasl 回复于 2017年3月10日 · 2248 次阅读
23224
本帖已被设为精华帖!

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] = "hello@126.com"
=> "hello@126.com"
irb(main):006:0> p.color
=> "red"
irb(main):007:0> p.email
=> "hello@126.com"
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 #这个会报错
共收到 24 条回复
De6df3 huacnlee 将本帖设为了精华贴 3月07日 17:22
15420

曾经的serialize

23224
15420pathbox 回复

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

1107
23224helapu 回复

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 的封装

96

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

4938
32chenzeyu 回复

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

23224
1107jasl 回复

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

1107
4938dy1901 回复

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

4938
1107jasl 回复

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

1107
4938dy1901 回复

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

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

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

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

15420

在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 项目中就恢复正常

4938
1107jasl 回复

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

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

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

1107
4938dy1901 回复

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

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

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

4938
1107jasl 回复

哦,这个用法没试过,回头试试。我想当然以为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.

1107
4938dy1901 回复

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

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

1107
4938dy1901 回复

总之是个比较高级的特性了,用好很强大,提另一种用法,来自 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 注意!
4938
1107jasl 回复

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

1107
4938dy1901 回复

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

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

27658
27658cqgsm 回复

haha

273

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

De6df3

这东西外加自定义 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') 
})
19780

瑕不掩瑜

23224

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

1107
23224helapu 回复

继续抛砖引玉哇 😂

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