0
votes

I'm using Mysql 8. I'm also utilizing 99designs/gqlgen to autogenerate the structs based on the GraphQL schema. I was to re-use the same structs when scanning MySql responses. And on top of that, while prototyping, I want to have some JSONs in my table. So the struct is:

type CustomizedItemInput struct {
    Sku              string                 `json:"sku"`
    Name             string                 `json:"name"`
    Skus             []*CustomizedComponent `json:"skus"`
    ...

Since storing(providing Value()) is simpler I managed to store Skus into DB as a top-level JSON successfully. Looks like this:

[{"sku": "123", "position": "LEFT"}, {"sku": "456", "position": "RIGHT"}]

Now, how do I get this value out of DB and back into a array of pointers inside the struct without much hustle? Of course, ideally it should be done without changing the underlying struct because it's autogenerated.

UPDATE: Adding debugging information. I need to read a DB row into CustomizedItemView which basically mirrors CustomizedItemInput from above:

type CustomizedItemView struct {
    Sku              string                     `json:"sku"`
    Name             string                     `json:"name"`
    Skus             []*CustomizedComponentView `json:"skus"`
    ...

Of course, when I say "without hustle" I mean having the DB row extracted into a struct seamlessly. I can add map[string]interface{}{} with all bells and whistles and get the value. But I want to have it neat, like:

    var storedCustItem = model.CustomizedItemView{}
    err := udb.Get(&storedCustItem, database.SelectCustomizationQuery, userID, custItem.Sku, createdAt)

The error I get is:

2020/10/10 20:38:24 sql: Scan error on column index 8, name "skus": unsupported Scan, storing driver.Value type []uint8 into type *[]*model.CustomizedComponentView

(8 because I removed some fields for the example). The main problem is that I can't create Scan() for an unnamed type. I have created wrappers for Value() because my inserts are more verbose and I do type conversion with the wrapper type in them:

type CustomizedComponentsIn []*CustomizedComponent
...
func (customizedComponents CustomizedComponentsIn) Value() (driver.Value, error)
...
tx.MustExec(database.SaveCustomizationCommand,
        custItem.Sku,
        custItem.Name,
        model.CustomizedComponentsIn(custItem.Skus)
...

,which is Ok for inserts because there will be some values that do not belong to the input struct. But I hoped to at least get the value scanned into a View struct automatically.

1
You can either change the type of the Skus field to a named slice type, then have this named slice type implement the sql.Scanner interface. If you can't change the type of the Skus field then you could write a wrapper type that implements the interface or a conversion function and invoke this this conversion function or use this wrapper type whenever you are scanning the Skus field, which requires that you explicitly enumerate the fields when scanning the rows.mkopriva
@mkopriva, I have added more detail to the question. Can you take a look? I'm relatively new to go, so Im not sure I get it what you mean by the wrapper. I see an example from your answer, but it does imply changing the generated struct.yuranos
I've updated my answer, let me know if that makes it more clear.mkopriva

1 Answers

1
votes

If you can change the type of the Skus field, the common approach would be to declare a slice type that implements the sql.Scanner and driver.Valuer interfaces and use that instead of the unnamed []*CustomizedComponent type.

For example:

type CustomizedItemInput struct {
    Sku  string                   `json:"sku"`
    Name string                   `json:"name"`
    Skus CustomizedComponentSlice `json:"skus"`
    // ...
}

type CustomizedComponentSlice []*CustomizedComponent

// Value implements driver.Valuer interface.
func (s CustomizedComponentSlice) Value() (driver.Value, error) {
    return json.Marshal(s)
}

// Scan implements sql.Scanner interface.
func (s *CustomizedComponentSlice) Scan(src interface{}) error {
    var data []byte
    switch v := src.(type) {
    case string:
        data = []byte(v)
    case []byte:
        data = v
    default:
        return nil
    }
    return json.Unmarshal(data, s)
}

If you can't change type of the Skus field you will have to explicitly convert the field during scanning.

For example, given the above named slice type, you could do something like this:

v := new(CustomizedItemView)
row := db.QueryRow("SELECT sku, name, skus FROM customized_item_view WHERE sku = ? LIMIT 1", sku)
err := row.Scan(
    &v.Sku,
    &v.Name,
    // do the conversion here, and any other place where you're scanning Skus... 
    (*CustomizedComponentSlice)(&v.Skus),
)
if err != nil {
   return err
}
fmt.Println(v.Skus) // result