02 灵活扩展模型非自有属性 —— EAV 存储模型

需求背景

在上一期,我们已经完成了一个简单的查询系统,随着业务发展,用户需要搜索更丰富的维度。例如,用户想根据作者朝代搜索中国古籍,比如《三国演义》、《战国策》这类的古籍。

核心矛盾

【作者朝代】是一个典型的稀疏属性,不是所有图书都有这个属性,【作者朝代】这个属性在图书数据中占比很小

当前需要解决的问题,是如何存储非模型自有属性的稀疏数据。非模型自有属性,意味着我们如果将这个属性加在基础模型(主表)上是显然不合理的。

所以我们需要另起一个扩展表,用于存储非模型自有的扩展属性。

接下来我们解决如何存储稀疏属性的问题,如果多个不同的稀疏属性,直接在扩展表里用多个列来存储,则会导致存储空间上空值太多,降低存储性能。

为了解决这些问题,我们采用 EAV 模型来存储这类稀疏扩展属性。

方案设计

EAV(Entity-Attribute-Value)模型是一种适用于存储稀疏的、动态扩展的非固定属性的存储模型设计。其核心是三元组设计:实体(Entity)、属性(Attribute)、属性值(Value)。在实际设计中,还会存储一部分元数据,比如属性类型枚举,来源,更新创建时间,操作人等等。

create table book_extension_attribute (
  entity_id bigint comment '实体ID',
  attribute_key varchar(255) comment '属性键',
  attribute_value text comment '属性值',
  attribute_type int comment '属性类型枚举code'
)

EAV 模型和 NoSQL 数据库(如 HBase、Cassandra 等)的列存储架构采用按列分布的数据模型很相似。

例如,在 HBase 中,每个列族可动态扩展,不同的行可以拥有完全不同的列,对稀疏数据存储十分友好,每一行可以只存储某些列,其他列为空,极大提升存储空间的利用率。

ES 文档设计

因为 ES 的文档是 JSON 格式存储,支持动态扩展字段,也对稀疏字段的存储比较友好,所以我们在 ES 文档里直接用新的字段映射扩展属性就行。

{
  "mappings":{
    "author_dynasty":{
      "type":"keyword"
    }
  }
}

当然,这也为后面的维护埋了个坑,每次新增一类扩展属性,我们都需要去修改读模型,不过由于 ES 可以动态映射字段,这个成本可以接受。