🦔

【Go】nullを許容するDBカラムに対するdate型の値の扱い

2024/05/06に公開

はじめに

DartからGolangのAPIにPOSTする場合、nullを許容するDBカラムに対するdate型の値をどのように扱うかで、ハマったので備忘録的にまとめ。

NULLを許可しない場合は、単純にtime.Time型として定義

前提としてデータベースのカラムがNULLを許容しない場合、Golangの構造体のフィールドを単純に time.Time 型として定義し、Dart側からは DateTime オブジェクトを toString() メソッド(または toIso8601String() を使用してISO 8601形式で)で文字列化して送信するだけでよい。

GolangがISO 8601形式の文字列を time.Time オブジェクトに解析できるのは、Golangの time パッケージが内部的にこのフォーマットを理解し、正しくパース(解析)する機能を提供しているから。特に time.Parse 関数を用いると、指定された日付・時刻形式の文字列を time.Time 型の値に変換することができる。

HTTP通信を行う場合、データはテキスト形式でなければならず、DateTime オブジェクトをそのまま送ることはできない。

Dartの DateTime オブジェクトのデフォルトの toString() メソッドを使用してデータを送ると、得られる文字列は通常 DateTime オブジェクトが持つ完全な日付と時刻の情報を含むが、この形式はISO 8601形式と異なることがある。例えば、toString() はタイムゾーンの情報を含む場合と含まない場合があり、そのフォーマットが一貫していないことがある。そのため、この方法でデータを送信すると、Golang側でのデータ受信・解析時に問題が発生する可能性がある。 toIso8601String() を使用することが推奨される。

type RequestBody struct {
    Date time.Time `json:"date"` // NULLを許容しない場合
}
import 'package:http/http.dart' as http;

void postData(DateTime date) async {
  final response = await http.post(
    Uri.parse('http://example.com/api/post'),
    headers: {
      'Content-Type': 'application/json'
    },
    body: jsonEncode({
      'date': date.toIso8601String(), // DateTimeをISO 8601形式の文字列に変換
    }),
  );
  if (response.statusCode == 200) {
    print("Data posted successfully");
  } else {
    print("Failed to post data");
  }
}

さて、nullを許容するDBカラムに対するdate型の値を、golangで処理する場合の方法を3つ紹介。

ポインタでnullを許可する

特徴:

  • Dart側ではnull許容のDateTime?型を使用し、Golang側ではポインタ型の*time.Timeを使用する。
  • Dart側でnullの場合は、Golang側でポインタがnilになる。
  • ポインタがnilかどうかでnullを判定できるので、Dart側でnullを判定するための条件分岐が不要。空文字をDBに格納したくない場合には、空文字列をnull値としてDBに格納するか、空文字列を無視してDBに格納しないように設定するなど、別途処理が必要(から文字は有効なメモリアドレスを指しているとみなされるため)
import 'package:http/http.dart' as http;

DateTime? date; // null許容のDateTime型

// APIにPOSTする処理
void postData() async {
  final response = await http.post(
    Uri.parse('http://example.com/api/post'),
    body: {'date': date?.toIso8601String()}, // DateTime型をISO 8601形式の文字列に変換して送信
  );
}
package main

import (
    "fmt"
    "net/http"
    "time"
)

type RequestBody struct {
    Date *time.Time `json:"date,omitempty"` // ポインタ型でnullを許可
}

func handlePost(w http.ResponseWriter, r *http.Request) {
    var requestBody RequestBody
    // リクエストボディをパースしてRequestBody構造体に格納
    // ...

    if requestBody.Date != nil {
        fmt.Println("Received date:", requestBody.Date)
    } else {
        fmt.Println("Received null date")
    }
}

func main() {
    http.HandleFunc("/api/post", handlePost)
    http.ListenAndServe(":8080", nil)
}

sql.NullTimeを使う

Dartから送信する際に、nullの場合はnullで送信し、有効な日付がある場合は時間型の値を送信する。Golang側では、sql.NullTime型を使用して、データを受け取る。

特徴:

  • Golang側でsql.NullTime型を使用して、nullを許容する時間型を表現する
  • Golang側でValidフィールドを介してnullを判定する。
  • SQLパッケージをインポートする必要がある。
  • nullを判定するために、Validフィールドを介して条件分岐が必要。Validがfalseの場合にnullであることを示す。
import 'package:http/http.dart' as http;

DateTime? date; // null許容のDateTime型

// APIにPOSTする処理
void postData() async {
  final response = await http.post(
    Uri.parse('http://example.com/api/post'),
    body: {'date': date != null ? date.toIso8601String() : null}, // nullの場合はnullを送信
  );
}
package main

import (
    "database/sql"
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

type RequestBody struct {
    Date sql.NullTime `json:"date"` // sql.NullTime型を使用
}

func handlePost(w http.ResponseWriter, r *http.Request) {
    var requestBody RequestBody
    // リクエストボディをパースしてRequestBody構造体に格納
    // ...

    if requestBody.Date.Valid {
        fmt.Println("Received date:", requestBody.Date.Time)
    } else {
        fmt.Println("Received null date")
    }
}

func main() {
    http.HandleFunc("/api/post", handlePost)
    http.ListenAndServe(":8080", nil)
}

sql.NullTime は、JSONにシリアライズすると辞書型になる

sql.NullTime は、TimeValid かどうかも保持するため、JSONにシリアライズすると、Time および Valid のフィールドを含むオブジェクトになる。そのため、Dart側でこのオブジェクトを単に文字列として扱おうとすると下記エラーが発生する。

Golangのサーバーサイドから、、、

続きは、こちらで記載しています。
https://kazulog.fun/dev/go-null-db-date-handling/

Discussion