Golang中使用GORM进行联表查询并映射到结构体的实践

Golang中使用GORM进行联表查询并映射到结构体的实践 我需要能够执行包含连接(JOIN)的查询,并将数据填充到一个结构体中,这可以实现吗?

例如这个查询:

err := db.Select("orders.*, user.customer_id, user.first_name, user.last_name, user.email").Where("orders.created_at >= now() - interval 3 day").Joins("left join users as user on user.customer_id = orders.customer_id").Find(&orders).Error

我希望以这种方式构建查询的原因是,存在许多变量(例如过滤器),因此查询可能差异很大,但返回的数据结构始终相同。只是查询条件会改变,例如颜色、材料、性别这类条件。

这个查询会返回正确的数据,但我真正想要的是能够填充像这样的结构体:

type Orders struct {
	CustomerId    int       `json:"customerId"`
	User          Users     `json:"user"`
    Products      []Products`json:"products"`
	Method        string    `json:"method"`
	Amount        float64   `json:"amount"`
	Subtotal      float64   `json:"subtotal"`
	Total         float64   `json:"total"`
	Btw           float64   `json:"btw"`
	Status        string    `json:"status"`
	OrderMailSend int       `json:"orderMailSend"`
}

其中,“user”字段显然会包含一个用户数据的结构体,而“products”字段将包含一个产品结构体的切片。

我无法以一种高效的方式实现这个功能。希望有人能告诉我如何操作,或者指引我正确的方向。

用户表的表名是“users”,用于匹配的ID列名为“customer_id”。

订单表的表名是“orders”,它也有一个名为“customer_id”的列,我们可以用它来进行匹配。

我的Go结构体:

type Orders struct {
	CustomerId    int     `json:"customerId"`
	Users         Users   `json:"users"`
	Method        string  `json:"method"`
	Amount        float64 `json:"amount"`
	Subtotal      float64 `json:"subtotal"`
	Total         float64 `json:"total"`
	Btw           float64 `json:"btw"`
	Status        string  `json:"status"`
	OrderMailSend int     `json:"orderMailSend"`
}

type Users struct {
	CustomerId int    `json:"customerId"`
	Email      string `json:"email"`
	FirstName  string `json:"firstName"`
	LastName   string `json:"lastName"`
}

我收到的错误信息是:

无法为 app.Orders 预加载 users 字段

而在那些我成功执行了连接查询的情况下,我的结构体只会填充订单详情,而“customer”详情部分则为空。


更多关于Golang中使用GORM进行联表查询并映射到结构体的实践的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中使用GORM进行联表查询并映射到结构体的实践的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


是的,可以通过GORM的预加载(Preload)和关联(Association)功能实现联表查询并映射到嵌套结构体。以下是几种实现方式:

方法1:使用预加载(Preload)和关联标签

首先,在结构体中使用GORM标签定义关联关系:

type Orders struct {
    ID            uint      `gorm:"primaryKey" json:"id"`
    CustomerId    int       `json:"customerId"`
    User          Users     `gorm:"foreignKey:CustomerId;references:CustomerId" json:"user"`
    Products      []Product `gorm:"foreignKey:OrderId" json:"products"`
    Method        string    `json:"method"`
    Amount        float64   `json:"amount"`
    Subtotal      float64   `json:"subtotal"`
    Total         float64   `json:"total"`
    Btw           float64   `json:"btw"`
    Status        string    `json:"status"`
    OrderMailSend int       `json:"orderMailSend"`
    CreatedAt     time.Time `json:"createdAt"`
}

type Users struct {
    CustomerId int    `gorm:"primaryKey" json:"customerId"`
    Email      string `json:"email"`
    FirstName  string `json:"firstName"`
    LastName   string `json:"lastName"`
}

type Product struct {
    ID      uint    `gorm:"primaryKey" json:"id"`
    OrderId uint    `json:"orderId"`
    Name    string  `json:"name"`
    Price   float64 `json:"price"`
}

然后使用预加载进行查询:

var orders []Orders

// 基本预加载查询
err := db.Preload("User").Preload("Products").
    Where("orders.created_at >= now() - interval 3 day").
    Find(&orders).Error

// 带条件的预加载
err := db.Preload("User").
    Preload("Products", func(db *gorm.DB) *gorm.DB {
        return db.Where("price > ?", 100)
    }).
    Where("orders.status = ?", "completed").
    Find(&orders).Error

// 动态条件查询示例
func GetOrdersWithFilters(filters map[string]interface{}) ([]Orders, error) {
    var orders []Orders
    query := db.Model(&Orders{}).Preload("User").Preload("Products")
    
    // 动态添加条件
    if color, ok := filters["color"]; ok {
        query = query.Joins("JOIN order_details ON order_details.order_id = orders.id").
            Where("order_details.color = ?", color)
    }
    
    if material, ok := filters["material"]; ok {
        query = query.Joins("JOIN order_details ON order_details.order_id = orders.id").
            Where("order_details.material = ?", material)
    }
    
    err := query.Find(&orders).Error
    return orders, err
}

方法2:使用Joins配合Select和Scan

如果需要更复杂的JOIN查询,可以使用Scan到自定义结构体:

type OrderWithDetails struct {
    Orders
    UserFirstName string    `json:"userFirstName"`
    UserLastName  string    `json:"userLastName"`
    UserEmail     string    `json:"userEmail"`
}

var results []OrderWithDetails

err := db.Table("orders").
    Select("orders.*, users.first_name as user_first_name, users.last_name as user_last_name, users.email as user_email").
    Joins("LEFT JOIN users ON users.customer_id = orders.customer_id").
    Where("orders.created_at >= now() - interval 3 day").
    Scan(&results).Error

方法3:使用Raw SQL和Scan

对于复杂的多表连接查询:

type OrderFullDetails struct {
    CustomerId    int       `json:"customerId"`
    Method        string    `json:"method"`
    Amount        float64   `json:"amount"`
    UserEmail     string    `json:"userEmail"`
    UserFirstName string    `json:"userFirstName"`
    UserLastName  string    `json:"userLastName"`
    ProductNames  string    `json:"productNames"` // 聚合字段
}

var details []OrderFullDetails

err := db.Raw(`
    SELECT 
        o.customer_id,
        o.method,
        o.amount,
        u.email as user_email,
        u.first_name as user_first_name,
        u.last_name as user_last_name,
        GROUP_CONCAT(p.name) as product_names
    FROM orders o
    LEFT JOIN users u ON u.customer_id = o.customer_id
    LEFT JOIN products p ON p.order_id = o.id
    WHERE o.created_at >= NOW() - INTERVAL 3 DAY
    GROUP BY o.id, u.customer_id
`).Scan(&details).Error

方法4:自定义预加载逻辑

如果需要处理复杂的嵌套结构:

func GetOrdersWithNestedData(filters map[string]interface{}) ([]Orders, error) {
    var orders []Orders
    
    // 先查询订单
    query := db.Model(&Orders{})
    
    // 动态添加条件
    for key, value := range filters {
        switch key {
        case "status":
            query = query.Where("status = ?", value)
        case "min_amount":
            query = query.Where("amount >= ?", value)
        case "date_from":
            query = query.Where("created_at >= ?", value)
        }
    }
    
    if err := query.Find(&orders).Error; err != nil {
        return nil, err
    }
    
    // 获取所有订单ID
    var orderIDs []uint
    for _, order := range orders {
        orderIDs = append(orderIDs, order.ID)
    }
    
    // 批量查询关联的用户
    var users []Users
    if err := db.Where("customer_id IN (SELECT customer_id FROM orders WHERE id IN ?)", orderIDs).
        Find(&users).Error; err != nil {
        return nil, err
    }
    
    // 批量查询关联的产品
    var products []Product
    if err := db.Where("order_id IN ?", orderIDs).Find(&products).Error; err != nil {
        return nil, err
    }
    
    // 手动映射数据
    userMap := make(map[int]Users)
    for _, user := range users {
        userMap[user.CustomerId] = user
    }
    
    productMap := make(map[uint][]Product)
    for _, product := range products {
        productMap[product.OrderId] = append(productMap[product.OrderId], product)
    }
    
    for i := range orders {
        if user, ok := userMap[orders[i].CustomerId]; ok {
            orders[i].User = user
        }
        if prods, ok := productMap[orders[i].ID]; ok {
            orders[i].Products = prods
        }
    }
    
    return orders, nil
}

注意事项

  1. 确保外键关系正确:GORM默认使用UserID作为外键,如果字段名不同需要使用标签指定
  2. 性能考虑:预加载会产生额外的查询,复杂查询时注意N+1问题
  3. 表名映射:GORM默认使用结构体名的复数形式作为表名,可通过TableName()方法自定义
  4. 字段映射:使用gorm:"column:custom_column_name"标签指定列名

对于你的具体需求,推荐使用方法1的预加载方式,它最符合GORM的设计理念且代码最简洁。

回到顶部