【Go実践】GoでMySQLを使おう(2) – 基本的なテーブル操作と構造体へのマッピング

シェアする

こんにちは。Goによるデータベース操作、MySQL編の第2回です。

前回の記事では、MySQL用のドライバをインストールしデータベースへの接続を行いました。
【Go実践】GoでMySQLを使おう(1) – ドライバのインストールからデータベース接続まで

今回は、実際にMySQLデータベース上のテーブルに対して更新を行ったり、テーブルを検索してデータを取得する方法を解説します。
クエリの発行自体は、パッケージが提供する関数で簡単に行うことができますが、この記事ではより実用的なコード実装の手順を紹介します。

テーブル定義

テーブル操作を行うためのサンプルとして、ユーザ情報のマスタテーブルを作成します。
テーブル定義は下記の通りとします。

CREATE TABLE M_USER (
     ID INT AUTO_INCREMENT NOT NULL PRIMARY KEY
    ,ACCOUNT NVARCHAR(20) NOT NULL DEFAULT “”
    ,NAME NVARCHAR(20) NOT NULL DEFAULT “”
    ,PASSWORD NVARCHAR(64) NOT NULL DEFAULT “”
    ,CREATED DATETIME NOT NULL
);

今後、ログイン機能などを実装する際にも流用しますので、アカウント名とパスワードのカラムを設けています。
また、プライマリキーとなる”ID”はオートインクリメントにしています。

クエリパッケージの作成

SQLやテーブル定義にあわせた構造体などを主処理に直接記述すると、メンテナンスが大変です。
queryというディレクトリを作成し、そこにクエリ用のパッケージを作成することにしましょう。

この際、テーブルごとにファイルを作成すると管理しやすくなると思います。
今回は、先ほど作成したM_USERテーブルに対応するファイルとして、m_user.goを作成します。

ルートディレクトリ
  |― conf/
      |― db.json
      |― db.go
  |― query/
      |― m_user.go
  |― main.go

confディレクトリ内のdb.jsondb.go前回の記事で作成した内容になります。

クエリファイルのサンプル

それではm_user.goの内容を見ていきましょう。
このファイルには、テーブル定義にあわせた構造体と、登録・更新・削除・検索といったテーブル操作を行う関数を定義します。

今回は、更新系の処理としてINSERTを行う関数と、IDをキーにSELECTを行う関数を実装してみましょう。

コードのサンプルを掲載し、後から詳細な解説を行います。

package query // 独自のクエリパッケージ

import (
	"database/sql"

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

// マスタからSELECTしたデータをマッピングする構造体
type M_USER struct {
	Id      string `db:"ID"`       // ID
	Account string `db:"ACCOUNT"`  // アカウント名
	Name    string `db:"NAME"`     // ユーザ名称
	Passwd  string `db:"PASSWORD"` // パスワード
	Created string `db:"CREATED"`  // 登録日
}

// データ登録関数
func InsertUser(acc, name, pw string, db *sql.DB) (id int64, err error) {

	// プリペアードステートメント
	stmt, err := db.Prepare("INSERT INTO M_USER(ACCOUNT,NAME,PASSWORD,CREATED) VALUES(?,?,?,now())")
	if err != nil {
		return 0, err
	}
	defer stmt.Close()

	// クエリ実行
	result, err := stmt.Exec(acc, name, pw)
	if err != nil {
		return 0, err
	}

	// オートインクリメントのIDを取得
	insertedId, err := result.LastInsertId()
	if err != nil {
		return 0, err
	}

	// INSERTされたIDを返す
	return insertedId, nil
}

// 単一行データ取得関数
func SelectUserById(id int64, db *sql.DB) (userinfo M_USER, err error) {

	// 構造体M_USER型の変数userを宣言
	var user M_USER

	// プリペアードステートメント
	stmt, err := db.Prepare("SELECT ID,ACCOUNT,NAME,PASSWORD,CREATED FROM M_USER WHERE ID = ?")
	if err != nil {
		return user, err
	}

	// クエリ実行
	rows, err := stmt.Query(id)
	if err != nil {
		return user, err
	}
	defer rows.Close()

	// SELECTした結果を構造体にマップ
	rows.Next()
	err = rows.Scan(&user.Id, &user.Account, &user.Name, &user.Passwd, &user.Created)
	if err != nil {
		return user, err
	}

	// 取得データをマッピングしたM_USER構造体を返す
	return user, nil
}

データ登録関数(func InsertUser)は、引数で受け取った内容でM_USERマスタへのINSERTを行っています。
引数で受け取っているのは、ACCOUNT、NAME、PASSWORDの3カラムのみです。
CREATED(登録日)には、MySQL側のNow()関数で現在時刻をセットしています。

また、IDカラムはオートインクリメントのため、セットされる値をプログラム側で制御できません。
このため関数の戻り値として、実際に登録されたレコードのID値をリターンするようにしています。

単一行データ取得関数(func SelectUserById)は、引数で受け取ったIDをキーにM_USERマスタの検索をを行っています。
検索結果をM_USER構造体にマッピングしリターンしています。

また、いずれの関数も、接続済みのDBオブジェクトを引数で受け取っています。
これは、データベースへの接続処理は主処理側で行うためです。

それでは、クエリ操作に関する箇所について詳細に解説していきます。

Prepare()関数 – プレースホルダを含むステートメントの作成

db.Prepare()関数は、sql操作を実行するプリペアードステートメントを返します。
サンプル内では22行目と51行目で、それぞれ実行したいクエリを引数に渡して記述しています。

	// プリペアードステートメント
	stmt, err := db.Prepare("INSERT INTO M_USER(ACCOUNT,NAME,PASSWORD,CREATED) VALUES(?,?,?,now())")
	// プリペアードステートメント
	stmt, err := db.Prepare("SELECT ID,ACCOUNT,NAME,PASSWORD,CREATED FROM M_USER WHERE ID = ?")

また、他の言語でも一般的なプレースホルダの機能も問題なく使用できます。
db.Prepare()関数の引数に記述しているSQL内の?がプレースホルダです。

外部からのデータをSQLに直接組み込まず、プレースホルダを使用してマップすることは、SQLインジェクションへの対策として必須です。

Exec()関数 – 更新系のクエリ実行を実行する

INSERT、UPDATE、DELETEなど、更新系のクエリを実行する場合、Exec()関数を使用します。
サンプルでは、データ登録関数内の29行目が該当します。

	// クエリ実行
	result, err := stmt.Exec(acc, name, pw)

実行対象となるstmtは、先ほどPrepare関数で作成したプリペアードステートメントです。
該当のINSERT文には、プレースホルダ(?)が三カ所あるため、引数も3つになっています。

Query()関数 – SELECTを実行する

Query関数は、クエリの実行結果となる行セットを返します。
このため、SELECTを実行する場合、Query()関数を使用します。

サンプルでは、単一行データ取得関数内の57行目が該当します。

	// クエリ実行
	rows, err := stmt.Query(id)

ここでは、実行対象となるSELECT文中のプレースホルダ(?)はWhere句の一カ所のみであるため、引数も一つのみです。

Scan()関数 – SELECT結果を構造体にマッピングする

SELECT結果を構造体にマッピングするには、Scan関数を使用します。
サンプルでは、単一行データ取得関数内の65行目が該当します。

	// SELECTした結果を構造体にマップ
	rows.Next()
	err = rows.Scan(&user.Id, &user.Account, &user.Name, &user.Passwd, &user.Created)

引数に構造体のフィールドのポインタをカンマ区切りで指定しています。
このようにすることで、Query関数で返された行セットのカレント行の値を構造体にマッピングできます。

複数行をSELECTしたい場合

今回のサンプルコードではIDをキーに単一レコードのみをSELECTしていますが、SELECT結果が複数行になる場合はループにより1行ずつScanを行う必要があります。
その場合、以下のように構造体の配列型を定義し、スキャンした各行を配列に格納してリターンするとよいでしょう。

// マスタからSELECTしたデータをマッピングする構造体
type M_USER struct {
	Id      string `db:"ID"`       // ID
	Account string `db:"ACCOUNT"`  // アカウント名
	Name    string `db:"NAME"`     // ユーザ名称
	Passwd  string `db:"PASSWORD"` // パスワード
	Created string `db:"CREATED"`  // 登録日
}

// 全行SELECT用の構造体配列
type UserList []M_USER

// 全行データ取得関数
func SelectUserAll(db *sql.DB) (userlist UserList, err error) {

	// 配列宣言
	var ul UserList

	// プリペアードステートメント
	stmt, err := db.Prepare("SELECT ID,ACCOUNT,NAME,PASSWORD,CREATED FROM M_USER")
	if err != nil {
		return ul, err
	}

	// クエリ実行
	rows, err := stmt.Query()
	if err != nil {
		return ul, err
	}
	defer rows.Close()

	// SELECTした結果を構造体にマップ
	for rows.Next() {
		// 構造体宣言
		var user M_USER
		err = rows.Scan(&user.Id, &user.Account, &user.Name, &user.Passwd, &user.Created)
		// 配列にScan結果を追加
		ul = append(ul, user)
	}

	// 取得データをマッピングしたM_USER構造体配列を返す
	return ul, nil
}

メイン処理からの呼び出し

それでは、いよいよクエリファイルをメイン処理から実行してみます。

package main

import (
	"database/sql"
	"fmt"

	"./conf"  // 実装した設定パッケージの読み込み
	"./query" // 実装したクエリパッケージの読み込み
	_ "github.com/go-sql-driver/mysql"
)

func main() {

	// 設定ファイルを読み込む
	confDB, err := conf.ReadConfDB()
	if err != nil {
		fmt.Println(err.Error())
	}

	// 設定値から接続文字列を生成
	conStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s", confDB.User, confDB.Pass, confDB.Host, confDB.Port, confDB.DbName, confDB.Charset)

	// データベース接続
	db, err := sql.Open("mysql", conStr)
	if err != nil {
		fmt.Println(err.Error())
	}
	// deferで処理終了前に必ず接続をクローズする
	defer db.Close()

	// INSERTの実行
	id, err := query.InsertUser("USER-0001", "山田 hoge郎", "pass", db)
	if err != nil {
		fmt.Println(err.Error())
	}
	fmt.Printf("登録されたユーザのIDは【%d】です。\n", id)

	// SELECTの実行
	user, err := query.SelectUserById(id, db)
	if err != nil {
		fmt.Println(err.Error())
	}
	fmt.Printf("SELECTされたユーザ情報は以下の通りです。\n")
	fmt.Printf("[ID] %s\n", user.Id)
	fmt.Printf("[アカウント] %s\n", user.Account)
	fmt.Printf("[名前] %s\n", user.Name)
	fmt.Printf("[パスワード] %s\n", user.Passwd)
	fmt.Printf("[登録日] %s\n", user.Created)

}

データベースへの接続までは、前回の記事で作成したコードと同様です。

32行目でデータ登録関数を実行し、INSERTされたレコードのIDを受け取って出力しています。
登録するユーザ情報のアカウント名、氏名、パスワードを引数で渡しています。

39行目では、単一行データ取得関数を実行し、取得したレコードの内容を出力しています。
この際、検索条件として、先ほどのデータ登録関数で取得したIDを引数に渡しています。

このサンプルの実行結果は以下の通りです。

$ go run main.go
登録されたユーザのIDは【1】です。

SELECTされたユーザ情報は以下の通りです。
[ID] 1
[アカウント] USER-0001
[名前] 山田 hoge郎
[パスワード] pass
[登録日] 2019-10-30 06:04:29

意図した内容でデータがINSERTされ、SELECTによって検索されていることが確認できますね。

終わりに

2記事にわたりMySQLデータベースの操作方法を解説してきましたが、いかがでしたでしょうか。
基本的なデータベース操作は非常に簡単で、他のプログラミング言語とほぼ変わりなく扱えることがお解りいただけたかと思います。
ただし、今回ご紹介したgo-sql-driver/mysqlパッケージだけでは、いわゆるORM(ORマッピング)の機能を利用できません。
ORMの機能を提供するパッケージは、ドライバ用パッケージとは別にいくつか存在しますので、必要な方は追加で導入を検討してみてください。

スポンサーリンク
スポンサーリンク
スポンサーリンク

シェアする

フォローする

スポンサーリンク
スポンサーリンク

コメント

  1. […] 前回の記事で作成したユーザ情報テーブルの項目を表示するテンプレート内容です。 7行目の{{.account}}、11行目の{{.name}}、15行目の{{.passwd}}にGoプログラムからデータを流し込みます。 […]