数据库动态属性存储

数据库动态属性数据存储

需求描述

实体的属性可能会随着业务的发展和变化而动态变化。例如,一个博客平台的文章实体可能最初只包含标题、内容和作者等基本属性,但随着功能的扩展,可能需要添加评论数量、浏览量、点赞数等属性。

亦或者一个产品属性不固定,可能一个产品包含名字,颜色,尺寸属性,其他同类产品增加了防水、抗摔等属性;等等数据结构会随着产品更新而变化,不希望重新改代码和数据结构的时候,怎么动态存储兼容这些变化;

方案一:EAV模型 Entity-Attribute-Value

EAV(Entity-Attribute-Value)模型是一种数据建模方法,用于处理具有动态、可变属性的实体。它通常用于解决以下问题:

  1. 灵活的属性存储: 在传统的关系型数据库模型中,每个实体通常有一个固定的属性集合,这样的设计无法满足实体属性的动态变化需求。EAV 模型允许实体拥有可变数量和类型的属性,使得系统可以灵活地存储和管理不同实体的属性。
  2. 应对不规则数据结构: 某些情况下,数据的结构可能是不规则的或者难以提前确定的。EAV 模型允许在不改变数据模型的情况下,动态地增加、删除或修改实体的属性,从而适应不同类型和形式的数据。
  3. 多态数据类型: EAV 模型可以存储多态数据类型,例如文本、数字、日期等,而不需要事先定义数据类型。这种灵活性使得 EAV 模型适用于处理各种类型的数据。
  4. 支持动态查询: 由于属性存储在单独的表中,并且可以根据需要动态增加或修改,因此 EAV 模型具有很强的灵活性和可扩展性,可以支持动态查询和分析需求。

尽管 EAV 模型具有灵活性和可扩展性等优点,但也存在一些挑战,如查询性能下降、数据完整性约束难以实现等。因此,在使用 EAV 模型时需要谨慎权衡其优缺点,并根据实际情况进行选择和设计。

sql 复制代码
-- 主表,存储所有数据的共同属性
CREATE TABLE main_table (
    id INT PRIMARY KEY AUTO_INCREMENT,
    type VARCHAR(255), 
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 属性表,存储不同类型数据的不同属性
CREATE TABLE attribute_table (
    id INT PRIMARY KEY AUTO_INCREMENT,
    main_id INT, -- 关联主表
    attribute_name VARCHAR(255), -- 属性名称
    attribute_value TEXT, -- 属性值
    FOREIGN KEY (main_id) REFERENCES main_table(id)
);

可以看到主表和属表的关系是 1对多,所以可以实现相同属性放在主表,不同属性放在属表,1对多关系,属表的多条数据,对应多个不同的属性;

sql 复制代码
-- 插入主表数据
INSERT INTO main_table (type) VALUES ('type1');
SET @main_id = LAST_INSERT_ID();

-- 插入属性表数据
INSERT INTO attribute_table (main_id, attribute_name, attribute_value) VALUES
(@main_id, 'name', 'kangqing'),
(@main_id, 'age', '30'),
(@main_id, 'city', 'Tianjin');


-- 插入主表数据
INSERT INTO main_table (type) VALUES ('type2');
SET @main_id = LAST_INSERT_ID();

-- 插入属性表数据
INSERT INTO attribute_table (main_id, attribute_name, attribute_value) VALUES
(@main_id, 'title', 'Product A'),
(@main_id, 'price', '100.00'),
(@main_id, 'quantity', '50');

例如:

主表 和 属表的结构如下

sql 复制代码
id | type
----------------
1         | type1
2         | type2


main_id | attribute_name | attribute_value
--------------------------------------------
1         | name           | kangqing
1         | age            | 30
2         | title          | Product A
2         | price          | 100.00
sql 复制代码
SELECT 
    mt.id,
    mt.type,
    MAX(CASE WHEN at.attribute_name = 'name' THEN at.attribute_value END) AS name,
    MAX(CASE WHEN at.attribute_name = 'age' THEN at.attribute_value END) AS age,
    MAX(CASE WHEN at.attribute_name = 'city' THEN at.attribute_value END) AS city,
    MAX(CASE WHEN at.attribute_name = 'title' THEN at.attribute_value END) AS title
FROM 
    main_table mt
LEFT JOIN 
    attribute_table at ON mt.id = at.main_id
GROUP BY 
    mt.id, mt.type;
sql 复制代码
-- 联表查询之后的结果
main_id | type  | name | age | city  | title
---------------------------------------------
1         | type1 | John | 30  | NULL  | NULL
2         | type2 | NULL | NULL| NULL  | Product A
sql 复制代码
-- 改进一下,参数for循环分配
<select id="searchMainTable" resultType="java.util.Map">
    SELECT 
        mt.id,
        mt.type,
        <foreach collection="attributeNames" item="attributeName" separator=",">
            MAX(CASE WHEN at.attribute_name = #{attributeName} THEN at.attribute_value END) AS ${attributeName}
        </foreach>
    FROM 
        main_table mt
    LEFT JOIN 
        attribute_table at ON mt.id = at.main_id
    GROUP BY 
        mt.id, mt.type;
</select>
java 复制代码
// 数据库,或者配置文件获取
List<String> attributeNames = Arrays.asList("name", "age", "city", "title");
params.put("attributeNames", attributeNames);
List<Map<String, Object>> resultList = mainMapper.searchMainTable(params);

示例:

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;

@RestController
public class EntityController {

    @Autowired
    private EntityMapper entityMapper;

    @GetMapping("/entities")
    public List<Map<String, Object>> getEntitiesByAttribute(
            @RequestParam("attribute") String attribute,
            @RequestParam("value") String value
    ) {
        // 执行查询操作
        List<Map<String, Object>> entities = entityMapper.findByAttribute(attribute, value);

        // 对查询结果进行处理,合并相同实体 ID 的属性
        Map<Integer, Map<String, String>> mergedEntities = new HashMap<>();
        for (Map<String, Object> entity : entities) {
            int entityId = (int) entity.get("entity_id");
            String attr = (String) entity.get("attribute");
            String val = (String) entity.get("value");
            if (!mergedEntities.containsKey(entityId)) {
                mergedEntities.put(entityId, new HashMap<>());
            }
            mergedEntities.get(entityId).put(attr, val);
        }

        // 将处理后的结果转换为列表返回给前端
        List<Map<String, Object>> result = new ArrayList<>();
        for (Map.Entry<Integer, Map<String, String>> entry : mergedEntities.entrySet()) {
            Map<String, Object> entityMap = new HashMap<>();
            entityMap.put("entity_id", entry.getKey());
            entityMap.putAll(entry.getValue());
            result.add(entityMap);
        }

        return result;
    }
}
json 复制代码
[
  {
    "entity_id": 1,
    "name": "John",
    "age": "30"
  },
  {
    "entity_id": 2,
    "name": "Jane",
    "age": "25",
    "city": "New York"
  }
]

方案二: JSON结构存储动态属性

MySQL 中的 JSON 数据类型允许存储和操作 JSON 文档。通过 JSON 数据类型,可以存储动态变化的属性,以适应实体属性的动态变化需求。以下是 MySQL 中 JSON 数据类型的介绍:

  1. JSON 数据类型: MySQL 5.7 版本及以上支持 JSON 数据类型。JSON 数据类型可以存储 JSON 文档,包括对象、数组、字符串、数字、布尔值和 null 值等。JSON 文档以原生的 JSON 格式存储,不需要转换成字符串形式。
  2. 存储格式: JSON 数据类型存储在数据库中的格式与原生的 JSON 格式相同,不需要额外的解析或转换。可以直接将 JSON 文档插入到 JSON 数据类型的列中,也可以通过 JSON 函数操作 JSON 数据。
  3. 动态属性: JSON 数据类型允许存储动态变化的属性。在 JSON 文档中,可以动态地增加、删除或修改属性,以适应实体属性的动态变化需求。这使得 MySQL 中的 JSON 数据类型成为一种灵活的解决方案,适用于存储实体属性的动态变化情况。
  4. 查询和操作: MySQL 提供了一系列的 JSON 函数,用于操作和查询 JSON 数据。这些函数包括 JSON_EXTRACT、JSON_CONTAINS、JSON_ARRAY、JSON_OBJECT 等,可以用于从 JSON 文档中提取数据、判断是否包含某个值、构建 JSON 数组和对象等操作。
  5. 索引支持: MySQL 5.7 版本及以上支持对 JSON 数据类型的索引。可以通过创建虚拟列和使用函数索引来对 JSON 列中的数据进行索引,从而提高查询性能。

通过 MySQL 中的 JSON 数据类型,可以方便地存储和操作动态变化的属性,适应实体属性的动态变化需求。这使得 MySQL 成为一种强大的解决方案,适用于存储各种类型和形式的数据。

sql 复制代码
-- 数据库结构,不同的属性结构以JSON格式存在 attributes 中
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    attributes JSON
);

-- 测试数据
INSERT INTO products (attributes)
VALUES
    ('{"name": "MacBook", "color": "silver", "price": 12345, "storage": "512GB", "screen_size": "13 inches"}'),
    ('{"name": "Dell", "color": "black", "price": 1699.99, "storage": "1TB", "graphics_card": "NVIDIA"}'),
    ('{"name": "iPhone", "color": "green", "price": 699.99, "storage": "128GB", "water_resistant": true}');

-- 条件查询
SELECT *
FROM products
WHERE JSON_EXTRACT(attributes, '$.color') = 'silver'
AND JSON_EXTRACT(attributes, '$.price') < 18000;

-- 更新
UPDATE products
SET attributes = JSON_SET(attributes, '$.price', 1999.98)
WHERE id = 1;

-- 创建虚拟列索引
ALTER TABLE products
ADD COLUMN color VARCHAR(50) AS (JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.color'))) STORED;

CREATE INDEX idx_color ON products(color);

本文由mdnice多平台发布

相关推荐
红尘散仙34 分钟前
我把终端小说阅读器接上了 AI Agent:TRNovel 现在能用 skill 生成书源了
人工智能·后端·rust
卷毛的技术笔记2 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
会编程的土豆2 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
喵个咪3 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
basketball6163 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
qq_2518364573 小时前
SpringBoot+Vue 共享电池柜管理系统 完整实现 前后端分离项目实战 完整代码
vue.js·spring boot·后端
zhangxingchao3 小时前
AI 大模型核心六:量化、Workflow 与 Agent、多轮 RAG
前端·人工智能·后端
IT_陈寒5 小时前
Vite打包时遇到的坑,原来问题出在这里
前端·人工智能·后端
ayqy贾杰6 小时前
基层管理的三板斧,在AI时代行不通了
前端·后端·团队管理
Apifox6 小时前
Apifox 5 月更新|Postman 导入优化、Runner 支持非 root 运行、请求代码自动带鉴权
前端·后端·安全