Rails:ActiveRecord的`store` 和`store_accessor` 方法介绍

Rails是一个大型框架,有很多方便的内置工具,适用于特定情况。在这个系列中,我们将看看隐藏在Rails庞大代码库中的一些不太知名的工具。

在这篇文章中,我们将重点介绍ActiveRecord的 store store_accessor 方法。这两种方法都是针对在数据库列中存储结构化数据的使用情况,如JSON或YAML。 store_accessor 为我们提供了一个方便的方法来抓取这些数据的值,而不需要用getter方法来堵塞模型, store 则更进一步,将数据透明地序列化/反序列化为我们选择的格式。为了理解这一点的用处,我们还将看看在关系型数据库中存储JSON的选项,以及你可能想要这样做的一些原因。

数据库中的JSON

我应该澄清一下,当我在本文中说到'数据库'时,我指的是关系型数据库,特别是PostgreSQL和MySQL,因为它们是Rails社区中使用最广泛的。

有人可能会问,为什么你要把JSON存储在关系型数据库中。的确,利用关系型数据库的好处的方法是将数据分割开来,这样它们之间的 关系 就可以由数据库来执行(比如外键),而且数据可以被索引以提高查询性能。

关系型数据库模型的一个缺点是,数据结构必须是提前知道的*,而且*表内的每一行都是相同的。如果你的应用程序是围绕不符合这些要求的数据建立的,你可能想研究NoSQL数据库。不过,对于大多数网络应用来说,我们想坚持使用我们所知道的大多数数据的 魔鬼 关系型数据库,只是明智地 "撒 "进这些动态数据结构。在这些情况下,像JSON列这样的东西会有很大的意义。

JSON vs. JSONB

PostgreSQL有两种JSON列: json jsonb 。主要的区别是: jsonb 是在写的时候解析的,这意味着数据是以数据库可以更快地查询的格式存储的。需要注意的是,由于JSON已经被解析,当作为文本输出时,它可能不再与用户输入的内容完全匹配。例如,重复的键可能被删除,或者键的顺序可能与原来的不一致。

PostgreSQL的文档指出,在大多数情况下, jsonb 是你想要的,除非你有特殊的原因。

MySQL的 json 列的行为类似于PostgreSQL中的 jsonb 。为了支持 "只是用户输入的内容 "的输出,你可能必须使用 varchar 列或类似的东西。

JSON与文本

除了允许对数据进行预解析外,使用JSON列而不是在文本字段中存储相同的数据,允许使用数据本身的查询。例如,你可以查询该列中存在特定键值对的所有记录。请注意,Rails本身并不支持许多(如果有的话)针对JSON的查询,因为它们是针对数据库的。因此,如果你想利用这些功能,你就必须使用SQL查询来实现。

Rails中的JSON列

Rails支持在迁移中创建 json (以及 jsonb on PostgreSQL)列。

class CreateItems < ActiveRecord::Migration[7.0]
  def change
    create_table :items do |t|
      t.jsonb :user_attributes

当读取该列时,返回的结果是一个Hash:

> Item.first.user_attributes
  Item Load (0.6ms)  SELECT "items".* FROM "items" ORDER BY "items"."id" ASC LIMIT $1  [["LIMIT", 1]]
=> {"color"=>"text-red-400"}
> Item.first.update!(user_attributes: {color: "text-blue-400"})
> Item.first.user_attributes.dig(:color)
=> "text-blue-400"

现在我们有了一个Hash属性,你可能想在模型中添加一些辅助方法来读/写值。

class Item < ApplicationRecord
  def color=(value)
    self.user_attributes["color"] = value
  def color
    user_attributes.dig("color")

像这样的方法功能非常好,但如果你有大量的JSON键需要处理,就会很快变得不方便;幸运的是,Rails为我们提供了保障。

ActiveRecord的store和store_accessor

在数据库中存储JSON有两个方面:序列化和访问。如果你在数据库中使用json 类型的列,那么你就不需要担心序列化方面的问题。Rails和数据库适配器将为你处理它(你可以直接跳到store_accessor )。如果你在文本列中存储数据,那么ActiveRecord的store 方法就是为你准备的,它可以确保你写到列中的数据被序列化为你选择的格式。

ActiveRecord的存储

ActiveRecord有一个store 方法来自动序列化我们读或写到我们列的数据。

class Item < ApplicationRecord
  store :user_attributes, accessors: [:color], coder: JSON

在这里,:user_attributes 是我们要使用的列,而accessors 是我们要访问的键的列表(在我们这里只是color ),最后,我们指定我们希望数据被编码的方式。我们使用JSON,但你可以在这里使用任何你喜欢的东西,包括像YAML或自定义编码。这个方法只是处理序列化(用你选择的编码),并在后台调用store_accessor

ActiveRecord的store_accessor

我们通过使用store_accessor ,在我们的模型中创建get/set方法:

class Item < ApplicationRecord
  store_accessor :user_attributes, :color
  store_accessor :user_attributes, :name, prefix: true
  store_accessor :user_attributes, :location, prefix: 'primary'

在这里,user_attributes 是我们想要使用的数据库列,其次是我们想要在JSON数据中使用的键,最后,我们可以选择使用前缀(或后缀)。注意,store_accessor 不支持嵌套数据,只支持顶层的键值对。prefixsuffix 选项可以是布尔值、字符串或符号。如果传入一个布尔值true ,那么列的名称就被用作前缀/后缀:

=>item = Item.create!(color: 'red', user_attributes_name: 'Jonathan', primary_location: 'New Zealand')
>#<Item:0x000055d63f4f0360
 id: 4,
 user_attributes: {"color"=>"red", "name"=>"Jonathan", "location"=>"New Zealand"}>
=>item.color
>"red"
=> item.user_attributes_name
>"Jonathan"
=> item.name
>NoMethodError: undefined method `name'...
=> item.primary_location
>"New Zealand"

真实世界的使用

我只是偶尔需要偏离典型的已知时间的关系型数据库模式。在为数不多的几次中,它使数据库的结构比没有这些选项的情况下更干净、更简单。

我遇到的一个例子是支持多个API,用户连接他们自己的账户。当这些API不使用相同的认证方案时,这就变得很棘手。有些可能使用用户名+密码,有些使用API密钥,还有一些有API密钥、秘密和商家ID。一种方法是不断向表中添加列,其中许多列对大多数供应商来说都是null 。然而,使用json ,我们可以只存储特定API需要的值。

我正在进行的一个辅助项目也使用JSON存储,允许用户在项目上设置任意的属性,包括用户定义的属性。考虑到这种数据的流动性和不可预测性,像JSON存储这样的东西(有store_accessor,用于已知的属性)是一种自然的适合。

当数据和数据结构是可改变的或不可知的时候,JSON数据(以及ActiveRecord周围的助手)可以非常有用。当然,这种数据存储和大多数事情一样,是一种权衡。虽然你在特定记录的数据结构中获得了很大的灵活性,但你放弃了一些数据库约束所能提供的数据完整性。你也减少了用典型的ActiveRecord查询、连接等方式查询记录的能力。

这里有一些经验法则,如果你:

  • 知道所有行的JSON键都是一样的,或
  • 正在存储任何其他数据库表的ID(主键),或
  • 在JSON中存储一个用于从表中查找记录的值
  •