Go開発者への道 第6回 SQLBoilerを使ってDBに接続しよう

DB

Go初心者向けに始めた記事の第6回になります。
今回はDBと接続させて、第5回で作成したAPIを完成させていきます。
GoのORMはGORMが有名かなと思うのですが、自分の会社ではSQLBoilerを採用していますので、この記事ではSQLBoilerを使用してDBと接続します。

SQLBoilerについて

SQLBoilerはGoのORMライブラリの1つで、採用される利用として、コマンドでDBからDBの型、CRUDのコードを自動生成できるところです。

GitHub - volatiletech/sqlboiler: Generate a Go ORM tailored to your database schema.
Generate a Go ORM tailored to your database schema. - volatiletech/sqlboiler

実装

インストール

# 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
}

最終的には以下のようなリポジトリになります。

GitHub - SND1231/go-column at go-column-6
Go コラムのサンプルコードをこちらに残す. Contribute to SND1231/go-column development by creating an account on GitHub.

実行結果

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について書いた記事がございますので、もし興味がありましたら、おすすめ記事も見てもらえたらと思います。

【おすすめ記事のリンク】

コメント

タイトルとURLをコピーしました

Fatal error: Uncaught JSMin_UnterminatedStringException: JSMin: Unterminated String at byte 703: "GoでORMを使用する際に、色々あるので悩むと思います。 in /home/c5287456/public_html/engineer-want-to-grow.com/wp-content/plugins/autoptimize/classes/external/php/jsmin.php:214 Stack trace: #0 /home/c5287456/public_html/engineer-want-to-grow.com/wp-content/plugins/autoptimize/classes/external/php/jsmin.php(152): JSMin->action(1) #1 /home/c5287456/public_html/engineer-want-to-grow.com/wp-content/plugins/autoptimize/classes/external/php/jsmin.php(86): JSMin->min() #2 /home/c5287456/public_html/engineer-want-to-grow.com/wp-content/plugins/autoptimize/classes/external/php/ao-minify-html.php(257): JSMin::minify('{"@context":"ht...') #3 [internal function]: AO_Minify_HTML->_removeScriptCB(Array) #4 /home/c5287456/public_html/engineer-want-to-grow.com/wp-content/plugins/autoptimize/classes/external/php/ao-minify-html.php(108): preg_replace_callback('/(\\s*)(<script\\...', Array, '<!doctype html>...') #5 /home/c52 in /home/c5287456/public_html/engineer-want-to-grow.com/wp-content/plugins/autoptimize/classes/external/php/jsmin.php on line 214