はじめに
この記事はディップ株式会社 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関数を定義しておきます。
コード全体
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
}
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);
テストデータです。
結果
{"id":"person0001","name":"山田太郎"}
{"id":"person0002"}
無事DBのNULL→JSONのundefinedが出来ました!
最後に
API GatewayとLambdaでWebAPIを実装するのが初めてだったため、色々と知見を得ることができました。また、標準ライブラリとは言えまだまだGoにも知らないことが沢山あることを実感し、勉強がんばろうと思いました。
1年目のひよっこエンジニアですが、2024年は色々なことに挑戦して学びの多い年にしたいです!