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
不支持嵌套数据,只支持顶层的键值对。prefix
和suffix
选项可以是布尔值、字符串或符号。如果传入一个布尔值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中存储一个用于从表中查找记录的值