Imagine you have a struct with some fields which are always there,
but you also have some fields which are dynamically populated with
different [key, value] pairs, which cannot be described with static
types upfront.
Example Record
1
2
3
4
5
6
7
|
type Order struct {
ID string `json:"id"`
ItemID string `json:"item_id"`
Category string `json:"category"`
Timestamp int64 `json:"timestamp"`
Attributes map[string]interface{} `json:"attributes"`
}
|
The attributes field is map and may contain an arbitrary number of values for each record.
This is the field that I want to store in JSON column. The other fields are stored in
statically defined columns with their respective types.
First attempt
BigQuery infer schema does not work with maps. So doing something like this
won’t work.
1
|
schema, err := bigquery.InferSchema(Order{})
|
Even if you make the Attributes field type json.RawMessage it won’t work again.
Second attempt
Manually create the schema and try again.
1
2
3
4
5
6
7
8
9
10
11
|
schema := bigquery.Schema{
{Name: "id", Type: bigquery.StringFieldType},
{Name: "item_id", Type: bigquery.StringFieldType},
{Name: "category", Type: bigquery.StringFieldType},
{Name: "timestamp", Type: bigquery.IntegerFieldType},
{Name: "attributes", Type: bigquery.JSONFieldType},
}
// ... create the table
err := table.Inserter().Put(ctx, order)
|
This won’t work (with the Golang client) as BigQuery won’t convert the map
to JSON. If we change the field type to json.RawMessage it won’t work again.
Third attempt is OK
We make the Order type implement the bigquery.ValueSaver
interface and serialize the attributes field as JSON string.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
type OrderValueSaver struct {
order *Order
}
// Save implements the bigquery.ValueSaver interface
// to serialize the "attributes" field as JSON string.
func (s *OrderValueSaver) Save() (row map[string]bigquery.Value, insertID string, err error) {
attr, err := json.Marshal(s.order.Attributes)
if err != nil {
return nil, "", err
}
row = map[string]bigquery.Value{
"id": s.order.ID,
"item_id": s.order.ItemID,
"category": s.order.Category,
"timestamp": s.order.Timestamp,
"attributes": string(attr),
}
return row, s.order.ID, nil
}
func (c *Client) SaveOrder(ctx context.Context, order *Order) error {
schema := bigquery.Schema{
{Name: "id", Type: bigquery.StringFieldType},
{Name: "item_id", Type: bigquery.StringFieldType},
{Name: "category", Type: bigquery.StringFieldType},
{Name: "timestamp", Type: bigquery.IntegerFieldType},
{Name: "attributes", Type: bigquery.JSONFieldType},
}
// ... create/get table, ideally do it in the constructor of the client once
saver := &OrderValueSaver{order: order}
return table.Inserter().Put(ctx, saver)
}
|