Go言語のGinフレームワークと、同じくGo言語のデータベース処理用ライブラリsqlxを連携させた実装例を今回お見せしたいと思います。
「gormネタは多いけど、sqlxネタは少ないな」と思ってましたので。
sqlxの使い方にクローズアップさせてまとめました。
(2022/6/19) 続編作りました。
SelectとGetの違い、バルクインサート、トランザクションについてまとめました。
始めに、おことわり
- 「使えればいい」レベルでの内容ですので、セキュリティやコード品質は無視しております。
ご了承ください。 - クリーンアーキテクチャではなく、ベタベタな
Controller+Model
形式での設計であることをご容赦ください。 -
Docker
+docker-compose
を用いた環境ですが、今回は省略させていただきます。
例題のソースを共有しました
環境
- Oracle Virtual Box for Windows 6.1 (OS:Ubuntu Server 20.04LTS)
- Docker Ver.20.10.17
- docker-compose Ver.2.5.1
- Go Ver.1.18.3
- Gin Ver.1.8.1
- go-sql-driver/mysql Ver.1.6.0
- jmoiron/sqlx Ver.1.3.5
- mysql Ver.8.0.29
ディレクトリ構成
/gin_sqlx_lesson01
├── controller
├── docker
├── docker-compose.yml
├── main.go
├── model
│ ├── members.go //←今回ご紹介する例
│ └── ranks.go //←今回ご紹介する例
└── myDatabase
└── myDatabase.go //←今回ご紹介する例
1: DB接続
"MYSQL_USER"などの環境変数を用いて、DSN(Data Source Name)を定義します。
そして、sqlx.Open
というメソッドでデータベースに接続します。
myDatabase/myDatabase.go
package myDatabase
import (
"fmt"
"os"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
// DbInit() データベースに接続する。
func DbInit() *sqlx.DB {
//dsn形式:username:password@protocol(address)/dbname?param=value
dsn := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?parseTime=true&loc=Asia%%2fTokyo",
os.Getenv("MYSQL_USER"),
os.Getenv("MYSQL_PASSWORD"),
os.Getenv("MYSQL_HOST"),
os.Getenv("MYSQL_DATABASE"),
)
//MySQLに接続
//sqlx.Openの第1引数は固定値でDBの種類を入力(今回は「mysql」)、第2引数は固定値でDSNを入れる。
conn, err := sqlx.Open("mysql", dsn)
if err != nil {
panic(err)
}
//接続オブジェクトのポインタを返します。
return conn
}
2: モデル(テーブルデータをすべて取得する例)
model/ranks.go
package model
import (
"time"
"github.com/rikimaru-tokyo/gin_sqlx_lesson01/myDatabase"
)
//データの受け手となる構造体を宣言
//`db`タグは必須。
//タグの値はDBテーブルのカラム名に合わせること。
type RankResult struct {
ID int `db:"id"`
Name string `db:"name"`
CreatedAt *time.Time `db:"created_at"`
UpdatedAt *time.Time `db:"updated_at"`
}
// GetRanksAll ranksテーブルデータ全出力
func GetRanksAll() ([]RankResult, error) {
//データベース接続
db := myDatabase.DbInit()
//SQL文字列定義
sql := `SELECT * FROM ranks;`
//データの受け手となる構造体をポインタにしてSelect()の第1引数に渡す。
//Select()の第2引数はSQL文字列をそのまま渡す。
//第3引数以降は、プレースフォルダのパラメータなどを入れる。
var ranks []RankResult
err := db.Select(&ranks, sql)
if err != nil {
return nil, err
}
return ranks, nil
}
3: モデル(プレースフォルダ「?」の使用例)
安全にSQLを実行するために、プレースフォルダ「?」に変数を投入する例です。
ポイントはSelect()
メソッドの第3引数以降に、プレースフォルダ「?」がついた順番にあわせてパラメータ変数を入れておくことです。
model/members.go
package model
import (
"github.com/rikimaru-tokyo/gin_sqlx_lesson01/myDatabase"
)
//データの受け手となる構造体を宣言
type MembersResult struct {
MemberID int `db:"member_id"`
MemberName string `db:"member_name"`
Rank string `db:"rank_name"`
}
// GetMembersWithRanks テーブル結合かつプレースフォルダを使った結果を取得
func GetMembersWithRanks(memberId int, rankName string)([]MembersResult, error){
//データベース接続
db := myDatabase.DbInit()
//SQL文字列定義
sql := `SELECT
m.id as member_id, m.name as member_name, r.name as rank_name
FROM
members as m
INNER JOIN
ranks as r ON m.rank_id = r.id
WHERE
m.id = ? OR r.name = ?;
`
var member []MembersResult
// 第3引数以降は、プレースフォルダ「?」がついた順番にあわせてパラメータ変数を入れておく。
err := db.Select(&member, sql, memberId, rankName)
if err != nil {
return nil, err
}
return member, nil
}
4: モデル(プレースフォルダ「:変数名」の使用例)
プレースフォルダ「?」の数が多すぎる、区別がつきにくいという場合には「:変数名」を使った定義も可能です。
ポイントはSQL上の「:変数名」に合わせた、map[string]interface(連想配列)
をあらかじめ用意して、NamedExec
メソッドの第2引数に投入することです。
model/ranks.go
// InsertRank() プレースフォルダ「:変数名」として使いたい場合
// ranksテーブルに{name:"Platinum"}を新規登録します。
func InsertRank() (int, error) {
db := myDatabase.DbInit()
now := time.Now()
sql := `INSERT INTO ranks
(id, name, created_at, updated_at)
VALUES
(:id, :name, :cr, :up);`
// 「:変数名」という形式でプレースフォルダを使いたい場合、NamedExecというメソッドを使います。
// 第1引数はSQL文字列、第2引数は「map[string]interface(連想配列)」
// 連想配列インデックス名は、SQL文中の「:変数名」に合わせます。
// [参考資料: https://github.com/jmoiron/sqlx の READMEより]
rows, err := db.NamedExec(sql, map[string]interface{}{
"id":4, "name": "Platinum", "cr": now, "up": now,
})
if err != nil {
return 0, err
}
// INSERT件数カウント
r, err := rows.RowsAffected()
if err != nil {
return 0, err
}
return int(r), nil
}
まだまだ足りないと思いますが、今回は以上となります。