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
我看到两种选择:
- 使用 DynamoDB 客户端库
- 或者使用具有关联关系的关系型数据库。
我认为,使用关系型数据库却不实际使用其关联关系是一种反模式。如果你想要单表设计,就应该使用为此设计而构建的数据库和客户端库。
更多关于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
}
避免大型结构体的原因
- 内存浪费:每个实例都包含所有字段的内存分配
- 类型安全缺失:编译器无法检查字段使用的正确性
- 维护困难:添加新类型需要修改所有使用该结构体的代码
- 序列化问题:空字段在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单表设计的优势。

