Go初心者向けに始めた記事の第6回になります。
今回はDBと接続させて、第5回で作成したAPIを完成させていきます。
GoのORMはGORMが有名かなと思うのですが、自分の会社ではSQLBoilerを採用していますので、この記事ではSQLBoilerを使用してDBと接続します。
SQLBoilerについて
SQLBoilerはGoのORMライブラリの1つで、採用される利用として、コマンドでDBからDBの型、CRUDのコードを自動生成できるところです。
実装
インストール
# SQLBoilerのコマンドを実行するためにインストール
go install github.com/volatiletech/sqlboiler/v4@latest
go install github.com/volatiletech/sqlboiler/v4/drivers/sqlboiler-mysql@latest
# モジュールをインストール
go get github.com/volatiletech/sqlboiler/v4
go get github.com/volatiletech/null/v8
コード
mysqlにユーザーテーブルを作成して、APIからそのユーザーテーブルに接続してユーザーの作成、詳細取得するAPIを作成します。
ここには前回から修正した分だけを記載します。
Makefile
コードを自動生成するSQLBoilerコマンドを追加、「sqlboiler mysql」を実行することで、modelsというディレクトリが作成されその配下にDBの型やCRUDが定義されたファイルが作成される。
.PHONY: build
build:
go build .
.PHONY: sqlboiler
sqlboiler:
sqlboiler mysql
Dockerfile
sqlboilerコマンドを実行できるようにインストールするコマンドを追加
FROM golang:1.19-bullseye
WORKDIR /work
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
wget \
make \
unzip \
git \
clang-format \
vim \
&& apt-get clean
# cobraのCLIをインストール
RUN go install github.com/spf13/cobra-cli@latest
# sqlboilerコマンドをインストール
RUN go install github.com/volatiletech/sqlboiler/v4@latest
RUN go install github.com/volatiletech/sqlboiler/v4/drivers/sqlboiler-mysql@latest
RUN pwd
COPY src/go.mod src/go.sum ./
RUN go mod download && go mod verify
docker-compose.yml
mysqlのイメージを追加した
version: "3.9"
services:
go-env:
build:
context: .
volumes:
- ./src:/work
working_dir: /work
tty: true
mysql:
image: mysql:8.0
ports:
- 3306:3306
environment:
MYSQL_ROOT_PASSWORD: test
MYSQL_DATABASE: test
TZ: Asia/Tokyo
volumes:
- ./volumes/mysql/db:/var/lib/mysql
command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci
schema/init.sql
ユーザーテーブルの定義。mysqlを立ち上げた後に、このファイルを使ってテーブルを登録する。
CREATE TABLE `user` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`age` int(10) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
db/mysql.go
DBコネクションを作成している。
各handlerでGetDBconnectionを呼びDB接続する。
package db
import (
"database/sql"
"fmt"
"github.com/SND1231/go-column/setting"
)
func GetDBconnection(dbSetting setting.DB) (*sql.DB, error) {
var dataSource string = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true",
dbSetting.User,
dbSetting.Password,
dbSetting.Host,
dbSetting.Port,
dbSetting.Name,
)
dataSource = dataSource + "&loc=Asia%2FTokyo"
db, err := sql.Open(dbSetting.Type, dataSource)
return db, err
}
router/router.go
dbの接続情報をもらって、handlerに設定することで、各APIでDB接続するための情報を使えるようにしている。
package router
import (
"github.com/SND1231/go-column/handler"
"github.com/SND1231/go-column/setting"
"github.com/go-chi/chi"
)
func Get(dbSetting setting.DB) *chi.Mux {
r := chi.NewRouter()
// ハンドラーの初期化
userHandler := handler.NewUserHandler(dbSetting)
// httpルーティング
r.Route("/user", func(r chi.Router) {
r.Post("/add", userHandler.Add)
r.Get("/detail", userHandler.Get)
})
return r
}
setting/mysql.go
DBの設定情報をsetting/mysql.goに記載する。
package setting
type DB struct {
Type string
Host string
Port int
User string
Password string
Name string
}
cmd/root.go
DBの設定情報をルーターに渡して、DB接続できるように設定している。
/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"log"
"net/http"
"os"
"github.com/SND1231/go-column/router"
"github.com/SND1231/go-column/setting"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var dbSetting setting.DB
var rootCmd = &cobra.Command{
Use: "go-column",
Run: func(cmd *cobra.Command, args []string) {
// configの中身を出力
log.Printf("configの中身:{type: %s, host: %s, port: %d, user: %s, pass: %s, name: %s}",
dbSetting.Type, dbSetting.Host, dbSetting.Port, dbSetting.User, dbSetting.Password, dbSetting.Name)
// サーバーの設定
r := router.Get(dbSetting)
srv := &http.Server{
Addr: ":3020",
Handler: r,
}
// サーバーの起動
srv.ListenAndServe()
},
}
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
// 初期化処理
// configの設定
rootCmd.Flags().StringP("configName", "n", "default.toml", "config file name")
// Runを実行するたびに、initConfigを呼び出す。その後に、Runの処理が動き出す。
cobra.OnInitialize(initConfig)
}
func initConfig() {
configName, _ := rootCmd.Flags().GetString("configName")
viper.SetConfigFile(configName)
// 設定ファイルを読み込む
if err := viper.ReadInConfig(); err != nil {
log.Println(err)
os.Exit(1)
}
// 設定ファイルの内容を構造体に設定
if err := viper.Unmarshal(&dbSetting); err != nil {
log.Println(err)
os.Exit(1)
}
}
handler/user.go
DBコネクションを作成、トランザクションを開始して、コンテキストやトランザクションの情報をusecaseに渡して、usecaseでDBと接続するようにしている。
DBとの接続を開始したら、deferで処理の最後にCloseするようにする。Closeを忘れるとコネクションリークが発生する可能性がある。
package handler
import (
"context"
"encoding/json"
"log"
"net/http"
"github.com/SND1231/go-column/db"
"github.com/SND1231/go-column/setting"
"github.com/SND1231/go-column/usecase"
"github.com/mholt/binding"
)
type UserHandler struct {
dbSetting setting.DB
}
func NewUserHandler(dbSetting setting.DB) *UserHandler {
return &UserHandler{
dbSetting: dbSetting,
}
}
// 失敗時のレスポンスの設定
func setErrorResponse(w http.ResponseWriter, status int) {
// レスポンスのヘッダ設定
w.Header().Set("Content-Type", "application/json; charset=utf-8")
// 引数のステータス設定
w.WriteHeader(status)
}
// 成功時のレスポンスの設定
func setSuccessResponse(w http.ResponseWriter, res []byte) {
// レスポンスの内容をjsonに変換
w.Write(res)
}
// ユーザーの作成API
func (h *UserHandler) Add(w http.ResponseWriter, r *http.Request) {
var err error
var berr binding.Errors
var response usecase.AddUserOutput
var res []byte
// request -> AddUserInput型に変換
var request usecase.AddUserInput
berr = binding.Bind(r, &request)
if berr != nil {
log.Println(berr)
setErrorResponse(w, http.StatusInternalServerError)
return
}
ctx := context.Background()
// dbのコネクション設定
conn, err := db.GetDBconnection(h.dbSetting)
if err != nil {
log.Println(err)
return
}
// コネクションのクローズ
defer func() {
conn.Close()
}()
// トランザクションの開始
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
log.Println(err)
return
}
// トランザクションのコミット or ロールバック
defer func() {
if err != nil {
tx.Rollback()
return
}
tx.Commit()
}()
// usecaseの初期化
userUsecase := usecase.NewUserUsecase(ctx, tx)
// ユーザー作成実施
response, err = userUsecase.Add(request)
// レスポンスをjsonに変換
res, err = json.Marshal(response)
if err != nil {
log.Println(err)
setErrorResponse(w, http.StatusInternalServerError)
return
}
setSuccessResponse(w, res)
}
// ユーザーの詳細取得API
func (h *UserHandler) Get(w http.ResponseWriter, r *http.Request) {
var err error
var berr binding.Errors
var response usecase.GetUserOutput
var res []byte
// request -> GetUserInput型に変換
var request usecase.GetUserInput
berr = binding.Bind(r, &request)
if berr != nil {
log.Println(berr)
setErrorResponse(w, http.StatusInternalServerError)
return
}
ctx := context.Background()
// dbのコネクション設定
conn, err := db.GetDBconnection(h.dbSetting)
if err != nil {
log.Println(err)
return
}
// コネクションのクローズ
defer func() {
conn.Close()
}()
// トランザクションの開始
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
log.Println(err)
return
}
// トランザクションのコミット or ロールバック
defer func() {
if err != nil {
tx.Rollback()
return
}
tx.Commit()
}()
// usecaseの初期化
userUsecase := usecase.NewUserUsecase(ctx, tx)
response, err = userUsecase.Get(request)
// レスポンスをjsonに変換
res, err = json.Marshal(response)
if err != nil {
log.Println(err)
setErrorResponse(w, http.StatusInternalServerError)
return
}
setSuccessResponse(w, res)
}
usecase/user.go
DBからデータを取得したり、データを作成する処理を書いている。
package usecase
import (
"context"
"database/sql"
"net/http"
"github.com/SND1231/go-column/models"
"github.com/mholt/binding"
"github.com/volatiletech/sqlboiler/v4/boil"
_ "github.com/go-sql-driver/mysql"
)
type UserUsecase struct {
ctx context.Context
tx *sql.Tx
}
func NewUserUsecase(ctx context.Context, tx *sql.Tx) *UserUsecase {
return &UserUsecase{
ctx: ctx,
tx: tx,
}
}
// ユーザーの作成APIのリクエスト
type AddUserInput struct {
Name string
Age int
}
// リクエストのマッピング。ポインターレシーバーにすること
func (input *AddUserInput) FieldMap(r *http.Request) binding.FieldMap {
return binding.FieldMap{
&input.Name: "name",
&input.Age: "age",
}
}
// ユーザーの作成APIのレスポンス
type AddUserOutput struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
// ユーザーを作成
func (u UserUsecase) Add(input AddUserInput) (AddUserOutput, error) {
user := models.User{
Name: input.Name,
Age: input.Age,
}
err := user.Insert(u.ctx, u.tx, boil.Infer())
if err != nil {
return AddUserOutput{}, err
}
output := AddUserOutput{
ID: user.ID,
Name: user.Name,
Age: user.Age,
}
return output, nil
}
// ユーザーの詳細取得APIのリクエスト
type GetUserInput struct {
ID int
}
// リクエストのマッピング。ポインターレシーバーにすること
func (input *GetUserInput) FieldMap(r *http.Request) binding.FieldMap {
return binding.FieldMap{
&input.ID: "id",
}
}
// ユーザーの詳細取得APIのレスポンス
type GetUserOutput struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
// ユーザー情報の取得
func (u UserUsecase) Get(input GetUserInput) (GetUserOutput, error) {
user, err := models.FindUser(u.ctx, u.tx, input.ID)
if err != nil {
return GetUserOutput{}, nil
}
output := GetUserOutput{
ID: user.ID,
Name: user.Name,
Age: user.Age,
}
return output, nil
}
最終的には以下のようなリポジトリになります。
実行結果
init.sqlを実行してテーブルを作成してから、以下のAPIをそれぞれ叩いてみる。
# ユーザーの作成API
POST http://localhost:3020/user/add
content-type: application/json
{
"name": "test",
"age": 30
}
###
# ユーザー情報の取得API
GET http://localhost:3020/user/detail?id=7
ユーザーの作成APIの実行結果
ユーザー作成に成功して、作成されたユーザーの情報が返ってくる
HTTP/1.1 200 OK
Date: Mon, 05 Jun 2023 00:05:17 GMT
Content-Length: 31
Content-Type: text/plain; charset=utf-8
Connection: close
{
"id": 7,
"name": "test",
"age": 30
}
ユーザー情報の取得APIの実行結果
id=7のユーザーの情報が返ってくる
HTTP/1.1 200 OK
Date: Mon, 05 Jun 2023 00:07:12 GMT
Content-Length: 31
Content-Type: text/plain; charset=utf-8
Connection: close
{
"id": 7,
"name": "test",
"age": 30
}
まとめ
今回はSQLBoilerを使ってDBと接続する方法について説明をしてきました。
次回はテストコードについて解説をしていきたいと思います。
今回の記事以外にもGoについて書いた記事がございますので、もし興味がありましたら、おすすめ記事も見てもらえたらと思います。
【おすすめ記事のリンク】
コメント