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

この記事は約12分で読めます。

はじめに

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() を使用することが推奨される。

Go
type RequestBody struct {
    Date time.Time `json:"date"` // NULLを許容しない場合
}
Dart
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に格納しないように設定するなど、別途処理が必要(から文字は有効なメモリアドレスを指しているとみなされるため)
Dart
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形式の文字列に変換して送信
  );
}
Go
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であることを示す。
Dart
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を送信
  );
}
Go
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のサーバーサイドから送信されるJSONレスポンス(sql.NullTime フィールド)

JSON
{
  "date": {
    "Time": "2024-05-06T14:00:00Z",
    "Valid": true
  }
}
ShellScript
flutter: type '_Map<String, dynamic>' is not a subtype of type 'String' in type cast

Dart/Flutter側でこのデータを適切に処理するためには、マップから直接文字列にキャストするのではなく、マップの特定のキーを参照して値を取り出す必要がある。

Dart
var response = await api.getUser();
var dateMap = response['date'] as Map<String, dynamic>;
if (dateMap['Valid'] == true) {
  var dateString = dateMap['Time'] as String;  // 正しい日付の文字列を取得
}

[]uint8 を使う

特徴:

  • バイナリデータや文字列データを表現するために使用される。
  • nullの場合でも、値が空文字列(または空のバイトスライス)として送信されるため、Golang側で空文字列とnullを区別するための条件分岐が必要。

バイトスライス→文字列→time.Timeオブジェクト

バイトスライスから文字列への変換:

  • requestBody.Date[]uint8 型のバイトスライスで、これを string(requestBody.Date) として文字列に変換する。これにより、元々Dartから送信された文字列データ(例えば “2024-05-01T12:00:00Z” のような形式)が再び文字列形式に戻される。

文字列の日付データを time.Time オブジェクトにパース:

  • 次に、time.Parse(time.RFC3339, string(requestBody.Date)) を用いて、文字列形式の日付データをGoの time.Time オブジェクトに変換する。time.RFC3339 は、日付と時刻の形式を定義しており、この形式に従った文字列を適切に time.Time オブジェクトに変換することができる。

データベースへの格納:

  • 最後に、この time.Time オブジェクトを使って、データベースのTIMESTAMP型のカラムに日付データを格納する。これにより、時間情報が正しくデータベースに保存され、日付や時刻に関するクエリ操作が可能になる。
Dart
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を送信
  );
}
Go
package main

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

type RequestBody struct {
    Date []uint8 `json:"date"` // []uint8型を使用
}

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

    if len(requestBody.Date) > 0 {
        date, err := time.Parse(time.RFC3339, string(requestBody.Date))
        if err != nil {
            fmt.Println("Error parsing date:", err)
            return
        }
        fmt.Println("Received date:", date)
    } else {
        fmt.Println("Received null date")
    }
}

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

コメント

タイトルとURLをコピーしました