logo

アルパカログ

BigQuery RECORD型をGoのネストした構造体(struct)にマッピングする方法

BigQuery はクエリが非常に高速なため、API クライアントを用いたアプリケーションからデータをクエリするケースも多いかと思います。

また、最近は Go を使ったアプリケーション開発の機会も増えてきました。

そこでこの記事では、BigQuery からデータを取得する際に RECORD 型を Go のネストした構造体(struct) にマッピングする方法を説明します。

BigQuery の RECORD 型と SELECT クエリ

例として挙げる BigQuery のテーブル request_logs は、以下の表のようになっているとします。

カラム名データ型REQUIRED or NULLABLE
timestampTIMESTAMPREQUIRED
request_infoRECORDNULLABLE
request_info.pathSTRINGNULLABLE
request_info.methodSTRINGNULLABLE

request_info が RECORD 型になっており、STRING 型の pathmethod を持っています。

このテーブルに対して下記のような SELECT クエリを発行したいとします。

SELECT
  `timestamp`,
  `request_info`
FROM
  `request_logs`
WHERE
  DATE(`timestamp`) = "2022-06-20"
ORDER BY `timestamp`
LIMIT 100;

Go の BigQuery クライアントから上記のクエリを発行し、 request_info をネストした構造体にマッピングして取得することを考えます。

Struct Tags を用いた構造体の定義とコード例

BigQuery の RECORD 型を Go の構造体にマッピングして取得するには、 bigquery 構造体タグ(Struct Tags) を使って構造体を定義します。

例えば下記のようになるでしょう。

type RequestLog struct {
	Timestamp   time.Time    `bigquery:"timestamp"`
	RequestInfo *RequestInfo `bigquery:"request_info,nullable"`
}

type RequestInfo struct {
	Path   string `bigquery:"path,nullable"`
	Method string `bigquery:"method,nullable"`
}

bigquery 構造体タグを使って、フィールドごとに対応するカラム名を記述します。上記の例では Timestamp フィールドに対応するカラム名は timestamp です。

また、NULL許容の場合は nullable も記述します。

RECORD 型に対応するフィールドは、構造体か構造体のポインタにします。REPEATED の場合はスライスとして定義します。

A repeated field corresponds to a slice or array of the element type. A STRUCT type (RECORD or nested schema) corresponds to a nested struct or struct pointer.

以上を踏まえて、最後に実際のコード例を示します。

import (
        "context"
        "fmt"
        "io"

        "cloud.google.com/go/bigquery"
        "google.golang.org/api/iterator"
)

type RequestLog struct {
	Timestamp   time.Time    `bigquery:"timestamp"`
	RequestInfo *RequestInfo `bigquery:"request_info,nullable"`
}

type RequestInfo struct {
	Path   string `bigquery:"path,nullable"`
	Method string `bigquery:"method,nullable"`
}

func queryExample(ctx context.Contest, projectID string) error {
        // projectID := "my-project-id"
        client, err := bigquery.NewClient(ctx, projectID)
        if err != nil {
                return fmt.Errorf("bigquery.NewClient: %v", err)
        }
        defer client.Close()

        q := client.Query(
                "SELECT " +
                    "`timestamp`, " +
                    "`request_info` " +
                    "FROM `<PROJECT_ID>.<DATASET>.request_logs` " +
                    "WHERE " +
                    "DATE(`timestamp`) = '2022-06-20' "
                    "ORDER BY `timestamp` " +
                    "LIMIT 100")

        job, err := q.Run(ctx)
        if err != nil {
                return err
        }

        status, err := job.Wait(ctx)
        if err != nil {
                return err
        }
        if err := status.Err(); err != nil {
                return err
        }

        var requestLogs []*RequestLog

        it, err := job.Read(ctx)
        for {
                var requestLog RequestLog
                err := it.Next(&requestLog)
                if err == iterator.Done {
                        break
                }
                if err != nil {
                        return err
                }

                requestLogs = append(requestLogs, &requestLog)
        }

        fmt.Printf("%#v", requestLogs)
        return nil
}

requestLog.RequestInfo.Path のようにしてネストされたデータにアクセスすることができます。

また、timestamp が日本時間(JST) で欲しい場合は次のようにします。

loc, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
        return err
}

for {
        //
        // 省略
        //

        requestLog.Timestamp = requestLog.Timestamp.In(loc) // 追加

        requestLogs = append(requestLogs, &requestLog)
}

以上です。

この記事では、BigQuery からデータを取得する際に RECORD 型を Go のネストした構造体(struct) にマッピングする方法を説明しました。