dbrとは?
dbrとは、GoのORMパッケージの1つです。
ORMとは
Object Relational Mapper
の略称で、リレーショナルデーターベースにおける操作をオブジェクト指向の考え方で操作できるようにする技術のことです。
GoでORMを利用すると、定義した構造体とデータベースを簡単に対応させることができ、その状態でデータベースの抽象的な操作をすることが可能になります。
早速dbrを使ってみる。
今回、Dockerでmysqlのコンテナを立ち上げて使ってみようと思います。
使用するmysqlのファイルは以下のようになっています。
services:
db:
image: mysql:8.0
container_name: mysql_dbr
restart: always
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: db
MYSQL_USER: user
MYSQL_PASSWORD: password
ports:
- "3306:3306"
volumes:
- db-volume:/var/lib/mysql
volumes:
db-volume:
インストール
dbrをインストールします。
go get -u github.com/gocraft/dbr
データベース接続
データベース接続は下のようになります。
package main
import (
"log"
"github.com/gocraft/dbr"
_ "github.com/go-sql-driver/mysql"
)
func main() {
connect()
}
func connect() {
conn, _ := dbr.Open("mysql", "user:password@tcp(127.0.0.1:3306)/db", nil)
sess := conn.NewSession(nil)
sess.Begin()
log.Println("begin")
}
SetMaxOpenConnsについて
Goのライブラリが集まっているドキュメントでは、
// create a connection (e.g. "postgres", "mysql", or "sqlite3")
conn, _ := Open("postgres", "...", nil)
conn.SetMaxOpenConns(10)
// create a session for each business unit of execution (e.g. a web request or goworkers job)
sess := conn.NewSession(nil)
// create a tx from sessions
sess.Begin()
と書かれていると思います。
ここで出てくるSetMaxOpenConns
は、dbが開けるコネクション数(dbが繋げる数)を設定しているそうです。
この引数の数を大きくするとdbに負荷がかかってしまうので、しっかり考えて使いましょう。
テーブル作成
dbrは、テーブル作成をメソッドがありません。
なので以下のように直接dbにクエリを打ち込むか、goのコードでdbrのExecメソッドを使って接続作業を行う必要があります。
tableName := "user"
cmd := fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (
id PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
age INTEGER NOT NULL)`, tableName)
_, err := sess.Exec(cmd)
if err != nil {
log.Println(err)
}
ちなみに、この場合Userの構造体は下のようになります。
type User struct {
ID int `db:"id"`
Name string `db:"name"`
Age int `db:"age"`
}
db:id
という部分はタグで、構造体のフィールドがデータベースでどのカラムと対応しているかを示しています。
データ挿入
データの挿入はINSERT
クエリを使って下のように使うと思います。
INSERT INTO user(id,name,age) VALUES (?,?,?);
この操作をdbrで行うには下のようにInsertInto
メソッドを使用します。
user := User{
ID: 1,
Name: "John",
Age: 20,
}
_, err = sess.InsertInto(tableName).Columns("id", "name", "age").Record(user).Exec()
if err != nil {
log.Println(err)
}
データを確認する
データの確認は、以下のようなSELECT
クエリを使って行うと思います。
SELECT * FROM user:
このような操作をするには、以下のように書きます。
var users []User
_, err = sess.Select("*").From(tableName).Load(&users)
dbrではSELECT
やFROM
といったコマンドがメソッド化されており、引数にはそのコマンドでいつもしているカラムやテーブル名などを指定することになります。
そしてLoad()
で結果を変数に代入します。
データを更新する
データを更新するときはUPDATE
クエリを使うと思います。
UPDATE user SET age=? WHERE id=?;
データを更新するには、Update
メソッドを使って行います。
_, err = sess.Update(tableName).Set("age", 22).Where("id = ?", 1).Exec()
if err != nil {
log.Println(err)
}
こちらも先ほどの挿入操作と似ているように、メソッドをクエリのよう連ねていくことで、データベース操作が可能となっています。
また複数カラムの値を更新する際は、SetMap()
が便利です。
下のようにマップを作成して、キーを対応するカラムにしてバリューを変更したいないようにすることで複数カラムの値の変更を簡単に行えます。
updates := make(map[string]interface{})
if user.Name != "" {
updates["name"] = user.Name
}
if user.Age != 0 {
updates["age"] = user.Age
}
_, err := sess.Update(userTable).SetMap(updates).Where("id = ?", user.ID).Exec()
if err != nil {
log.Println(err)
}
makeは引数に入れられた配列やスライス、データ構造を動的に作成するメソッドです。
mapは辞書型のデータ構造を作成して、interface{}の部分は返り値として設定していて、どのような型でもバリューに設定することができます。
データを消去する
データを消去するクエリは、一般的には下のようなものになっています。
DELETE FROM user WHERE id = ?;
dbrでは、上のクエリを下のコードで再現します。
_, err := sess.DeleteFrom(tableName).Where("id = ?", 1).Exec()
if err != nil {
log.Fatal(err)
}
結合
Circleテーブルを作成するとします。
CREATE TABLE IF NOT EXISTS circle (
id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
owner_id INTEGER NOT NULL,
FOREIGN KEY (owner_id) REFERENCES user(id));
入れるデータを下のような構造体で定義して、Insert()
で挿入します。
circle := Circle{
ID: 1,
Name: "testcircle",
OwnerID: 1,
}
_, err := sess.InsertInto(tableName).Columns("id", "name", "owner_id").Record(circle).Exec()
userテーブルとcircleテーブルを左外部結合して表示する場合、下のようなクエリで作成します。
SELECT * FROM user LEFT OUTER JOIN circle ON user.id = circle.owner_id;
これをdbrで表示する場合は、下のようになります。
var result struct {
UserID int `db:"id"`
UserName string `db:"name"`
UserAge int `db:"age"`
CircleID int `db:"circle_id"`
Circle string `db:"circle_name"`
}
_, err := sess.Select("user.id AS id", "user.name AS name", "user.age AS age", "circle.id AS circle_id", "circle.name AS circle_name").
From(userTable).
LeftJoin(circleTable, fmt.Sprintf("%s.id = %s.owner_id", userTable, circleTable)).
Load(&result)
if err != nil {
log.Println(err)
}
UserとCircleのカラム名を同じにしてしまったため、カラム名をAS句でつけてしまっています。
LOADに関してなのですが、受け取ったカラムが同じだった場合、上書きされます。
例えば上のコードのresultの構造体を下のようにしていたとします。
var result struct {
User
Circle
}
UserとCircleの構造体は先述していましたが、Userの名前とCircleの名前のカラム名を同じにしています。
type User struct {
ID int `db:"id"`
Name string `db:"name"`
Age int `db:"age"`
}
type Circle struct {
ID int `db:"id"`
Name string `db:"name"`
OwnerID int `db:"owner_id"`
}
この場合に下のように外部結合で出力させてLoadメソッドで入れた出力結果はどうなると思いますか?
_, err := sess.Select("*").From(userTable).LeftJoin(circleTable, fmt.Sprintf("%s.id = %s.owner_id", userTable, circleTable)).Load(&result)
この場合は、下のような結果となります。
2024/10/02 13:42:53 {1 testcircle 22 0 }
name
というカラムが被っているため、本来Userの名前が入って欲しいところがCircleの名前になってしまっています。
この対策としては、そもそもテーブル定義の際にカラムを被らないように設計するか、AS句を使って識別できるようにしてください。
内部結合の場合は下のようになります。
SELECT * FROM user INNER JOIN circle ON user.id = circle.owner_id;
_, err := sess.Select("user.id AS id", "user.name AS name", "user.age AS age", "circle.id AS circle_id", "circle.name AS circle_name").
From(userTable).
LeftJoin(circleTable, fmt.Sprintf("%s.id = %s.owner_id", userTable, circleTable)).
Load(&result)
右外部結合の場合はRightJoin()
完全外部結合はFullJoin()
とメソッドが準備されていますが、左外部結合と使い方がほぼ同じだと感じたので省略します。
トランザクション
DBを使った実装でよく使われるトランザクション機能も用意されています。
トランザクションはを使った実装は下のようになります。
tx, err := sess.Begin()
if err != nil {
log.Println(err)
}
user := User{
ID: 1,
Name: "John",
Age: 20,
}
_, err = tx.InsertInto(userTable).Columns("id", "name", "age").Record(user).Exec()
if err != nil {
tx.Rollback()
log.Println(err)
}
var showUser User
_, err = tx.Select("*").From(userTable).Where("id = ?", 1).Load(&showUser)
if err != nil {
tx.Rollback()
log.Println(err)
}
log.Println(showUser)
tx.Commit()
tx.Begin()
では、トランザクション開始を行います。
コンテキストを使う場合は下のようなコードを使います。
tx,err := sess.BeginTx(ctx,nil)
tx.Rollback()
でロールバックを支持することができます。
tx.Commit()
でトランザクションを終了させることができます。
クエリを確認する
実際にdbrが使われている場合に、dbrの抽象化されたメソッドが使われているばかりこの操作は何をやっているのかわかりにくいことがあります。
その際には、下のようなにするとクエリを確認することが可能になります。
r.buildQueryByAppNotificationFilter(stmt, filter)
buf := dbr.NewBuffer()
stmt.Build(dialect.MySQL, buf)
log.Println(buf.String())
gormとの比較
GoのORMライブラリとして有名なものの中に、gormがあります。
一応Githubでのスター数的は、2024年9月時点で
となっており、gormの方が人気であるとわかります。
機能面の違い
私が確認している機能面の違いとしては以下の2つです。
- テーブル作成機能
- オートマイグレーション機能
この二つの機能をgormではAutoMigrate()
で行うことができます。
しかし、dbrではそのような機能がないため、SQLを書いて手動で行う必要があります。
書き方の違い
INSERT文の書き方は、下のように異なっています。
- gorm
db.Create(&Person{
Name: "jack",
Age: 21,
})
- dbr
_, err = sess.InsertInto(tableName).Columns("id", "name", "age").Record(user).Exec()
SELECT文は下のようになっています。
- gorm
db.Find(&users)
- dbr
_, err = sess.Select("*").From(tableName).Load(&users)
こう見るとgormはsqlの知識がなくても扱いやすいように抽象化されているのに対して、dbrはsqlの知識がある使い慣れている人からするとクエリを書くような感覚でコードが書けるので良いなと感じました。
gormとdbrどっちがいいのか
個人的にはSQLっぽく複雑なクエリを書きたいのであればdbr、SQLの知識が少なくdb操作がしたいのであればgormだと思いました。
総合的に考えると、gormの方が人気も高く、日本語での情報も多いため、gormを使うことを自分は薦めたいなと思いました。
ただ、自分はSQLの知識をしっかり身に付けたいのでdbrの方が今は好きだなと感じています。