mysqldata-structuresgo-gormgrpc-go

How to create a data structure for objects that are technically the same thing but doesn't always share the same attributes?


I know that title was a mouthful, but I hope that summarizes my question well. So I want to represent some sensors that vary in the fields they have. Some of them share fields, other have a field that only they have. Now, I want to get those infos in a good way. I thought of some hacky way to do it, but it involves hardcoding the table name and passing that to the client as well. Here's what my sensor info tables looks like:

Sensor1
    -Info1
    -Info2
    -Info3
Sensor2
    -Info4
    -Info5
    -Info6
    -Info7
Sensor3
    -Info8
Sensor4
    -Info9
    -Info10
    -Info11
Sensor5
    -Info12
    -Info13
    -Info14
    -Info15
Sensor6
    -Info15
    -Info14
    -Info18
    -Info9
    -Info10
    -Info11
    -Info12

I could think of some solutions to this, but everything would just be messy and ugly.

Here's what my current hacks looks like:

ginGine.GET("/sensors", func(ctx *gin.Context) {
    //[table name][display name]
    sensorLst := map[string]string{
        "sensor_1_table_name": "Sensor 1",
        "sensor_2_table_name": "Sensor 2",
        "sensor_3_table_name": "Sensor 3",
        "sensor_4_table_name": "Sensor 4",
        "sensor_5_table_name": "Sensor 5",
        "sensor_6_table_name": "Sensor 6",
    }

    type Record struct {
        ID string
    }

    //[table name][info array]
    respMap := make(map[string][]map[string]string)
    for k, v := range sensorLst {
        recs := []Record{}
        cmmHelpers.GetDB().Raw(fmt.Sprintf("SELECT id FROM %s", k)).Find(&recs)

        for _, el := range recs {
            //[record id][display name]
            info := map[string]string{}

            //populate info map
            info["id"] = el.ID
            info["table_name"] = k
            info["display_name"] = v
            //get the existing info list/map associated with the table name key of sensorLst
            innerMap := respMap[k]

            //add the new info to innerMap
            innerMap = append(innerMap, info)
            //replace the old innerMap with the updated one
            respMap[k] = innerMap
        }
    }

    //convert map to byte[]
    jsonData, err := json.Marshal(respMap)
    if err != nil {
        fmt.Println("Error marshaling JSON:", err)
        return
    }

    ctx.Data(200, "application/json; charset=utf-8", []byte(jsonData))
})

UPDATE:

I thought of a possible solution and want to get some opinion about it. What if I made a monolith Sensor model which has all the possible fields. Then on the gin parts, I'll use the old small models for the parts that need them. Then on saving, I'll "transfer" the data those small models contains to the monolith model, so I'll have a sensor table. I tried looking at interfaces, but it doesn't work like most object oriented language. (Just realized that solution is still messy, but at least it's not a pig pen I guess that would involve more hacky ways to retrieve it later)


Solution

  • I already found a solution to this, which is basically what I had in my update. I made a monolith Sensor model then used the smaller ones for data structure validation with gin to ensure that only the fields necessary for a sensor type is required.

    Here's what the monolith looks like:

    type Sensor struct {
        gorm.Model
        ID string `gorm:"primarykey; size:40;" json:"id"`
        Name string `json:"name"`
        Type SensorType `json:"type"`
        Info1 float32 `json:"info_1"`
        Info2 float32 `json:"info_2"`
        ...
        ...
        InfoN float32 `json:"info_n"`
    }
    

    Then for the validation models, they look like this:

    type Sensor1 struct {
        gorm.Model
        ID string `gorm:"primarykey; size:40;" json:"id"`
        Name string
        Info1 *float32 `gorm:"not null" json:"info_1" binding:"required"`
        Info2 *float32 `gorm:"not null" json:"info_2" binding:"required"`
        ...
    }
    

    Notice that pointer for the variable type and binding:"required", they are both necessary for gin to really require them.

    Then the actual code looks like this:

    //client side

    ginGine.POST("/sensor_1", func(ctx *gin.Context) {
        var sensor_1 models.Sensor1
    
        err := ctx.ShouldBind(&sensor_1)
        if err != nil {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "error": err.Error(),
            })
            return
        }
    }
    

    //server side

    func (*Sensor1Server) CreateSensor1(ctx context.Context, req *pb.CreateSensor1Request) (*pb.CreateSensor1Response, error) {
        fmt.Printf("Create Sensor1 Timestamp: %v\n", time.Now())
    
        sensor_1 := req.GetSensor1()
        sensor_1.Id = uuid.New().String()
    
        data := models.Sensor{
            ID:    sensor_1.GetId(),
            Name:  sensor_1.GetName(),
            Type:  models.SENSOR1,
            Info1: sensor_1.GetInfo1(),
            Info2: sensor_1.GetInfo2(),
            ...
            ...
        }
    
        res := cmmHelpers.GetDB().Create(&data)
        ...
        ...
    }