はじめに
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
は、Time
が Valid
かどうかも保持するため、JSONにシリアライズすると、Time
および Valid
のフィールドを含むオブジェクトになる。そのため、Dart側でこのオブジェクトを単に文字列として扱おうとすると下記エラーが発生する。
Golangのサーバーサイドから送信されるJSONレスポンス(sql.NullTime
フィールド)
{
"date": {
"Time": "2024-05-06T14:00:00Z",
"Valid": true
}
}
flutter: type '_Map<String, dynamic>' is not a subtype of type 'String' in type cast
Dart/Flutter側でこのデータを適切に処理するためには、マップから直接文字列にキャストするのではなく、マップの特定のキーを参照して値を取り出す必要がある。
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型のカラムに日付データを格納する。これにより、時間情報が正しくデータベースに保存され、日付や時刻に関するクエリ操作が可能になる。
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 (
"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)
}
コメント