15
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ディップAdvent Calendar 2023

Day 20

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

Last updated at Posted at 2023-12-19

はじめに

この記事はディップ株式会社 Advent Calendar 2023の20日目の投稿です!

はじめまして。2023年10月にディップ株式会社に新卒で入社した@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に対応

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になります。

type NullString struct {
	String string
	Valid  bool // Valid is true if String is not NULL
}

JSONのundefinedに対応

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

レスポンスのJSONでキーごとなし(undefined)を実現するには、レスポンス用の構造体で、構造体タグにomitemptyを追加すれば対応できます。

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)の状態になります。

func parseNullString(s sql.NullString) *string {
	if s.Valid {
		return &s.String
	}
	return nil
}

レスポンス用の構造体にそのままNullStringは使えないので、Validを見てfalseであればnilを返すparseNullString関数を定義しておきます。

コード全体

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
}
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'
;
`

挙動の確認

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許容です。

USE ADVENTDB;

TRUNCATE TABLE `person`;
INSERT INTO `person` (`id`, `name`)
VALUES ('person0001', '山田太郎'),
       ('person0002', NULL);

テストデータです。

結果

?person_id=person0001
{"id":"person0001","name":"山田太郎"}
?person_id=person0002
{"id":"person0002"}

無事DBのNULL→JSONのundefinedが出来ました!

最後に

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

15
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?