Mysql存储-EAV模式

Mysql存储-EAV模式

最近又又又搞一点新东西,要整合不同业务进行存储和查询,一波学习过后总结了一下可扩展性MAX的eav模式存储。

在eav这里的数据结构设计尤为关键,需要充分考虑你需要使用的字段、使用场景,当数据结构设计完成后便会发现eav模型需要多次join操作才能完成查询,因此性能优化的难点也是在如何充分使用索引

一、简介

1、概念

EAV(Entity-Attribute-Value)模式,也称为对象-属性-值模式,是一种常用于数据库设计的灵活模式,适用于具有大量属性和属性值的实体。它在MySQL数据库中的实现可以解决一些传统关系型数据库表结构无法轻松满足的需求,例如动态属性、稀疏属性等。

EAV模式的核心思想是将实体(Entity)的属性(Attribute)和值(Value)分别存储在不同的表中。这样可以在不修改表结构的情况下轻松添加或删除属性,从而提高数据库的灵活性。

EAV模式在MySQL数据库中通常包含以下三个表:

  1. 实体表(Entity Table):存储实体的基本信息,如ID、名称等。每个实体对应该表中的一行记录。
  2. 属性表(Attribute Table):存储属性的元数据,如属性ID、属性名称、数据类型等。每个属性对应该表中的一行记录。
  3. 值表(Value Table):存储实体的属性值。每个属性值对应该表中的一行记录,包括实体ID、属性ID和属性值。

在这里插入图片描述

2、特点

EAV模式的优点:

  1. 高度灵活:可以轻松添加、删除或修改属性,而无需更改表结构。
  2. 节省存储空间:对于具有大量稀疏属性的实体,EAV模式可以避免在数据表中存储大量NULL值。

EAV模式的缺点:

  1. 查询复杂:由于属性和值分散在多个表中,查询和聚合操作通常需要多表连接,导致查询性能较差。
  2. 数据完整性:EAV模式较难实现属性值的数据类型和约束检查,可能导致数据完整性问题。

二、详细设计

在这里插入图片描述

写入时:

  • 在实际业务上会接入不同领域的数据,不同领域数据内容也不尽相同,在领域分治的情况下便只需要考虑单一的固定数据。

  • 同一领域内数据具有一定的相似性,将较多出现的数据存放于entity表中,以减少多次join操作的情况,性能++

  • 同一领域内的相同扩展字段名称可能会出现不同数据类型的情况,因此需要在attributes表中增加name、type的唯一键,进行upsert操作,保证该表数据满足全部场景

  • 根据传入的interface类型,将数据存储到对应的字段中。例如,如果传入的数据是整数类型,将数据存储到int_value字段中

查询时:

  • 需要增加表,用于记录单个领域下的entity中的固定字段,在查询时先查询该领域的固定字段是否cover查询要求的字段,如果cover住则不需要查询values表。
  • 根据attributes表中的type字段进行“类型断言”。例如,如果attributes表中的type值为’int’,则从values表中的int_value字段中读取数据(应在各场景下最大程度地减少使用断言)
    • 类型断言是Golang内置的特性,不需要额外引入包
    • 反射是指在运行时动态获取变量的类型信息、操作变量的方法

三、demo

SQL:

CREATE TABLE entities (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    status VARCHAR(255) NOT NULL,
    type VARCHAR(255) NOT NULL
);

CREATE TABLE attributes (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL UNIQUE
);

CREATE TABLE values (
    entity_id INT,
    attribute_id INT,
    value VARCHAR(255) NOT NULL,
    PRIMARY KEY (entity_id, attribute_id),
    FOREIGN KEY (entity_id) REFERENCES entities(id),
    FOREIGN KEY (attribute_id) REFERENCES attributes(id)
);

Golang:

package main

import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
)

type Data struct {
	ID         int
	Name       string
	Status     string
	Type       string
	ExtraData  map[string]string
}

func main() {
	db, err := sql.Open("mysql", "username:password@tcp(localhost:3306)/dbname")
	if err != nil {
		panic(err)
	}
	defer db.Close()

	// 插入数据
	extraData := map[string]string{
		"check_data": "2021-10-01",
		"start_time": "10:00:00",
	}

	entityID, err := insertData(db, "name1", "status1", "type1", extraData)
	if err != nil {
		panic(err)
	}

	// 查询数据
	data, err := getData(db, entityID)
	if err != nil {
		panic(err)
	}

	fmt.Printf("Data: %+v\n", data)
}

func insertData(db *sql.DB, name, status, dataType string, extraData map[string]string) (int, error) {
	res, err := db.Exec("INSERT INTO entities (name, status, type) VALUES (?, ?, ?)", name, status, dataType)
	if err != nil {
		return 0, err
	}

	entityID, err := res.LastInsertId()
	if err != nil {
		return 0, err
	}

	for attributeName, value := range extraData {
		attributeID, err := getOrCreateAttribute(db, attributeName)
		if err != nil {
			return 0, err
		}

		_, err = db.Exec("INSERT INTO values (entity_id, attribute_id, value) VALUES (?, ?, ?)", entityID, attributeID, value)
		if err != nil {
			return 0, err
		}
	}

	return int(entityID), nil
}

func getData(db *sql.DB, entityID int) (*Data, error) {
	row := db.QueryRow("SELECT id, name, status, type FROM entities WHERE id = ?", entityID)

	var data Data
	err := row.Scan(&data.ID, &data.Name, &data.Status, &data.Type)
	if err != nil {
		return nil, err
	}

	rows, err := db.Query("SELECT a.name, v.value FROM attributes a JOIN values v ON a.id = v.attribute_id WHERE v.entity_id = ?", entityID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	data.ExtraData = make(map[string]string)
	for rows.Next() {
		var attributeName, value string
		if err := rows.Scan(&attributeName, &value); err != nil {
			return nil, err
		}
		data.ExtraData[attributeName] = value
	}

	return &data, nil
}

func getOrCreateAttribute(db *sql.DB, attributeName string) (int, error) {
	var attributeID int
	err := db.QueryRow("SELECT id FROM attributes WHERE name = ?", attributeName).Scan(&attributeID)
	if err == sql.ErrNoRows {
		res, err := db.Exec("INSERT INTO attributes (name) VALUES (?)", attributeName)
		if err != nil {
			return 0, err
		}

		id, err := res.LastInsertId()
		if err != nil {
			return 0, err
		}
		attributeID = int(id)
	} else if err != nil {
		return 0, err
	}

	return attributeID, nil
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,动态表单数据的存储方式可以使用 EAV(Entity-Attribute-Value)模型,它可以灵活地存储不同表单的数据,但是在查询时需要进行多个表的联结,效率较低。另一种存储方式是使用 JSON 字段,将表单数据存储为 JSON 格式的字符串,但是在查询时需要使用 JSON 函数,也会影响效率。 下面是一个基于 EAV 模型的动态表单数据存储表结构设计: 1. 表单定义表(form_definition): | 字段名 | 类型 | 描述 | | ------ | ---- | ---- | | id | int | 表单定义 ID | | name | varchar | 表单名称 | | description | varchar | 表单描述 | 2. 字段定义表(field_definition): | 字段名 | 类型 | 描述 | | ------ | ---- | ---- | | id | int | 字段定义 ID | | form_id | int | 表单定义 ID | | name | varchar | 字段名称 | | type | varchar | 字段类型(文本、数字、日期等) | | is_required | boolean | 是否必填 | | other_properties | json | 其他属性(例如最大长度、正则表达式等) | 3. 表单数据表(form_data): | 字段名 | 类型 | 描述 | | ------ | ---- | ---- | | id | int | 表单数据 ID | | form_id | int | 表单定义 ID | | created_at | datetime | 数据创建时间 | 4. 字段数据表(field_data): | 字段名 | 类型 | 描述 | | ------ | ---- | ---- | | id | int | 字段数据 ID | | form_data_id | int | 表单数据 ID | | field_id | int | 字段定义 ID | | value | varchar | 字段值 | 使用这种表结构设计,可以实现灵活的动态表单数据存储查询。当需要查询某个表单的数据时,可以通过联结表单定义表、字段定义表和表单数据表,再关联字段数据表,以获取完整的表单数据。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值