0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Go言語】time.Timeをjsonでunixtimeを返しつつ、time.Timeの型でGCP/DataStoreに保存する

Posted at

概要

掲題の通りです。
ニッチな内容なので需要ないと思いますが、自分の備忘用としての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値を返したいというケースも多く、そういう場合ちょっと面倒ですよね。

解決策

いくつかありますが、それぞれメリット/デメリットがあったりします。

  1. MarchalJSON / UnmarshalJSON を各structにて実装
  2. 独自のTime構造体で対応
  3. Named Typeで対応 (NG)
  4. 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に保存

  1. がうまくいかなかったんで、 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. でやってるけど、もっといい方法あったりしないかなぁって思ったり思わなかったり。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?