title: 【転載】Go 言語による MySQL 操作
date: 2021-08-11 10:52:59
comment: false
toc: true
category:
- Golang
tags: - 転載
- Go
- MySQL
- 操作
本文は転載です:Go 言語による MySQL 操作 | 李文周のブログ
MySQL は業界で一般的に使用されるリレーショナルデータベースであり、この記事では Go 言語が MySQL データベースを操作する方法を紹介します。
Go による MySQL 操作#
接続#
Go 言語のdatabase/sql
パッケージは、SQL または類似の SQL データベースの汎用インターフェースを提供しますが、具体的なデータベースドライバは提供しません。database/sql
パッケージを使用する際には、(少なくとも)1 つのデータベースドライバを注入する必要があります。
私たちが一般的に使用するデータベースには、ほぼすべて完全なサードパーティの実装があります。例えば:MySQL ドライバ
依存関係のダウンロード#
go get -u github.com/go-sql-driver/mysql
MySQL ドライバの使用#
func Open(driverName, dataSourceName string) (*DB, error)
Open は、driverName で指定されたデータベースを開き、dataSourceName で指定されたデータソースを使用します。通常、データベースファイル名とその他の接続に必要な情報を含む必要があります。
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// DSN:データソース名
dsn := "user:password@tcp(127.0.0.1:3306)/dbname"
db, err := sql.Open("mysql", dsn)
if err != nil {
panic(err)
}
defer db.Close() // この行は上のerrチェックの下に書く必要があります
}
考察問題 : なぜ上記のコードのdefer db.Close()
文はif err != nil
の前に書くべきではないのでしょうか?
接続の初期化#
Open 関数は、引数の形式が正しいかどうかを確認するだけで、実際にはデータベースとの接続を作成しません。データソースの名前が実際に有効かどうかを確認するには、Ping メソッドを呼び出す必要があります。
返される DB オブジェクトは、安全に複数の goroutine で同時に使用でき、自身のアイドル接続プールを維持します。したがって、Open 関数は 1 回だけ呼び出されるべきであり、この DB オブジェクトを閉じる必要はほとんどありません。
// グローバルオブジェクトdbを定義
var db *sql.DB
// データベースを初期化する関数を定義
func initDB() (err error) {
// DSN:データソース名
dsn := "user:password@tcp(127.0.0.1:3306)/sql_test?charset=utf8mb4&parseTime=True"
// アカウントとパスワードの正しさは確認しません
// 注意!!!ここで:=を使用しないでください。グローバル変数に値を割り当て、main関数でグローバル変数dbを使用します
db, err = sql.Open("mysql", dsn)
if err != nil {
return err
}
// データベースとの接続を試みる(dsnが正しいか確認)
err = db.Ping()
if err != nil {
return err
}
return nil
}
func main() {
err := initDB() // データベース初期化関数を呼び出す
if err != nil {
fmt.Printf("データベースの初期化に失敗しました, err:%v\n", err)
return
}
}
ここでsql.DB
は接続されたデータベースオブジェクト(構造体インスタンス)を表し、データベース接続に関連するすべての情報を保持します。内部には、0 から複数の基盤接続を持つ接続プールが維持され、複数の goroutine が同時に安全に使用できます。
SetMaxOpenConns#
func (db *DB) SetMaxOpenConns(n int)
SetMaxOpenConns
は、データベースとの接続を確立する最大数を設定します。 n が 0 より大きく、最大アイドル接続数より小さい場合、最大アイドル接続数は最大オープン接続数の制限に合わせて減少します。 n<=0 の場合、最大オープン接続数に制限はなく、デフォルトは 0(無制限)です。
SetMaxIdleConns#
func (db *DB) SetMaxIdleConns(n int)
SetMaxIdleConns は、接続プール内の最大アイドル接続数を設定します。 n が最大オープン接続数より大きい場合、新しい最大アイドル接続数は最大オープン接続数の制限に合わせて減少します。 n<=0 の場合、アイドル接続は保持されません。
CRUD#
データベースとテーブルの作成#
まず、MySQL でsql_test
という名前のデータベースを作成します。
CREATE DATABASE sql_test;
このデータベースに入ります:
use sql_test;
テスト用のデータテーブルを作成するために、以下のコマンドを実行します:
CREATE TABLE `user` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(20) DEFAULT '',
`age` INT(11) DEFAULT '0',
PRIMARY KEY(`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
クエリ#
クエリを簡単にするために、事前に user テーブルのデータを格納するための構造体を定義します。
type user struct {
id int
age int
name string
}
単一行クエリ#
単一行クエリdb.QueryRow()
は、1 回のクエリを実行し、最大 1 行の結果(Row)を返すことを期待します。QueryRow は常に非 nil の値を返し、返り値の Scan メソッドが呼び出されるまで、遅延エラーを返しません。(例:結果が見つからない場合)
func (db *DB) QueryRow(query string, args ...interface{}) *Row
具体的なサンプルコード:
// 単一データのクエリ例
func queryRowDemo() {
sqlStr := "select id, name, age from user where id=?"
var u user
// 非常に重要:QueryRowの後にScanメソッドを呼び出すことを確認してください。そうしないと、保持しているデータベース接続が解放されません
err := db.QueryRow(sqlStr, 1).Scan(&u.id, &u.name, &u.age)
if err != nil {
fmt.Printf("スキャンに失敗しました, err:%v\n", err)
return
}
fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
}
複数行クエリ#
複数行クエリdb.Query()
は、1 回のクエリを実行し、複数行の結果(Rows)を返します。一般的に select コマンドを実行するために使用されます。args パラメータは query 内のプレースホルダパラメータを示します。
func (db *DB) Query(query string, args ...interface{}) (*Rows, error)
具体的なサンプルコード:
// 複数データのクエリ例
func queryMultiRowDemo() {
sqlStr := "select id, name, age from user where id > ?"
rows, err := db.Query(sqlStr, 0)
if err != nil {
fmt.Printf("クエリに失敗しました, err:%v\n", err)
return
}
// 非常に重要:rowsを閉じて保持しているデータベース接続を解放します
defer rows.Close()
// 結果セット内のデータをループして読み取ります
for rows.Next() {
var u user
err := rows.Scan(&u.id, &u.name, &u.age)
if err != nil {
fmt.Printf("スキャンに失敗しました, err:%v\n", err)
return
}
fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
}
}
データの挿入#
挿入、更新、削除操作はすべてExec
メソッドを使用します。
func (db *DB) Exec(query string, args ...interface{}) (Result, error)
Exec は、1 回のコマンド(クエリ、削除、更新、挿入など)を実行し、返される Result は実行された SQL コマンドの要約です。args パラメータは query 内のプレースホルダパラメータを示します。
具体的なデータ挿入のサンプルコードは以下の通りです:
// データの挿入
func insertRowDemo() {
sqlStr := "insert into user(name, age) values (?,?)"
ret, err := db.Exec(sqlStr, "王五", 38)
if err != nil {
fmt.Printf("挿入に失敗しました, err:%v\n", err)
return
}
theID, err := ret.LastInsertId() // 新しく挿入されたデータのid
if err != nil {
fmt.Printf("最後の挿入IDの取得に失敗しました, err:%v\n", err)
return
}
fmt.Printf("挿入成功, idは%dです。\n", theID)
}
データの更新#
具体的なデータ更新のサンプルコードは以下の通りです:
// データの更新
func updateRowDemo() {
sqlStr := "update user set age=? where id = ?"
ret, err := db.Exec(sqlStr, 39, 3)
if err != nil {
fmt.Printf("更新に失敗しました, err:%v\n", err)
return
}
n, err := ret.RowsAffected() // 操作に影響を与えた行数
if err != nil {
fmt.Printf("RowsAffectedの取得に失敗しました, err:%v\n", err)
return
}
fmt.Printf("更新成功, 影響を受けた行数:%d\n", n)
}
データの削除#
具体的なデータ削除のサンプルコードは以下の通りです:
// データの削除
func deleteRowDemo() {
sqlStr := "delete from user where id = ?"
ret, err := db.Exec(sqlStr, 3)
if err != nil {
fmt.Printf("削除に失敗しました, err:%v\n", err)
return
}
n, err := ret.RowsAffected() // 操作に影響を与えた行数
if err != nil {
fmt.Printf("RowsAffectedの取得に失敗しました, err:%v\n", err)
return
}
fmt.Printf("削除成功, 影響を受けた行数:%d\n", n)
}
MySQL のプリペアドステートメント#
プリペアドステートメントとは?#
通常の SQL 文の実行プロセス:
- クライアントが SQL 文のプレースホルダを置き換えて完全な SQL 文を得る。
- クライアントが完全な SQL 文を MySQL サーバーに送信する。
- MySQL サーバーが完全な SQL 文を実行し、結果をクライアントに返す。
プリペアドステートメントの実行プロセス:
- SQL 文を命令部分とデータ部分に分ける。
- まず命令部分を MySQL サーバーに送信し、MySQL サーバーが SQL のプリプロセスを行う。
- 次にデータ部分を MySQL サーバーに送信し、MySQL サーバーが SQL 文のプレースホルダを置き換える。
- MySQL サーバーが完全な SQL 文を実行し、結果をクライアントに返す。
なぜプリペアドステートメントを使用するのか?#
- MySQL サーバーの SQL の繰り返し実行を最適化し、サーバーのパフォーマンスを向上させ、サーバーに事前にコンパイルさせ、一度のコンパイルで何度も実行し、後続のコンパイルコストを節約します。
- SQL インジェクションの問題を回避します。
Go による MySQL のプリペアドステートメントの実装#
database/sql
では、以下のPrepare
メソッドを使用してプリペアドステートメント操作を実現します。
func (db *DB) Prepare(query string) (*Stmt, error)
Prepare
メソッドは、SQL 文を MySQL サーバーに送信し、後のクエリやコマンドのための準備された状態を返します。返り値は、複数のクエリやコマンドを同時に実行できます。
クエリ操作のプリペアドステートメントのサンプルコードは以下の通りです:
// プリペアドクエリの例
func prepareQueryDemo() {
sqlStr := "select id, name, age from user where id > ?"
stmt, err := db.Prepare(sqlStr)
if err != nil {
fmt.Printf("準備に失敗しました, err:%v\n", err)
return
}
defer stmt.Close()
rows, err := stmt.Query(0)
if err != nil {
fmt.Printf("クエリに失敗しました, err:%v\n", err)
return
}
defer rows.Close()
// 結果セット内のデータをループして読み取ります
for rows.Next() {
var u user
err := rows.Scan(&u.id, &u.name, &u.age)
if err != nil {
fmt.Printf("スキャンに失敗しました, err:%v\n", err)
return
}
fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
}
}
挿入、更新、削除操作のプリペアドステートメントも非常に似ています。ここでは挿入操作のプリペアドステートメントの例を示します:
// プリペアド挿入の例
func prepareInsertDemo() {
sqlStr := "insert into user(name, age) values (?,?)"
stmt, err := db.Prepare(sqlStr)
if err != nil {
fmt.Printf("準備に失敗しました, err:%v\n", err)
return
}
defer stmt.Close()
_, err = stmt.Exec("小王子", 18)
if err != nil {
fmt.Printf("挿入に失敗しました, err:%v\n", err)
return
}
_, err = stmt.Exec("沙河娜扎", 18)
if err != nil {
fmt.Printf("挿入に失敗しました, err:%v\n", err)
return
}
fmt.Println("挿入成功です。")
}
SQL インジェクションの問題#
私たちは、どんな時でも自分で SQL 文を結合すべきではありません!
ここでは、name フィールドに基づいて user テーブルをクエリする関数の例を示します:
// SQLインジェクションの例
func sqlInjectDemo(name string) {
sqlStr := fmt.Sprintf("select id, name, age from user where name='%s'", name)
fmt.Printf("SQL:%s\n", sqlStr)
var u user
err := db.QueryRow(sqlStr).Scan(&u.id, &u.name, &u.age)
if err != nil {
fmt.Printf("実行に失敗しました, err:%v\n", err)
return
}
fmt.Printf("user:%#v\n", u)
}
この時、以下の入力文字列はすべて SQL インジェクションの問題を引き起こす可能性があります:
sqlInjectDemo("xxx' or 1=1#")
sqlInjectDemo("xxx' union select * from user #")
sqlInjectDemo("xxx' and (select count(*) from user) <10 #")
補足: 異なるデータベースでは、SQL 文で使用されるプレースホルダの構文は異なります。
データベース | プレースホルダ構文 |
---|---|
MySQL | ? |
PostgreSQL | $1 , $2 等 |
SQLite | ? と$1 |
Oracle | :name |
Go による MySQL トランザクションの実装#
トランザクションとは?#
トランザクション:最小の不可分な作業単位;通常、1 つのトランザクションは 1 つの完全なビジネスに対応します(例えば、銀行口座の振込業務は、最小の作業単位です)。この完全な業務は、複数回の DML(insert、update、delete)文を共同で実行する必要があります。A が B に振込を行う場合、ここで 2 回の update 操作を実行する必要があります。
MySQL では、Innodb
データベースエンジンを使用しているデータベースまたはテーブルのみがトランザクションをサポートします。トランザクション処理は、データベースの整合性を維持し、バッチの SQL 文がすべて実行されるか、すべて実行されないことを保証するために使用されます。
トランザクションの ACID#
通常、トランザクションは 4 つの条件(ACID)を満たす必要があります:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持続性(Durability)。
条件 | 説明 |
---|---|
原子性 | トランザクション(transaction)内のすべての操作は、すべて完了するか、すべて完了しないかのいずれかであり、中間の段階で終了することはありません。トランザクションの実行中にエラーが発生した場合、トランザクション開始前の状態にロールバック(Rollback)されます。まるでこのトランザクションが実行されなかったかのように。 |
一致性 | トランザクション開始前とトランザクション終了後に、データベースの整合性が損なわれていないことを示します。これは、書き込まれたデータがすべての事前定義されたルールに完全に従う必要があることを意味します。これには、データの正確性、一貫性、および後続のデータベースが自発的に予定された作業を完了できることが含まれます。 |
隔離性 | データベースは、複数の同時トランザクションが同時にデータを読み書きおよび変更する能力を許可し、隔離性は、複数のトランザクションが同時に実行される際に交差実行によってデータの不整合が発生するのを防ぎます。トランザクションの隔離は、異なるレベルに分かれています。これには、未コミットの読み取り(Read uncommitted)、コミットされた読み取り(read committed)、再現可能な読み取り(repeatable read)、および直列化(Serializable)が含まれます。 |
持続性 | トランザクション処理が終了した後、データの変更は永続的であり、システム障害が発生しても失われません。 |
トランザクション関連メソッド#
Go 言語では、以下の 3 つのメソッドを使用して MySQL でのトランザクション操作を実現します。 トランザクションを開始
func (db *DB) Begin() (*Tx, error)
トランザクションをコミット
func (tx *Tx) Commit() error
トランザクションをロールバック
func (tx *Tx) Rollback() error
トランザクションの例#
以下のコードは、2 回の更新操作が同時に成功するか、同時に失敗することを保証する簡単なトランザクション操作を示しています。中間状態は存在しません。
// トランザクション操作の例
func transactionDemo() {
tx, err := db.Begin() // トランザクションを開始
if err != nil {
if tx != nil {
tx.Rollback() // ロールバック
}
fmt.Printf("トランザクション開始に失敗しました, err:%v\n", err)
return
}
sqlStr1 := "Update user set age=30 where id=?"
ret1, err := tx.Exec(sqlStr1, 2)
if err != nil {
tx.Rollback() // ロールバック
fmt.Printf("sql1の実行に失敗しました, err:%v\n", err)
return
}
affRow1, err := ret1.RowsAffected()
if err != nil {
tx.Rollback() // ロールバック
fmt.Printf("ret1.RowsAffected()の実行に失敗しました, err:%v\n", err)
return
}
sqlStr2 := "Update user set age=40 where id=?"
ret2, err := tx.Exec(sqlStr2, 3)
if err != nil {
tx.Rollback() // ロールバック
fmt.Printf("sql2の実行に失敗しました, err:%v\n", err)
return
}
affRow2, err := ret2.RowsAffected()
if err != nil {
tx.Rollback() // ロールバック
fmt.Printf("ret1.RowsAffected()の実行に失敗しました, err:%v\n", err)
return
}
fmt.Println(affRow1, affRow2)
if affRow1 == 1 && affRow2 == 1 {
fmt.Println("トランザクションをコミットします...")
tx.Commit() // トランザクションをコミット
} else {
tx.Rollback()
fmt.Println("トランザクションをロールバックします...")
}
fmt.Println("トランザクションの実行に成功しました!")
}
練習問題#
net/http
とdatabase/sql
を組み合わせて、MySQL にユーザー情報を保存するための登録およびログインの簡易 Web プログラムを実装します。