Golang中单表设计的最佳实践与处理方法

Golang中单表设计的最佳实践与处理方法 处理单表设计(例如 Dynamo DB)的最佳实践是什么? 假设我们有以下示例表“购物车”:

Id | SubType | Additional attributes ...
abc | Product_def | Milk (Name) | $1 (Price)
abc | Product_ghi | Coffee (Name) | $5 (Price)
abc | Member_jkl | Tanax (Username) | emailaddress@example.com (Email)
abc | DeliveryAddress_mno | Some Address 123 (Address)

获取特定行(例如 Id = "abc" && SubType = "Product_def")很容易,因为你知道它是一个产品,所以可以将其解析为产品。但是,当获取完整的购物车(即 Id = "abc")时,你会得到多行不同类型且具有不同属性的数据。产品有名称和价格,而例如成员有用户名和电子邮件。

我见过几种可以解析这些动态字段的方法。 一种方法是使用一个包含所有子类型所有可能字段的大型结构体,然后你可以使用辅助方法来帮助你判断它是哪种子类型,并基于此获取特定的结构体。

type MyLargeStruct struct {
    Id string
    SubType string
    Name string
    Price string
    Username string
    Email string
    Address string
}

type Product struct {
    Id string
    SubType string
    Name string
    Price string
}

func (mls *MyLargeStruct) IsProduct() bool {
    return strings.HasPrefix(mls.SubType, "Product")
}

func (mls *MyLargeStruct) GetProduct() *Product {
    return &Product{ copy fields values to product here.. }
}

另一种方法可以简单地将每个结果行解析为 var line map[string]interface{},然后基于 strings.HasPrefix(line["SubType"], "Product") 的判断,你可以将其编组为 JSON,然后解组到特定的产品结构体中。

你会建议使用哪一种?还有我没有想到的其他选项吗? 谢谢!


更多关于Golang中单表设计的最佳实践与处理方法的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

我看到两种选择:

我认为,使用关系型数据库却不实际使用其关联关系是一种反模式。如果你想要单表设计,就应该使用为此设计而构建的数据库和客户端库。

更多关于Golang中单表设计的最佳实践与处理方法的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你好 lutzhorn,

感谢你的回复! 我同意你所说的一切,并且我已经在使用一个 Dynamo DB 客户端库(以 gocloud 的 docstore 形式存在的 Go 自己的库)。

我的问题不是如何从数据库获取数据,而是如何以最佳方式构建 Go 结构体(即数据实体)来处理单表设计。据我所知,目前没有库内置此功能。

也许嵌入类型会有所帮助。

package main

import (
	"fmt"
)

type Common struct {
	Id      string
	SubType string
}

type User struct {
	Common   Common
	Username string
	Email    string
	Address  string
}

func main() {
	user := User{Username: "john.doe", Common: Common{Id: "user-1", SubType: "User"}}
	fmt.Println(user)
}

在单表设计中处理多实体类型时,推荐使用结构化映射而非大型结构体或动态map。以下是具体实现方案:

方案一:接口驱动设计(推荐)

// 定义基础接口
type CartItem interface {
    GetID() string
    GetSubType() string
    Validate() error
}

// 基础结构体
type BaseItem struct {
    ID      string `dynamodbav:"Id"`
    SubType string `dynamodbav:"SubType"`
}

func (b BaseItem) GetID() string    { return b.ID }
func (b BaseItem) GetSubType() string { return b.SubType }

// 具体类型实现
type ProductItem struct {
    BaseItem
    Name  string `dynamodbav:"Name"`
    Price string `dynamodbav:"Price"`
}

func (p ProductItem) Validate() error {
    if p.Name == "" || p.Price == "" {
        return errors.New("product fields missing")
    }
    return nil
}

type MemberItem struct {
    BaseItem
    Username string `dynamodbav:"Username"`
    Email    string `dynamodbav:"Email"`
}

func (m MemberItem) Validate() error {
    if m.Username == "" || m.Email == "" {
        return errors.New("member fields missing")
    }
    return nil
}

// 工厂函数解析
func ParseCartItem(attrs map[string]interface{}) (CartItem, error) {
    // 先解析基础字段
    data, err := dynamodbattribute.MarshalMap(attrs)
    if err != nil {
        return nil, err
    }
    
    var base BaseItem
    if err := dynamodbattribute.UnmarshalMap(data, &base); err != nil {
        return nil, err
    }
    
    // 根据SubType选择具体类型
    switch {
    case strings.HasPrefix(base.SubType, "Product"):
        var product ProductItem
        if err := dynamodbattribute.UnmarshalMap(data, &product); err != nil {
            return nil, err
        }
        return product, nil
        
    case strings.HasPrefix(base.SubType, "Member"):
        var member MemberItem
        if err := dynamodbattribute.UnmarshalMap(data, &member); err != nil {
            return nil, err
        }
        return member, nil
        
    default:
        return nil, fmt.Errorf("unknown subtype: %s", base.SubType)
    }
}

// 使用示例
func GetCartItems(id string) ([]CartItem, error) {
    result, err := dynamodbClient.Query(&dynamodb.QueryInput{
        TableName:              aws.String("ShoppingCart"),
        KeyConditionExpression: aws.String("Id = :id"),
        ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
            ":id": {S: aws.String(id)},
        },
    })
    if err != nil {
        return nil, err
    }
    
    var items []CartItem
    for _, item := range result.Items {
        cartItem, err := ParseCartItem(item)
        if err != nil {
            return nil, err
        }
        items = append(items, cartItem)
    }
    
    return items, nil
}

方案二:类型安全的JSON编组

// 使用自定义JSON编组
type CartItem struct {
    ID      string                 `json:"id"`
    SubType string                 `json:"subType"`
    Data    map[string]interface{} `json:"data"`
}

// 类型安全的获取方法
func (c CartItem) AsProduct() (*Product, error) {
    if !strings.HasPrefix(c.SubType, "Product") {
        return nil, errors.New("not a product")
    }
    
    jsonData, err := json.Marshal(c.Data)
    if err != nil {
        return nil, err
    }
    
    var product Product
    if err := json.Unmarshal(jsonData, &product); err != nil {
        return nil, err
    }
    
    return &product, nil
}

func (c CartItem) AsMember() (*Member, error) {
    if !strings.HasPrefix(c.SubType, "Member") {
        return nil, errors.New("not a member")
    }
    
    jsonData, err := json.Marshal(c.Data)
    if err != nil {
        return nil, err
    }
    
    var member Member
    if err := json.Unmarshal(jsonData, &member); err != nil {
        return nil, err
    }
    
    return &member, nil
}

方案三:使用泛型(Go 1.18+)

// 泛型解析器
type ItemParser[T any] struct {
    SubTypePrefix string
}

func (p ItemParser[T]) Parse(attrs map[string]interface{}) (*T, error) {
    // 检查SubType
    subType, ok := attrs["SubType"].(string)
    if !ok || !strings.HasPrefix(subType, p.SubTypePrefix) {
        return nil, errors.New("type mismatch")
    }
    
    // 解析到具体类型
    var item T
    if err := dynamodbattribute.UnmarshalMap(attrs, &item); err != nil {
        return nil, err
    }
    
    return &item, nil
}

// 使用示例
func GetCartItemsGeneric(id string) ([]interface{}, error) {
    // 查询逻辑...
    
    var items []interface{}
    for _, attr := range result.Items {
        switch {
        case strings.HasPrefix(attr["SubType"].(string), "Product"):
            parser := ItemParser[ProductItem]{SubTypePrefix: "Product"}
            product, err := parser.Parse(attr)
            if err != nil {
                return nil, err
            }
            items = append(items, product)
            
        case strings.HasPrefix(attr["SubType"].(string), "Member"):
            parser := ItemParser[MemberItem]{SubTypePrefix: "Member"}
            member, err := parser.Parse(attr)
            if err != nil {
                return nil, err
            }
            items = append(items, member)
        }
    }
    
    return items, nil
}

避免大型结构体的原因

  1. 内存浪费:每个实例都包含所有字段的内存分配
  2. 类型安全缺失:编译器无法检查字段使用的正确性
  3. 维护困难:添加新类型需要修改所有使用该结构体的代码
  4. 序列化问题:空字段在JSON/DynamoDB中仍会占用空间

查询优化建议

// 使用GSI进行类型过滤
func GetCartItemsByType(id, subTypePrefix string) ([]CartItem, error) {
    result, err := dynamodbClient.Query(&dynamodb.QueryInput{
        TableName:              aws.String("ShoppingCart"),
        IndexName:              aws.String("Id-SubType-index"), // GSI
        KeyConditionExpression: aws.String("Id = :id AND begins_with(SubType, :prefix)"),
        ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
            ":id":     {S: aws.String(id)},
            ":prefix": {S: aws.String(subTypePrefix)},
        },
    })
    // 解析逻辑...
}

接口驱动方案提供最佳的类型安全和可维护性,同时保持DynamoDB单表设计的优势。

回到顶部