Go言語でMySQLを操作してみる

会社で主に使用している言語はC#とJavaなんですが、Javaからそろそろ他の言語に移りたいなーと言う話を先日上司としていました。

というのも、Javaも堅実な言語なので基幹系のシステムの構築には悪くない言語なんですが、いかんせん記述量も多いし学習コストが高いという問題もあったので、乗り換え先の言語を模索している状態です。

その中で、まずはGoogleの開発したGo言語はどうだろうかという意味合いでGo言語に触れてみました。

Go言語は記述量も少なく実装できますし、コンパイル言語で動作も高速、型推論付きの静的型付けと、チームで同じく使用しているC#と特徴が似ており、C#自体の評判は社内では良いので、候補に上がった次第です。

(半分Javaに飽きたという理由もあったりする……)

正式に採用するかどうかは全然分かりませんが、実際に触れてみるのも大事だと思い入門してみました。

今回は業務上必要不可欠なMySQLの接続と各操作を一通りやってみます、自分の為のメモみたいなものなので不足している情報もありますのでご注意ください。

検証環境

今回の検証で使用した環境になります。

  • Go言語 1.18.4
  • Visual Studio Code 1.69.2(Go言語拡張機能インストール済み)
  • MySQL 8.0.27(別サーバで稼働、ローカルネットワーク内)

また、今回利用するテストテーブルのDDLは以下になります。

CREATE TABLE test.test_user
(
 user_id SMALLINT(4) UNSIGNED ZEROFILL NOT NULL DEFAULT 0000,
 user_name VARCHAR(50) NOT NULL DEFAULT '',
 create_datetime DATETIME NOT NULL,
 update_datetime TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP,
 PRIMARY KEY (user_id)
) ENGINE=INNODB CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

また、初期データとして以下のDMLを流しておきました、内容は適当です。

INSERT INTO test.test_user
VALUES
(0001,'佐藤義之',NOW(),NOW()),
(0002,'鈴木花子',NOW(),NOW()),
(0003,'高橋有紀美',NOW(),NOW())

DB接続

まずは初めにDBへの接続を行います、Go言語でMySQLへ接続する場合には標準のライブラリであるdatabase/sqlを使用します。

ただ、これだけだとドライバが無いみたいなので、別途GitHubからMySQLに対応したドライバをインストールします。

ドライバのインストールはコマンドライン上で行います、今回だと以下のコマンドでインストールしました。

go install github.com/go-sql-driver/mysql@latest

これで下準備は完了です、まずはDB操作を行う為に接続を作成します。

接続までの流れは以下の様に実装することで出来ました。

package main

import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	fmt.Println("MySQL Connection Test")

	dbconf := "(ユーザー名):(パスワード)@tcp((ホスト名):(ポート番号))/(接続データベース名)?charset=(接続文字コード)"

	db, err := sql.Open("mysql", dbconf)

	defer db.Close()

	if err != nil {
		fmt.Println(err.Error())
	}

	err = db.Ping()

	if err != nil {
		fmt.Println("データベース接続失敗")
	} else {
		fmt.Println("データベース接続成功")
	}
}

内容ですが、まずdatabase/sqlgithub.com/go-sql-driver/mysqlをインポートします、後者の方は直接利用しないでinit関数だけを実行させるので、頭にアンダースコアを入れるのがルールらしいですね。

dbconfの接続文字列には各日本語で記載した部分を接続するMySQLの設定に合わせて記載します、前後の括弧は不要なので注意してください。

@tcp以降の括弧だけ必要です、なので例で言うとuser_name:password@tcp(127.0.0.1:3306)/database_name/?charset=utf8mb4な感じになります。

JavaやC#のコネクタと比べると、結構違和感がありますね。

sql.Open()で接続を開始します、これ一つで接続がない場合は新規で取得、既に接続されている場合はそれを利用するみたいなので、あれこれ管理する必要がなくて便利ですね。

defer sql.Close()で接続の終了を遅延実行しておきます、この構文も中々慣れないものです。

実際に接続できたかどうかは、取得した接続を利用しdb.Ping()で確認します、これでエラーが返って来なければ接続は成功しています。

SELECT

恐らく一番使われるであろうSELECTを投げてみます、先程のコードに一部追加してみます。

package main

import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql"
)

type User struct{
	id string `db:"user_id"`
	name string `db:"user_name"`
}

func main() {
	fmt.Println("MySQL Connection Test")

	dbconf := "(ユーザー名):(パスワード)@tcp((ホスト名):(ポート番号))/(接続データベース名)?charset=(接続文字コード)"

	db, err := sql.Open("mysql", dbconf)

	defer db.Close()

	if err != nil {
		fmt.Println(err.Error())
	}

	err = db.Ping()

	if err != nil {
		fmt.Println("データベース接続失敗")
	} else {
		fmt.Println("データベース接続成功")
	}

	rows, err := db.Query("SELECT user_id,user_name FROM test.test_user")

	for rows.Next() {
		var user User
		err = rows.Scan(&user.id, &user.name)
		if err != nil {
			panic(err.Error())
		}
		fmt.Println(user.id, user.name)
	}
}

追加したコードは以下の2つになります。

type User struct{
	id string `db:"user_id"`
	name string `db:"user_name"`
}

取得した結果を保持しておくための構造体です、型の後ろにテーブル上でのカラム名を指定しておくことで、変数名がカラム名と違っていてもマッピングしてくれます。

	rows, err := db.Query("SELECT user_id,user_name FROM test.test_user")

	for rows.Next() {
		var user User
		err = rows.Scan(&user.id, &user.name)
		if err != nil {
			panic(err.Error())
		}
		fmt.Println(user.id, user.name)
	}

db.Query("SELECT user_id,user_name FROM test.test_user")で実際にクエリを投げていますが、SELECTの場合にはQuery関数を使用しますがUPDATE等の場合には違う関数となります。

また、今回は複数の結果を取得するのでこの書き方ですが、単一行を取得する場合はQueryRow()関数になります。

結果はrowsに入っているので、for文で要素毎に回し、先程宣言したUser構造体にrows.Scan(&user.id, &user.name)で取得した結果の内容をuser変数に入れます。

この時、SELECTで指定したカラムの順番でScanをしているみたいなので、引数を逆にすると中身も逆になってしまいました。

UPDATE/INSERT/DELETE

次に多く利用するであろう更新系クエリを投げてみます。

package main

import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	fmt.Println("MySQL Connection Test")

	dbconf := "(ユーザー名):(パスワード)@tcp((ホスト名):(ポート番号))/(接続データベース名)?charset=(接続文字コード)"

	db, err := sql.Open("mysql", dbconf)

	defer db.Close()

	if err != nil {
		fmt.Println(err.Error())
	}

	err = db.Ping()

	if err != nil {
		fmt.Println("データベース接続失敗")
	} else {
		fmt.Println("データベース接続成功")
	}

	res, err := db.Exec("UPDATE test.test_user SET user_name = '黒崎芳行' WHERE user_id = 0001")

  if err != nil {
    panic(err.Error())
  }

  rows, err := res.RowsAffected()

  fmt.println(rows)
}

先程も書いた通り、更新系のクエリではExec関数を使用します。

また、UPDATEやINSERT、DELETEでは戻り値で影響のあった行数を返してくれます、この辺の仕様は割と他の言語系のORMでもよく見ますね。

res, err := db.Exec("INSERT INTO test.test_user VALUES (?,?,NOW(),NOW())", 0004, "池田純子")

INSERTのパターンです、UPDATEと変わらないですが、今回はパラメータの渡し方を変えてみました。

MySQLの場合は?を書くことで、その後に続くパラメータを自動的に割り当ててくれます。

res, err := db.Exec("DELETE FROM test.test_user WHERE user_id = ?", 0004)

DELETEのパターンも変わりはありません、こちらも結構簡単に実装ができますね。

Go言語でDB操作は簡単だった

今回軽く触ってみた感じでは、Go言語でMySQLの操作は簡単に出来てしまう、という感想になります。

本番使用ではもっと厳密なコネクション管理だったり、GORM等のORMマッパーを使用したりとこの記事のまま使えはしないでしょうが、少なくとも他の言語よりは手続きが少なくて楽ちんでしたね。

ただ、まだGo言語に慣れていないせいか関係ない場所でエラーが多発していた+解決方法がいまいちわからない状態に陥ったりして、これらの検証をするだけでも数時間かかってしまいました……

ちゃんとそもそもの言語の理解をしてから検証するべきですね、次回からはちゃんと基礎を固めます。

コメントを残す

メールアドレスが公開されることはありません。