【Go】DBのnullをJSONのundefinedで返す

# はじめに
この記事は[ディップ株式会社 Advent Calendar 2023](https://qiita.com/advent-calendar/2023/dip-dev)の20日目の投稿です！

はじめまして。2023年10月にディップ株式会社に新卒で入社した[@Taro000](https://qiita.com/Taro000)です！
社内向けアプリのバックエンド開発をしているチームに所属しています。

# この記事について
Go言語におけるJSONのundefinedの扱いと、DBのnullの扱いについて学びがあったので共有できればと思います。

### 作るもの
この記事では、クエリパラメータperson_idを使ってpersonテーブルから人物情報を取得できるWebAPIを作ってみます。
APIGatewayとLambdaを使い、Goで実装します。（Goの実装に焦点を当てます）
- golang:1.15
- mysql:8.0.23

### 条件
- DBにNULL許容のカラムが存在する
- レスポンスのJSONとして、値がNULLのカラムはキーごと無し（undefined）で返したい

# DBのnullに対応
```go
type Person struct {
    Id   string         `db:"id"`
	Name sql.NullString `db:"name"`
}
```
DBのNULL許容のカラムにはGoの標準ライブラリ```"database/sql"```のNullstringを使えば対応可能です。（NullInt16などもあります）
Nullstringは単純な構造体で、DBに値が有ればValidがtrue、NULLであればfalseになります。
```go
type NullString struct {
	String string
	Valid  bool // Valid is true if String is not NULL
}
```

https://pkg.go.dev/database/sql#NullString

# JSONのundefinedに対応
```go
type Response struct {
    Id      string  `json:"id"`
    Name    *string `json:"name,omitempty"`
}
```
レスポンスのJSONでキーごとなし（undefined）を実現するには、レスポンス用の構造体で、構造体タグに```omitempty```を追加すれば対応できます。

```go
dst, err := _model.GetRecord(personId)
// エラー処理（省略）

res := Response{
    Id: dst.Id,
	Name: parseNullString(dst.Name),
}
jsonBytes, _ := json.Marshal(res)
```
標準ライブラリ```"encoding/json"```のMarshalでJSONにへ関する際、構造体タグで```omitempty```を指定したフィールドがnilの場合は、そのフィールドの変換はスキップされて、JSONとしてはキーごと無し（undefined）の状態になります。

https://pkg.go.dev/encoding/json#Marshal

```go
func parseNullString(s sql.NullString) *string {
	if s.Valid {
		return &s.String
	}
	return nil
}
```
レスポンス用の構造体にそのままNullStringは使えないので、Validを見てfalseであればnilを返すparseNullString関数を定義しておきます。

# コード全体
```go:main.go
package main

import (
	"database/sql"
	"encoding/json"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

type Response struct {
    Id      string  `json:"id"`
    Name    *string `json:"name,omitempty"`
}

func main() {
    lambda.Start(handler)
}

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    // リクエストから人物IDを取得
    personId := request.MultiValueQueryStringParameters["person_id"][0]

    // DB接続
	db, err := ConnDB()
    // エラー返却（省略）

    _model := New(db)

    // レコード取得
    dst, err := _model.GetRecord(personId)
    // エラー返却（省略）

    // レスポンス返却
    res := Response{
        Id: dst.Id,
        Name: parseNullString(dst.Name),
    }
    jsonBytes, _ := json.Marshal(res)
    
    return events.APIGatewayProxyResponse{
		Body:       string(jsonBytes),
		StatusCode: 200,
	}, nil
}

func parseNullString(s sql.NullString) *string {
	if s.Valid {
		return &s.String
	}
	return nil
}
```
```go:model.go
package main

import (
	"database/sql"
	"fmt"
	"os"
	"strings"
	
	_ "github.com/go-sql-driver/mysql"
	"github.com/jmoiron/sqlx"
)

type Person struct {
	Id   string         `db:"id"`
	Name sql.NullString `db:"name"`
}

type Model struct {
	db *sqlx.DB
}

func New(db *sqlx.DB) *Model {
	return &Model{db: db}
}

func ConnDB() (*sqlx.DB, error) {
	dsn := fmt.Sprintf(
		"%s:%s@tcp(%s:%s)/%s",
		os.Getenv("DB_USER"),
		os.Getenv("DB_PASS"),
		os.Getenv("DB_HOST"),
		os.Getenv("DB_PORT"),
		os.Getenv("DB_SCHEMA"),
	)

	db, err := sqlx.Connect("mysql", dsn)
	if err != nil {
		return nil, err
	}

	return db, nil
}

func (this *Model) GetRecord(personId string) (*Person, error) {
	var dst []*Person
	query := strings.Replace(queryStmt, ":person_id", personId, -1)
	err := this.db.Select(&dst, query)
	if err != nil {
		return nil, err
	}
	if len(dst) == 0 {
		return nil, nil
	}

	return dst[0], nil
}

const queryStmt = `
SELECT
    id
    , name
FROM
    ADVENTDB.person
WHERE
    id = ':person_id'
;
`
```

# 挙動の確認
```sql
USE ADVENTDB;

DROP TABLE IF EXISTS `person`;
CREATE TABLE `person` (
  `id` char(10) COLLATE utf8mb4_general_ci NOT NULL,
  `name` varchar(30) COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='人物';
```
人物テーブルを用意しました。idは主キーなのでNOT NULL、nameはNULL許容です。

```sql
USE ADVENTDB;

TRUNCATE TABLE `person`;
INSERT INTO `person` (`id`, `name`)
VALUES ('person0001', '山田太郎'),
       ('person0002', NULL);
```
テストデータです。

### 結果
```json:?person_id=person0001
{"id":"person0001","name":"山田太郎"}
```

```json:?person_id=person0002
{"id":"person0002"}
```
無事DBのNULL→JSONのundefinedが出来ました！

# 最後に
API GatewayとLambdaでWebAPIを実装するのが初めてだったため、色々と知見を得ることができました。また、標準ライブラリとは言えまだまだGoにも知らないことが沢山あることを実感し、勉強がんばろうと思いました。
１年目のひよっこエンジニアですが、2024年は色々なことに挑戦して学びの多い年にしたいです！