You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

PostgreSQL关联表查询与Go Gin API开发:实现产品及关联变体列表返回并解决类型错误问题

问题:Go实现产品关联变体的GET接口时遇到类型不匹配错误,如何解决并生成期望的响应格式?

我在PostgreSQL里有两张表:productsproduct_variation,后者通过product_id外键关联前者的主键id。我需要开发一个GET接口,返回每个产品及其所有变体的列表,期望的响应格式如下:

{ 
  "product": { 
    "id": 1, 
    "name": "iphone14", 
    "owner": "admin", 
    "price": 50000, 
    "description": "Brand New ", 
    "imgs_url": [ 
      "http://cs.com/variation-images/gsmzuydpmgxltpomrdrglhpgjistxlcqubboahkiatcihktrjunihnlutzyvbbnqi14.jpeg", 
      "http://cs.com/variation-images/gsmzuydpmgxltpomrdrglhpgjistxlcqubboahkiatcihktrjunihnlutzyvbbnqi14.jpeg" 
    ], 
    "imgs": [ 
      "product-images/mlbthlzgvhipftqrwibqwnkcfuywooueavxevnmcwehxvphpbtowxlfrkjpzrmnwapple-iphone-14-pro-max-1.jpg", 
      "product-images/vfnkitshpcoiumbeirmcehsiibciwihmaehmuhnfjnitcctnfpkvmhpobqwfwebni14.jpeg" 
    ], 
    "created_at": "2022-09-26T12:43:41.795886Z" 
  }, 
  "variations": [ 
    { 
      "id": 1, 
      "product_id": 1, 
      "size": "45", 
      "color": "black", 
      "img_url": "http://cs.com/variation-images/jxmhzbbfktqtqmzomgxtnallmnhphttyomqcaifecodguwdmwzvnhxeefdkedtfgi14.jpeg", 
      "img_name": "variation-images/jxmhzbbfktqtqmzomgxtnallmnhphttyomqcaifecodguwdmwzvnhxeefdkedtfgi14.jpeg" 
    }, 
    { 
      "id": 2, 
      "product_id": 1, 
      "size": "46", 
      "color": "blue", 
      "img_url": "http://cs.com/variation-images/gsmzuydpmgxltpomrdrglhpgjistxlcqubboahkiatcihktrjunihnlutzyvbbnqi14.jpeg", 
      "img_name": "variation-images/gsmzuydpmgxltpomrdrglhpgjistxlcqubboahkiatcihktrjunihnlutzyvbbnqi14.jpeg" 
    } 
  ] 
}

我尝试了分开查询产品和变体的SQL,但结果要么是产品数据重复,要么是分离的列表,无法得到期望格式。现有的SQL语句:

-- name: ListProducts :many
SELECT * FROM products ORDER BY id;
-- name: ListVariations :many
SELECT * FROM product_variation ORDER BY id;

Go结构体定义:

type FinalList struct {
  ID int64 `json:"id"`
  Name string `json:"name"`
  Owner string `json:"owner"`
  Price decimal.Decimal `json:"price"`
  Description string `json:"description"`
  ImgsUrl []string `json:"imgs_url"`
  ImgsName []string `json:"imgs_name"`
  CreatedAt time.Time `json:"created_at"`
  Variation ProductVariation `json:"variation"`
}
type ProductVariation struct {
  ID int64 `json:"id"`
  ProductID int64 `json:"product_id"`
  Size string `json:"size"`
  Color string `json:"color"`
  ImgUrl string `json:"img_url"`
  ImgName string `json:"img_name"`
}

核心接口实现:

func (server *Server) listProductsWithVariation(ctx *gin.Context) {
  products, err := server.store.ListProducts(ctx)
  if err != nil {
    ctx.JSON(http.StatusInternalServerError, errorResponse(err))
    return
  }
  var prodVar []FinalList
  for i, prods := range products {
    prodsItems := db.Product{
      Name: prods.Name,
      Price: prods.Price,
      Description: prods.Description,
      Owner: prods.Owner,
      ImgsUrl: prods.ImgsUrl,
      ImgsName: prods.ImgsName,
      CreatedAt: prods.CreatedAt,
    }
    ctx.JSON(http.StatusOK, prodsItems)
    pv, err := server.store.ListVariations(ctx)
    if err != nil {
      if pqErr, ok := err.(*pq.Error); ok {
        switch pqErr.Code.Name() {
        case "foreign_key_violation", "unique_violation":
          ctx.JSON(http.StatusForbidden, errorResponse(err))
          return
        }
      }
      ctx.JSON(http.StatusInternalServerError, errorResponse(err))
      return
    }
    if prods.ID == pv[i].ProductID {
      prodVar = append(prodVar, pv[i])
    }
    ctx.JSON(http.StatusOK, pv)
  }
  ctx.JSON(http.StatusOK, products)
}

现在代码报错:无法将pv[i]db.ProductVariation类型)作为FinalList类型值传入append函数。请问如何解决这个类型错误,同时实现符合期望格式的API响应?


解决方案

咱们一步步拆解问题,先解决类型错误,再优化逻辑实现期望的响应格式:

1. 修正结构体定义(类型错误的核心原因)

你的FinalList结构体有两个关键问题:

  • 每个产品对应多个变体,但你定义的Variation是单个对象,应该改成切片类型
  • 结构体字段结构和期望的响应格式不匹配,期望的是外层包含productvariations两个顶级字段,而不是把产品字段直接平铺

调整后的结构体如下:

// 对应期望响应里的单个产品+变体组合
type ProductResponse struct {
    Product    Product            `json:"product"`
    Variations []ProductVariation `json:"variations"`
}

// 产品结构体,字段标签严格匹配期望的JSON格式
type Product struct {
    ID          int64             `json:"id"`
    Name        string            `json:"name"`
    Owner       string            `json:"owner"`
    Price       decimal.Decimal   `json:"price"`
    Description string            `json:"description"`
    ImgsUrl     []string          `json:"imgs_url"`
    Imgs        []string          `json:"imgs"` // 注意之前的ImgsName要对应期望里的imgs字段
    CreatedAt   time.Time         `json:"created_at"`
}

// 变体结构体,保持原有字段即可
type ProductVariation struct {
    ID        int64  `json:"id"`
    ProductID int64  `json:"product_id"`
    Size      string `json:"size"`
    Color     string `json:"color"`
    ImgUrl    string `json:"img_url"`
    ImgName   string `json:"img_name"`
}

2. 重构接口逻辑(解决N+1查询和匹配错误)

你当前的循环逻辑有两个大问题:

  • 每次循环产品都查询所有变体,属于低效的N+1查询
  • 用产品的索引i去匹配变体的索引,这完全不合理(产品和变体的索引没有对应关系)

正确的逻辑应该是:

  1. 一次性查询所有产品和所有变体
  2. 将变体按product_id分组存入Map,方便快速查找
  3. 遍历每个产品,从Map中取出对应的变体,组装成响应对象

重构后的代码:

func (server *Server) listProductsWithVariation(ctx *gin.Context) {
    // 1. 查询所有产品
    products, err := server.store.ListProducts(ctx)
    if err != nil {
        ctx.JSON(http.StatusInternalServerError, errorResponse(err))
        return
    }

    // 2. 查询所有变体,并按product_id分组
    variations, err := server.store.ListVariations(ctx)
    if err != nil {
        if pqErr, ok := err.(*pq.Error); ok {
            switch pqErr.Code.Name() {
            case "foreign_key_violation", "unique_violation":
                ctx.JSON(http.StatusForbidden, errorResponse(err))
                return
            }
        }
        ctx.JSON(http.StatusInternalServerError, errorResponse(err))
        return
    }

    // 构建变体分组Map:key是product_id,value是该产品的所有变体
    variationMap := make(map[int64][]ProductVariation)
    for _, v := range variations {
        variationMap[v.ProductID] = append(variationMap[v.ProductID], v)
    }

    // 3. 组装最终响应
    var response []ProductResponse
    for _, prod := range products {
        // 转换数据库产品模型到响应模型(如果db.Product和我们定义的Product字段一致可直接复用)
        responseProduct := Product{
            ID:          prod.ID,
            Name:        prod.Name,
            Owner:       prod.Owner,
            Price:       prod.Price,
            Description: prod.Description,
            ImgsUrl:     prod.ImgsUrl,
            Imgs:        prod.ImgsName, // 映射数据库的ImgsName到期望的imgs字段
            CreatedAt:   prod.CreatedAt,
        }

        // 获取当前产品的变体,无变体则返回空切片
        prodVariations := variationMap[prod.ID]

        // 组装单个产品的响应条目
        responseItem := ProductResponse{
            Product:    responseProduct,
            Variations: prodVariations,
        }
        response = append(response, responseItem)
    }

    // 返回最终响应
    ctx.JSON(http.StatusOK, response)
}

3. 可选:用PostgreSQL聚合函数优化查询(减少数据库交互)

如果想进一步优化性能,可以用PostgreSQL的JSON_AGG函数一次性查询出产品和对应的变体,避免两次数据库查询:

-- name: ListProductsWithVariations :many
SELECT 
    p.id,
    p.name,
    p.owner,
    p.price,
    p.description,
    p.imgs_url,
    p.imgs AS imgs_name, -- 对应数据库字段
    p.created_at,
    JSON_AGG(v) AS variations
FROM products p
LEFT JOIN product_variation v ON p.id = v.product_id
GROUP BY p.id
ORDER BY p.id;

然后在Go里定义对应的结构体接收查询结果,直接返回即可,代码会更简洁。


内容的提问来源于stack exchange,提问作者pythonGo

火山引擎 最新活动