Goでデータベースを扱う
ここからは Go でプログラムを書いてデータベースを扱っていきます。task up
を実行してデータベースが立ち上がっていることを確認してください。 まずは VSCode で先ほどクローンしてきたリポジトリを開きましょう。画像のようなファイルが入っているはずです。 main.go を開いてください。
データベースに接続する
Goのプログラムを書く
サンプルのプログラムが書いてありますが、データベースと接続できるように書き換えます。 Go でデータベースに接続するためのライブラリは様々ありますが、今回は SQL 文を書く sqlx を使います。
- 参考
package main
import (
"database/sql"
"errors"
"log"
"os"
"time"
"github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
// #region city
type City struct {
ID int `json:"ID,omitempty" db:"ID"`
Name string `json:"name,omitempty" db:"Name"`
CountryCode string `json:"countryCode,omitempty" db:"CountryCode"`
District string `json:"district,omitempty" db:"District"`
Population int `json:"population,omitempty" db:"Population"`
}
// #endregion city
func main() {
jst, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
log.Fatal(err)
}
conf := mysql.Config{
User: os.Getenv("DB_USERNAME"),
Passwd: os.Getenv("DB_PASSWORD"),
Net: "tcp",
Addr: os.Getenv("DB_HOSTNAME") + ":" + os.Getenv("DB_PORT"),
DBName: os.Getenv("DB_DATABASE"),
ParseTime: true,
Collation: "utf8mb4_unicode_ci",
Loc: jst,
}
db, err := sqlx.Open("mysql", conf.FormatDSN())
if err != nil {
log.Fatal(err)
}
log.Println("connected")
// #region get
var city City
err = db.Get(&city, "SELECT * FROM city WHERE Name = ?", "Tokyo")
if errors.Is(err, sql.ErrNoRows) {
log.Printf("no such city Name = '%s'\n", "Tokyo")
return
}
if err != nil {
log.Fatalf("DB Error: %s\n", err)
}
// #endregion get
log.Printf("Tokyoの人口は%d人です\n", city.Population)
}
// #region
などのコメントは無視してください。
書き換えた後、 import の周りで赤字のエラーが出た場合は、ターミナルでgo mod tidy
を実行してください。 26 から 40 行目でデータベースに接続するための設定をして、42 行目のdb, err := sqlx.Open("mysql", conf.FormatDSN())
でデータベースに接続しています。32 行目などでos.Getenv()
という関数が出てきていますが、これは環境変数と呼ばれる、コンピューター側で設定してプログラムで使えるようにしている変数です。今は必要なデータベースのパスワードなどの環境変数を何も設定していないので、設定します。
詳しく知りたい人向け
dsn とは
42 行目にFormatDSN
という関数がありますが、DSN
は「Data Source Name」の頭文字をとったものです。プログラムがデータベースを指定するために使われます。今回のFormatDSN
という関数は、データベースのユーザー名、パスワード、使うデータベース、どこにデータベースのサーバーがあるのか、用いる標準時、文字種などの設定をconf
という変数から読み取って DSN を組み立てています。
環境変数を設定する
.env
というファイルを作り、以下の内容を書いてください。
export DB_USERNAME="root"
export DB_PASSWORD="password"
export DB_HOSTNAME="localhost"
export DB_PORT="3306"
export DB_DATABASE="world"
今回は手元で動いているデータベースを使うのでパスワードなどが知られても問題ありませんが、実際には環境変数など外部の人に知られたくない、GitHub などに上げたくないファイルもあります。そのような場合は、.gitignore
というファイルを使うことで特定のファイルやフォルダを Git 管理の対象外にできます。.gitignore
ファイルの最後に.env
を追記しましょう。
...
# Go workspace file
go.work
.tool-versions
.env
.gitignore
ファイルは便利ですが、既に Git の追跡対象になっているファイルを書いても追跡対象から外れないので注意しましょう。
https://docs.github.com/ja/get-started/getting-started-with-git/ignoring-files
最後に環境変数ファイル.env
を環境変数として読み込むために、ターミナルでsource .env
を実行してください。
WARNING
$ source .env
このコマンドによって読み込んだ環境変数は、コマンドを入力したターミナルを終了すると消えてしまいます。また、コマンドを入力したターミナル以外では環境変数として読み込まれません。新しくターミナルを開きなおした場合などは、もう一度実行してください。
実行する
$ go run main.go
出力はこのようになります。
connected
Tokyoの人口は7980230人です
main.go
を解説してきます。
type City struct {
ID int `json:"ID,omitempty" db:"ID"`
Name string `json:"name,omitempty" db:"Name"`
CountryCode string `json:"countryCode,omitempty" db:"CountryCode"`
District string `json:"district,omitempty" db:"District"`
Population int `json:"population,omitempty" db:"Population"`
}
このCity
構造体の横にあるバッククオートで囲まれたタグにdb
でデータベースのカラム名を指定します。これによってライブラリがデータベースから取得したレコードを構造体に上手くあてはめてくれます。
参考: Struct タグについて | text.Baldanders.info
var city City
err = db.Get(&city, "SELECT * FROM city WHERE Name = ?", "Tokyo")
if errors.Is(err, sql.ErrNoRows) {
log.Printf("no such city Name = '%s'\n", "Tokyo")
return
}
if err != nil {
log.Fatalf("DB Error: %s\n", err)
}
City
型のcity
という変数のポインタを sqlx ライブラリのGet
関数の第 1 引数に指定します。第 2 引数には SQL 文を書きます。Name = ?
としていますが、第 3 引数以降の値が順番に?
へと当てはめられて SQL 文が実行され、取得したレコードがcity
変数に代入されます。
基本問題
$ go run main.go {都市の名前}
と入力して、同様に人口を表示するようにしましょう。
ヒント:Go言語 - os.Argsでコマンドラインパラメータを受け取る - 覚えたら書く
答え
package main
import (
"database/sql"
"errors"
"log"
"os"
"time"
"github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
type City struct {
ID int `json:"ID,omitempty" db:"ID"`
Name string `json:"name,omitempty" db:"Name"`
CountryCode string `json:"countryCode,omitempty" db:"CountryCode"`
District string `json:"district,omitempty" db:"District"`
Population int `json:"population,omitempty" db:"Population"`
}
func main() {
jst, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
log.Fatal(err)
}
conf := mysql.Config{
User: os.Getenv("DB_USERNAME"),
Passwd: os.Getenv("DB_PASSWORD"),
Net: "tcp",
Addr: os.Getenv("DB_HOSTNAME") + ":" + os.Getenv("DB_PORT"),
DBName: os.Getenv("DB_DATABASE"),
ParseTime: true,
Collation: "utf8mb4_unicode_ci",
Loc: jst,
}
db, err := sqlx.Open("mysql", conf.FormatDSN())
if err != nil {
log.Fatal(err)
}
log.Println("connected")
cityName := os.Args[1] //[!code ++]
var city City
err = db.Get(&city, "SELECT * FROM city WHERE Name = ?", "Tokyo") //[!code --]
err = db.Get(&city, "SELECT * FROM city WHERE Name = ?", cityName) //[!code ++]
if errors.Is(err, sql.ErrNoRows) {
log.Printf("no such city Name = '%s'\n", "Tokyo") //[!code --]
log.Printf("no such city Name = '%s'\n", cityName) //[!code ++]
return
}
if err != nil {
log.Fatalf("DB Error: %s\n", err)
}
log.Printf("Tokyoの人口は%d人です\n", city.Population)
}
応用問題
基本問題 1 と同様に都市を入力したとき、その都市の人口がその国の人口の何%かを表示してみましょう。
ヒント: 1 回のクエリでも取得できますが、2 回に分けた方が楽に考えられます。
答え
package main
import (
"database/sql"
"errors"
"log"
"os"
"time"
"github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
type City struct {
ID int `json:"ID,omitempty" db:"ID"`
Name string `json:"name,omitempty" db:"Name"`
CountryCode string `json:"countryCode,omitempty" db:"CountryCode"`
District string `json:"district,omitempty" db:"District"`
Population int `json:"population,omitempty" db:"Population"`
}
func main() {
jst, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
log.Fatal(err)
}
conf := mysql.Config{
User: os.Getenv("DB_USERNAME"),
Passwd: os.Getenv("DB_PASSWORD"),
Net: "tcp",
Addr: os.Getenv("DB_HOSTNAME") + ":" + os.Getenv("DB_PORT"),
DBName: os.Getenv("DB_DATABASE"),
ParseTime: true,
Collation: "utf8mb4_unicode_ci",
Loc: jst,
}
db, err := sqlx.Open("mysql", conf.FormatDSN())
if err != nil {
log.Fatal(err)
}
log.Println("connected")
cityName := os.Args[1]
var city City
err = db.Get(&city, "SELECT * FROM city WHERE Name = ?", cityName)
if errors.Is(err, sql.ErrNoRows) {
log.Printf("no such city Name = '%s'\n", cityName)
return
}
if err != nil {
log.Fatalf("DB Error: %s\n", err)
}
log.Printf("%sの人口は%d人です\n", city.Name, city.Population)
var population int //[!code ++]
err = db.Get(&population, "SELECT Population FROM country WHERE Code = ?", city.CountryCode) //[!code ++]
if errors.Is(err, sql.ErrNoRows) { //[!code ++]
log.Printf("no such country Code = '%s'\n", city.CountryCode) //[!code ++]
return //[!code ++]
} //[!code ++]
if err != nil { //[!code ++]
log.Fatalf("DB Error: %s\n", err) //[!code ++]
} //[!code ++]
//[!code ++]
percent := (float64(city.Population) / float64(population)) * 100 //[!code ++]
//[!code ++]
log.Printf("これは%sの人口の%f%%です\n", city.CountryCode, percent) //[!code ++]
}
複数レコードを取得する
Get
関数の代わりにSelect
関数を使い、第 1 引数を配列のポインタに変えると、複数レコードを取得できます。main.go
のmain
関数を以下のように書き換えて実行してみましょう。
func main() {
jst, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
log.Fatal(err)
}
conf := mysql.Config{
User: os.Getenv("DB_USERNAME"),
Passwd: os.Getenv("DB_PASSWORD"),
Net: "tcp",
Addr: os.Getenv("DB_HOSTNAME") + ":" + os.Getenv("DB_PORT"),
DBName: os.Getenv("DB_DATABASE"),
ParseTime: true,
Collation: "utf8mb4_unicode_ci",
Loc: jst,
}
db, err := sqlx.Open("mysql", conf.FormatDSN())
if err != nil {
log.Fatal(err)
}
log.Println("connected")
var cities []City
err = db.Select(&cities, "SELECT * FROM city WHERE CountryCode = 'JPN'") //?を使わない場合、第3引数以降は不要
if err != nil {
log.Fatal(err)
}
log.Println("日本の都市一覧")
for _, city := range cities {
log.Printf("都市名: %s, 人口: %d\n", city.Name, city.Population)
}
}
以下のように日本の都市一覧を取得できます。
connected
日本の都市一覧
都市名: Tokyo, 人口: 7980230
都市名: Jokohama [Yokohama], 人口: 3339594
都市名: Osaka, 人口: 2595674
都市名: Nagoya, 人口: 2154376
都市名: Sapporo, 人口: 1790886
都市名: Kioto, 人口: 1461974
...省略
日本の都市一覧を取得出来たら、スクリーンショットを講習会用チャンネルに投稿しましょう。
レコードを書き換える
INSERT
やUPDATE
、DELETE
を実行したい場合は、Exec
関数を使うことができます。第 1 引数に SQL 文を渡し、第 2 引数以降は?
に当てはめたい値を入れます。
result, err := db.Exec("INSERT INTO city (Name, CountryCode, District, Population) VALUES (?,?,?,?)", name, countryCode, district, population)
例えばINSERT
ならば、このように使うことができます。result
には操作によって変更があったレコード数などの情報が入っています。
詳しく知りたい人向け
なぜ「?
」を使うのか
sqlx で変数を含む SQL を使いたいときは「?
」を使わなくてはいけません。これはセキュリティ上の問題です。例として、国のコードからその国の都市の情報一覧を取得することを考えましょう。fmt
ライブラリのSprintf
関数を使うとこのように処理を書くことができます。
err = db.Select(&city, fmt.Sprintf("SELECT * FROM city WHERE CountryCode = '%s'", code))
code
に入っている値がただの国名コードなら問題はないのですが、JPN' OR 'A' = 'A
という値が入っていたらどうなるでしょうか。データベースで実行されるとき、SQL 文は下のようになります。
SELECT * FROM city WHERE CountryCode = 'JPN' OR 'A' = 'A'
OR
でつなげた条件文のうち、「'A' = 'A'
」は常に成り立つので、WHERE
句の条件は常に真です。よって、この SQL を実行すると、作成者が意図しない方法で全ての都市が取得できてしまいます。このような攻撃は「SQL インジェクション」と呼ばれます。
sqlx ではこれを防ぐために?
を使うことができ、SQL 文が意図しない動きをしないようになっています。