概要
掲題の通りです。
ニッチな内容なので需要ないと思いますが、自分の備忘用としてのpostになります。
課題
普段、Webサーバー側の開発はGAE/Goを利用しており、特に指定がない限りDBはDatastoreを利用しています。
ただ JSONの取り扱いでよく困るのが time.Time
の挙動です。
この子、何も指定がない場合は RFC3339
形式の文字列としてJSONに格納されます。
import (
"time"
"encoding/json"
)
type Hoge struct {
ID int64 `json:"id" datastore:"-"`
Name string `json:"name" datastore:"name"`
CreatedAt time.Time `json:"createdAt" datastore:"createdAt"`
}
var hoge := Hoge {
ID : 1,
Name : "test",
CreatedAt: time.Now(),
}
jsonStr, _ := json.Marshal(&hoge)
fmt.Println(string(jsonStr)) // { "id": 1, "name":"test", "createdAt": "2022-02-18T14:12:53.4242+09:00" }
まぁ、JavaScript側も RFC3339
に対応してるんで、それでもいいんですけどね。
ただUnixTime値を返したいというケースも多く、そういう場合ちょっと面倒ですよね。
解決策
いくつかありますが、それぞれメリット/デメリットがあったりします。
- MarchalJSON / UnmarshalJSON を各structにて実装
- 独自のTime構造体で対応
- Named Typeで対応 (NG)
- Named Typeで対応し、mercari/boomを利用してDataStoreに保存
解決方法1: MarchalJSON / UnmarshalJSON を各structにて実装
上記の場合、以下のようにMarshalJSON, UnmarshalJSONを定義することで解決します。
MarshalJSON, UnmarshalJSON はそれぞれ Marshaler
Unmarshaler
インタフェースの実装内容で、このインタフェースを持たせておくと json.Marshal (json.Unmarshal) 時に自動で呼び出され、変換内容を設定することができます。
type Hoge struct {
ID int64 `json:"id" datastore:"-"`
Name string `json:"name" datastore:"name"`
CreatedAt time.Time `json:"-" datastore:"createdAt"` // jsonの指定を外す
}
type HogeJSON struct {
Hoge
CreatedAtUnixTime int64 `json:"createdAt"`
}
func (h *Hoge) MarshalJSON() ([]byte, error) {
hoge := HogeJSON {
Hoge: *h,
CreatedAtUnixTime: h.CreatedAt.Unix(),
}
return json.Marshal(&hoge)
}
func (h *Hoge) UnmarshalJSON(bt []byte) error {
hoge := HogeJSON {}
if err := json.Unmarshal(bt, &hoge); err != nil {
return err
}
hoge.Hoge.CreatedAt = time.Unix(hoge.CreatedAtUnixTime, 0)
*h = hoge.Hoge
return nil
}
やってることは Hoge
を embedding してる HogeJSON
を作成し、json ⇔ struct への変換時に噛ませてる感じです。
メリット
正攻法な気がする。標準のパッケージだけでうまくいく
デメリット
いちいちやるのめんどい
解決方法2: 独自のTime構造体で対応
よくネットに載ってますが、DataStoreの保存時に変になっちゃう…?
type Hoge struct {
ID int64 `json:"id" datastore:"-"`
Name string `json:"name" datastore:"name"`
CreatedAt CustomTime `json:"createdAt" datastore:"createdAt"`
}
type CustomTime struct {
time.Time
}
func (h *CustomTime) MarshalJSON() ([]byte, error) {
return []byte(strconv.FormatInt(h.Unix(), 10)), nil
}
func (h *CustomTime) UnmarshalJSON(bt []byte) error {
tt, err := strconv.ParseInt(string(data), 10, 64)
if err != nil {
return err
}
h.Time = Time(time.Unix(tt, 0))
return nil
}
メリット
簡単、使いまわしできる
デメリット
datastoreの保存時に、time.Timeではなく構造体として保存されちゃって腹立つ
※ 今 pure Datastoreパッケージを利用してないんで、そっちだとうまくいったりする…?調べてないけど
解決方法3: Named Typeで対応 (NG)
解決方法2. ではstructとして扱いましたが、Named Typeでも同じようなことはできます。
ただ、Datastoreの保存時にうまくいかないみたい。
(そもそも保存がされない。。pure Datastoreパッケージを利用してないんで、そっちだとうまくいったりするのかも?)
type Hoge struct {
ID int64 `json:"id" datastore:"-"`
Name string `json:"name" datastore:"name"`
CreatedAt CustomTime `json:"createdAt" datastore:"createdAt"`
}
type CustomTime time.Time
func (h *CustomTime) MarshalJSON() ([]byte, error) {
ht := time.Time(*h)
return []byte(strconv.FormatInt(ht.Unix(), 10)), nil
}
func (h *CustomTime) UnmarshalJSON(bt []byte) error {
tt, err := strconv.ParseInt(string(data), 10, 64)
if err != nil {
return err
}
t := time.Unix(tt, 0)
*h = CustomTime(t)
return nil
}
メリット
簡単、使いまわしできる
デメリット
datastoreの保存時に、そもそも保存されなかった
※ 今 pure Datastoreパッケージを利用してないんで、そっちだとうまくいったりする…?調べてないけど
解決方法4: Named Typeで対応し、mercari/boomを利用してDataStoreに保存
- がうまくいかなかったんで、 mercari/boom に実装されている機能を利用して保存をします。
mercari/boom については以下に作成者のQiita記事があります。
type Hoge struct {
ID int64 `json:"id" datastore:"-"`
Name string `json:"name" datastore:"name"`
CreatedAt *CustomTime `json:"createdAt" datastore:"createdAt"` // mercari/datastore/#PropertyTranslator インタフェースを実装するためポインタに変更
}
type CustomTime time.Time
func (h *CustomTime) MarshalJSON() ([]byte, error) {
ht := time.Time(*h)
return []byte(strconv.FormatInt(ht.Unix(), 10)), nil
}
func (h *CustomTime) UnmarshalJSON(bt []byte) error {
tt, err := strconv.ParseInt(string(data), 10, 64)
if err != nil {
return err
}
t := time.Unix(tt, 0)
*h = CustomTime(t)
return nil
}
// 以下はPropertyTranslator インタフェースの実装
func (t CustomTime) ToPropertyValue(ctx context.Context) (interface{}, error) {
tm := time.Time(t)
return tm, nil
}
func (t *CustomTime) FromPropertyValue(ctx context.Context, p datastore.Property) (dst interface{}, err error) {
tm, ok := p.Value.(time.Time)
if ok {
tme := CustomTime(tm)
return &tme, nil
}
return nil, nil
}
メリット
簡単、使いまわしできる
デメリット
実装のパッケージ依存度が増すから別のDBを使うとなった時ちょっと面倒そう
まとめ
上記でも書いてたけど、2, 3 の解決方法のdatastoreの保存時の課題は、pure datastore パッケージを利用したら解決するのかもしれないけど、(面倒だから)確かめてません。
今 mericari/boom を使わせてもらってるから4. でやってるけど、もっといい方法あったりしないかなぁって思ったり思わなかったり。