Bird87 ZA
Bird87 ZA

Reputation: 2160

Converting time from DB to custom time fails

I need to read dates from a db, convert it to a certain timestamp and convert it to JSON.

I have the following code:

package usages

import (
    "fmt"
    "time"
)

type SpecialOffer struct {
    PublishedDate   jsonTime    `gorm:"column:publishing_date" json:"published_date"`
    ExpirationDate  jsonTime    `gorm:"column:expiration_date" json:"expiration_date"`
}

type jsonTime struct {
    time.Time
}

func (tt jsonTime) MarshalJSON() ([]byte, error) {
    jsonTime := fmt.Sprintf("\"%s\"", tt.Format("20060102"))
    return []byte(jsonTime), nil
}

When I run it like this I get the following error:

sql: Scan error on column index 8, name "publishing_date": unsupported Scan, storing driver.Value type time.Time into type *usages.trvTime 

And the data is wrong:

{"published_date":"00010101","expiration_date":"00010101"}

If I change the SpecialOffer struct to use time.Time, it return correct, but obviously the format is wrong:

{"published_date":"2020-03-12T00:00:00Z","expiration_date":"2020-06-12T00:00:00Z"}

What am I doing wrong?

Upvotes: 6

Views: 2050

Answers (1)

mkopriva
mkopriva

Reputation: 38203

You should implement the sql.Scanner and driver.Valuer interfaces.

Something like this:

func (j *jsonTime) Scan(src interface{}) error {
    if t, ok := src.(time.Time); ok {
        j.Time = t
    }
    return nil
}

func (j jsonTime) Value() (driver.Value, error) {
    return j.Time, nil
}

This is necessary because the database/sql package which is used by gorm and some other go ORMs, if not all of them, provides out-of-the-box support for only a handful of types.

Most of the supported types are the language's basic builtin types like string, int, bool, etc. by extension it also supports any custom user defined type whose underlying type is one of the aforementioned basic types, then there's supports for the []byte type and the related sql.RawBytes type, and lastly the time.Time type is also supported ootb.

Any other type that you may want to write to or read from the database will need to implement the two interfaces above. The sql.Scanner's Scan method is invoked automatically after a column's value is decoded into one of the supported types (that's why you need to type assert against time.Time rather than against, say []byte). The driver.Valuer's Value method is invoked automatically before the driver encodes it into a format that's valid for the target column (that's why you can return time.Time directly rather than having the do the encoding yourself).

And keep in mind that

type jsonTime struct {
    time.Time
}

or even

type jsonTime time.Time

declares a new type that is not equal to time.Time and that is why it's not picked up by the database/sql package.

Upvotes: 9

Related Questions