🐬

GolangでMySQL/MariaDB(その1)

2023/05/29に公開

はじめに

前回は、Golang で PostgreSQL の各種データ型の入出力を検証しました。

https://zenn.dev/robon/articles/9d6ba760a74b89

今回は、MySQL と MariaDB を評価します。

MySQL と MariaDB

データベース

Amazon RDS 上に構築した MySQL サーバーを使用します。

設定
エンジンバージョン 8.0.28
インスタンスクラス db.t3.micro
vCPU 2
RAM 1GB
ストレージ 20GiB

同じく MariaDB サーバーです。

設定
エンジンバージョン 10.6.10
インスタンスクラス db.t3.micro
vCPU 2
RAM 1GB
ストレージ 20GiB

クライアント

Amazon EC2 の Amazon Linux 2 から接続します。

ツール

mysql を使用しますので、Amazon Linux 2 の場合は、以下のようにインストールします。

$ sudo yum install mariadb
MySQL [agra]> status
--------------
mysql  Ver 15.1 Distrib 5.5.68-MariaDB, for Linux (x86_64) using readline 5.1

Connection id:          17
Current database:       agra
Current user:           agra@■.■.■.■
SSL:                    Not in use
Current pager:          stdout
Using outfile:          ''
Using delimiter:        ;
Server:                 MySQL
Server version:         8.0.28 Source distribution
Protocol version:       10
Connection:             mysql.■■■■.ap-northeast-1.rds.amazonaws.com via TCP/IP
Server characterset:    utf8mb4
Db     characterset:    utf8mb3
Client characterset:    utf8mb3
Conn.  characterset:    utf8mb3
TCP port:               3306
Uptime:                 30 min 20 sec

Threads: 3  Questions: 1399  Slow queries: 0  Opens: 262  Flush tables: 3  Open tables: 180  Queries per second avg: 0.768
--------------

MySQL [agra]> show variables like "character%";
+--------------------------+-------------------------------------------+
| Variable_name            | Value                                     |
+--------------------------+-------------------------------------------+
| character_set_client     | utf8mb3                                   |
| character_set_connection | utf8mb3                                   |
| character_set_database   | utf8mb3                                   |
| character_set_filesystem | binary                                    |
| character_set_results    | utf8mb3                                   |
| character_set_server     | utf8mb4                                   |
| character_set_system     | utf8mb3                                   |
| character_sets_dir       | /rdsdbbin/mysql-8.0.28.R4/share/charsets/ |
+--------------------------+-------------------------------------------+
8 rows in set (0.00 sec)
MariaDB [agra]> status
--------------
mysql  Ver 15.1 Distrib 5.5.68-MariaDB, for Linux (x86_64) using readline 5.1

Connection id:          62
Current database:       agra
Current user:           agra@■.■.■.■
SSL:                    Not in use
Current pager:          stdout
Using outfile:          ''
Using delimiter:        ;
Server:                 MariaDB
Server version:         10.6.10-MariaDB-log managed by https://aws.amazon.com/rds/
Protocol version:       10
Connection:             maria.■■■■.ap-northeast-1.rds.amazonaws.com via TCP/IP
Server characterset:    latin1
Db     characterset:    latin1
Client characterset:    utf8mb3
Conn.  characterset:    utf8mb3
TCP port:               3306
Uptime:                 38 min 32 sec

Threads: 3  Questions: 1387  Slow queries: 0  Opens: 44  Open tables: 15  Queries per second avg: 0.599
--------------

MariaDB [agra]> show variables like "character%";
+--------------------------+----------------------------------------------+
| Variable_name            | Value                                        |
+--------------------------+----------------------------------------------+
| character_set_client     | utf8mb3                                      |
| character_set_connection | utf8mb3                                      |
| character_set_database   | latin1                                       |
| character_set_filesystem | binary                                       |
| character_set_results    | utf8mb3                                      |
| character_set_server     | latin1                                       |
| character_set_system     | utf8mb3                                      |
| character_sets_dir       | /rdsdbbin/mariadb-10.6.10.R1/share/charsets/ |
+--------------------------+----------------------------------------------+
8 rows in set (0.01 sec)

テスト用のテーブル

データ型の一覧

マニュアル から抜粋します。

名称 別名 説明
tinyint 1byte整数
smallint 2byte整数
mediumint 3byte整数
int 4byte整数
bigint 8byte整数
decimal numeric 固定小数点数
float 4byte浮動小数点数
double 8byte浮動小数点数
bit ビット値
date 日付
datetime 日時
timestamp UNIXタイムスタンプ
time 時間値
year 年(1901~2155)
char 固定長文字列
varchar 可変長文字列
binary 固定長byte列
varbinary 可変長byte列
tinyblob binary large object 大きさ順
blob
mediumblob
longblob
tinytext character large object 大きさ順
text
mediumtext
longtext
enum 列挙
set 集合
geometry pointかlinestringかpolygon
point 座標
linestring 直線で保管される座標の列
polygon 単純かつ閉じたlinestring
multipoint pointの列
multilinestring linestringの列
multipolygon polygonの列
geometrycollection geometryの列
json JSON文字列

テスト用のテーブル

そのまま CREATE TABLE文にします。

CREATE TABLE datatypes (
    col01 tinyint COMMENT '1byte整数',
    col02 smallint COMMENT '2byte整数',
    col03 mediumint COMMENT '3byte整数',
    col04 int COMMENT '4byte整数',
    col05 bigint COMMENT '8byte整数',
    col06 decimal(10,4) COMMENT '固定小数点数',
    col07 float COMMENT '4byte浮動小数点数',
    col08 double COMMENT '8byte浮動小数点数',
    col09 bit(3) COMMENT 'ビット値',
    col10 date COMMENT '日付',
    col11 datetime COMMENT '日時',
    col12 timestamp COMMENT 'UNIXタイムスタンプ',
    col13 time COMMENT '時間値',
    col14 year COMMENT '年',
    col15 char(10) COMMENT '固定長文字列',
    col16 varchar(10) COMMENT '可変長文字列',
    col17 binary(10) COMMENT '固定長byte列',
    col18 varbinary(10) COMMENT '可変長byte列',
    col19 tinyblob COMMENT 'tinyblob',
    col20 blob COMMENT 'blob',
    col21 mediumblob COMMENT 'mediumblob',
    col22 longblob COMMENT 'longblob',
    col23 tinytext COMMENT 'tinytext',
    col24 text COMMENT 'text',
    col25 mediumtext COMMENT 'mediumtext',
    col26 longtext COMMENT 'longtext',
    col27 enum('S', 'M', 'L') COMMENT '列挙',
    col28 set('R', 'G', 'B')  COMMENT '集合',
    col29 geometry COMMENT 'POINTかLINESTRINGかPOLYGON',
    col30 point COMMENT '座標',
    col31 linestring COMMENT '直線で補間される一連の点',
    col32 polygon COMMENT '単純かつ閉じたLINESTRING',
    col33 multipoint COMMENT '座標の集まり',
    col34 multilinestring COMMENT 'LINESTRINGの集まり',
    col35 multipolygon COMMENT 'POLYGONの集まり',
    col36 geometrycollection COMMENT 'GEOMETRYの集まり',
    col37 json COMMENT 'JSON文字列',
    CONSTRAINT pk_datatypes PRIMARY KEY(col01)
) COMMENT='データ型テスト';

Golang

処理系

$ go version
go version go1.18.6 linux/amd64

ライブラリ

github.com/go-sql-driver/mysql の v1.6.0 を使用します。データ型については、以下の仕様でつくられているようです。

https://github.com/go-sql-driver/mysql/blob/v1.6.0/fields.go#L128-L194

アプリケーション

テスト用のテーブルに対して入出力を行うアプリケーションを実装します。歯抜けの状態から徐々に埋めていけるように、Nullable 型で入出力します。

main.go
package main

import (
	"database/sql"
	"log"
	"os"
	"reflect"
	"time"

	_ "github.com/go-sql-driver/mysql"
	"github.com/google/go-cmp/cmp"
	"github.com/jmoiron/sqlx"
)

// Datatypes は、データ型テストテーブル
type Datatypes struct {
	Col01 sql.NullInt32   `db:"col01"` // tinyint
	Col02 sql.NullInt32   `db:"col02"` // smallint
	Col03 sql.NullInt64   `db:"col03"` // mediumint
	Col04 sql.NullInt64   `db:"col04"` // int
	Col05 sql.NullInt64   `db:"col05"` // bigint
	Col06 sql.NullString  `db:"col06"` // decimal(10,4)
	Col07 sql.NullFloat64 `db:"col07"` // float
	Col08 sql.NullFloat64 `db:"col08"` // double
	Col09 []byte          `db:"col09"` // bit(3)
	Col10 sql.NullTime    `db:"col10"` // date
	Col11 sql.NullTime    `db:"col11"` // datetime
	Col12 sql.NullTime    `db:"col12"` // timestamp
	Col13 sql.NullString  `db:"col13"` // time
	Col14 sql.NullInt32   `db:"col14"` // year
	Col15 sql.NullString  `db:"col15"` // char(10)
	Col16 sql.NullString  `db:"col16"` // varchar(10)
	Col17 []byte          `db:"col17"` // binary(10)
	Col18 []byte          `db:"col18"` // varbinary(10)
	Col19 []byte          `db:"col19"` // tinyblob
	Col20 []byte          `db:"col20"` // blob
	Col21 []byte          `db:"col21"` // mediumblob
	Col22 []byte          `db:"col22"` // longblob
	Col23 sql.NullString  `db:"col23"` // tinytext
	Col24 sql.NullString  `db:"col24"` // text
	Col25 sql.NullString  `db:"col25"` // mediumtext
	Col26 sql.NullString  `db:"col26"` // longtext
	Col27 sql.NullString  `db:"col27"` // enum('S', 'M', 'L')
	Col28 sql.NullString  `db:"col28"` // set('R', 'G', 'B')
	Col29 []byte          `db:"col29"` // geometry
	Col30 []byte          `db:"col30"` // point
	Col31 []byte          `db:"col31"` // linestring
	Col32 []byte          `db:"col32"` // polygon
	Col33 []byte          `db:"col33"` // multipoint
	Col34 []byte          `db:"col34"` // multilinestring
	Col35 []byte          `db:"col35"` // multipolygon
	Col36 []byte          `db:"col36"` // geometrycollection
	Col37 sql.NullString  `db:"col37"` // json
}

// Key は、データ型テストテーブルのキー
type Key struct {
	Col01 int32 `db:"col01"`
}

func main() {
	dsn := os.Getenv("DSN")
	db, err := sqlx.Open("mysql", dsn)
	if err != nil {
		log.Printf("sql.Open error %s", err)
	}

	JST, _ := time.LoadLocation("Asia/Tokyo")
	key := Key{1}
	src := Datatypes{
		Col01: sql.NullInt32{Int32: 1, Valid: true},
		Col02: sql.NullInt32{Int32: 2, Valid: true},
		Col03: sql.NullInt64{Int64: 3, Valid: true},
		Col04: sql.NullInt64{Int64: 4, Valid: true},
		Col05: sql.NullInt64{Int64: 5, Valid: true},
		Col06: sql.NullString{String: "123456.1234", Valid: true},
		Col07: sql.NullFloat64{Float64: 1.5, Valid: true},
		Col08: sql.NullFloat64{Float64: 1.25, Valid: true},
		Col09: []byte{3},
		Col10: sql.NullTime{Time: time.Date(2001, 2, 3, 0, 0, 0, 0, JST), Valid: true}, // loc 指定で JST
		Col11: sql.NullTime{Time: time.Date(2001, 2, 3, 4, 5, 6, 0, JST), Valid: true}, // loc 指定で JST, ミリ秒は0
		Col12: sql.NullTime{Time: time.Date(2001, 2, 3, 4, 5, 6, 0, JST), Valid: true}, // MariaDB は default が Now
		Col13: sql.NullString{String: "12:34:56", Valid: true},
		Col14: sql.NullInt32{Int32: 2000, Valid: true},
		Col15: sql.NullString{String: "char(10)", Valid: true}, // 末尾空白なし
		Col16: sql.NullString{String: "varchar", Valid: true},
		Col17: []byte{1, 2, 3, 4, 5, 6, 7, 8, 0, 0}, // 末尾 0 埋め
		Col18: []byte{1, 2, 3, 4, 5, 6, 7, 8},
		Col19: []byte{1, 2, 3, 4, 5, 6, 7, 8},
		Col20: []byte{1, 2, 3, 4, 5, 6, 7, 8},
		Col21: []byte{1, 2, 3, 4, 5, 6, 7, 8},
		Col22: []byte{1, 2, 3, 4, 5, 6, 7, 8},
		Col23: sql.NullString{String: "tinytext", Valid: true},
		Col24: sql.NullString{String: "text", Valid: true},
		Col25: sql.NullString{String: "mediumtext", Valid: true},
		Col26: sql.NullString{String: "longtext", Valid: true},
		Col27: sql.NullString{String: "S", Valid: true},
		Col28: sql.NullString{String: "R,G,B", Valid: true},
		Col29: []byte{
			0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00,
			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0,
			0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0,
			0xBF,
		}, // POINT(1,-1)
		Col37: sql.NullString{String: `{"key": 1}`, Valid: true}, // : の後ろのスペースは1つ
	}

	_, err = db.NamedExec(`
		INSERT INTO datatypes (
			col01, col02, col03, col04, col05, col06, col07, col08, col09, col10,
			col11, col12, col13, col14, col15, col16, col17, col18, col19, col20,
			col21, col22, col23, col24, col25, col26, col27, col28, col29, col30,
			col31, col32, col33, col34, col35, col36, col37
		) VALUES (
			:col01, :col02, :col03, :col04, :col05, :col06, :col07, :col08, :col09, :col10,
			:col11, :col12, :col13, :col14, :col15, :col16, :col17, :col18, :col19, :col20,
			:col21, :col22, :col23, :col24, :col25, :col26, :col27, :col28, :col29, :col30,
			:col31, :col32, :col33, :col34, :col35, :col36, :col37
		)`,
		src,
	)
	if err != nil {
		log.Printf("db.Exec error %s", err)
	}

	dst := Datatypes{}
	query, args, err := db.BindNamed(`
		SELECT 
			col01, col02, col03, col04, col05, col06, col07, col08, col09, col10,
			col11, col12, col13, col14, col15, col16, col17, col18, col19, col20,
			col21, col22, col23, col24, col25, col26, col27, col28, col29, col30,
			col31, col32, col33, col34, col35, col36, col37
		FROM datatypes 
		WHERE col01 = :col01`,
		key,
	)
	if err != nil {
		log.Printf("db.BindNamed error %s", err)
	}
	err = db.QueryRowx(query,
		args...,
	).StructScan(
		&dst,
	)
	if err != nil {
		log.Printf("db.QueryRow error %s", err)
	}
	if !reflect.DeepEqual(src, dst) {
		// log.Printf("\nsrc = %#v\ndst = %#v\n", src, dst)
		diff := cmp.Diff(src, dst)
		if len(diff) > 0 {
			log.Print(diff)
		}
	}

	_, err = db.NamedExec(`
		DELETE FROM datatypes
		WHERE col01 = :col01`,
		key,
	)
	if err != nil {
		log.Printf("db.Exec error %s", err)
	}
}

おわりに

コメントにも書きましたが、気がついたこととしては、

  • 接続文字列に ?parseTime=true&loc=Asia%2FTokyo のようにオプションを付けることで、time.Time が使え、タイムゾーンを日本標準時に設定できます。
  • UNIXタイムスタンプのカラムは、MariaDB では NOT NULL になって、デフォルト値 Now が設定されました。
  • CHAR の末尾の空白はデフォルトでは出力されませんでした。
  • CHAR とは対照的に BINARY の末尾の 0 は出力されます。
  • BINARY 系は、文字列でも受けられなくはないですが、[]byte を string にキャストしたような状態になるので、[]byte が安全と思います。
  • GEOMETRY 系は、組み込み関数を使わない場合は、[]byte での入出力になりますが、解析が面倒なので省略しました。

日本語については、別途、テストしたいと思います。

GitHubで編集を提案
株式会社ROBONの技術ブログ

Discussion