2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ORMパッケージdbrとは? gormとの違いは?[Go][ORM]

Last updated at Posted at 2024-10-07

dbrとは?

dbrとは、GoのORMパッケージの1つです。

ORMとは
Object Relational Mapperの略称で、リレーショナルデーターベースにおける操作をオブジェクト指向の考え方で操作できるようにする技術のことです。

GoでORMを利用すると、定義した構造体とデータベースを簡単に対応させることができ、その状態でデータベースの抽象的な操作をすることが可能になります。

早速dbrを使ってみる。

今回、Dockerでmysqlのコンテナを立ち上げて使ってみようと思います。

使用するmysqlのファイルは以下のようになっています。

docker-compose.yml
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をインストールします。

zsh
go get -u github.com/gocraft/dbr

データベース接続

データベース接続は下のようになります。

go
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のライブラリが集まっているドキュメントでは、

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メソッドを使って接続作業を行う必要があります。

go
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の構造体は下のようになります。

go
type User struct {
	ID   int    `db:"id"`
	Name string `db:"name"`
	Age  int    `db:"age"`
}

db:idという部分はタグで、構造体のフィールドがデータベースでどのカラムと対応しているかを示しています。

データ挿入

データの挿入はINSERTクエリを使って下のように使うと思います。

sql
INSERT INTO user(id,name,age) VALUES (?,?,?);

この操作をdbrで行うには下のようにInsertIntoメソッドを使用します。

go
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クエリを使って行うと思います。

sql
SELECT * FROM user:

このような操作をするには、以下のように書きます。

go
var users []User
_, err = sess.Select("*").From(tableName).Load(&users)

dbrではSELECTFROMといったコマンドがメソッド化されており、引数にはそのコマンドでいつもしているカラムやテーブル名などを指定することになります。

そしてLoad()で結果を変数に代入します。

データを更新する

データを更新するときはUPDATEクエリを使うと思います。

sql
UPDATE user SET age=? WHERE id=?;

データを更新するには、Updateメソッドを使って行います。

go
_, err = sess.Update(tableName).Set("age", 22).Where("id = ?", 1).Exec()
if err != nil {
	log.Println(err)
}

こちらも先ほどの挿入操作と似ているように、メソッドをクエリのよう連ねていくことで、データベース操作が可能となっています。

また複数カラムの値を更新する際は、SetMap()が便利です。
下のようにマップを作成して、キーを対応するカラムにしてバリューを変更したいないようにすることで複数カラムの値の変更を簡単に行えます。

go
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{}の部分は返り値として設定していて、どのような型でもバリューに設定することができます。

dbr.Eq

Eqメソッドを使うことでプレースホルダーを使わずに書くようにできます。

dbr.Eq("id", user.ID)

データを消去する

データを消去するクエリは、一般的には下のようなものになっています。

sql
DELETE FROM user WHERE id = ?;

dbrでは、上のクエリを下のコードで再現します。

go
_, err := sess.DeleteFrom(tableName).Where("id = ?", 1).Exec()
if err != nil {
	log.Fatal(err)
}

結合

Circleテーブルを作成するとします。

sql
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()で挿入します。

go
circle := Circle{
		ID:      1,
		Name:    "testcircle",
		OwnerID: 1,
	}

 _, err := sess.InsertInto(tableName).Columns("id", "name", "owner_id").Record(circle).Exec()

userテーブルとcircleテーブルを左外部結合して表示する場合、下のようなクエリで作成します。

sql
SELECT * FROM user LEFT OUTER JOIN circle ON user.id = circle.owner_id;

これをdbrで表示する場合は、下のようになります。

go
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の構造体を下のようにしていたとします。

go
var result struct {
	User
	Circle
}

UserとCircleの構造体は先述していましたが、Userの名前とCircleの名前のカラム名を同じにしています。

go
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メソッドで入れた出力結果はどうなると思いますか?

go
_, err := sess.Select("*").From(userTable).LeftJoin(circleTable, fmt.Sprintf("%s.id = %s.owner_id", userTable, circleTable)).Load(&result)

この場合は、下のような結果となります。

zsh
2024/10/02 13:42:53 {1 testcircle 22 0 }

nameというカラムが被っているため、本来Userの名前が入って欲しいところがCircleの名前になってしまっています。

この対策としては、そもそもテーブル定義の際にカラムを被らないように設計するか、AS句を使って識別できるようにしてください。

内部結合の場合は下のようになります。

sql
SELECT * FROM user INNER JOIN circle ON user.id = circle.owner_id;
go
_, 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を使った実装でよく使われるトランザクション機能も用意されています。

トランザクションとは

トランザクションとは、sqlによるデータベース操作の一連の更新処理のことを指します。

データの更新のために複数回データベース操作が必要になった場合に、1連の必要なデータベース操作を行うことができたらコミット、できなかったらロールバックしてトランザクション実行前の状態に戻します。

トランザクションはを使った実装は下のようになります。

go
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()では、トランザクション開始を行います。

コンテキストを使う場合は下のようなコードを使います。

go
tx,err := sess.BeginTx(ctx,nil)

コンテキストとは

Goにはcontextパッケージと呼ばれる標準パッケージが用意されており、処理の締め切りキャンセル信号API境界やプロセス間を横断する必要のあるリクエストスコープな値を伝達させることができます。

tx.Rollback()でロールバックを支持することができます。

tx.Commit()でトランザクションを終了させることができます。

クエリを確認する

実際にdbrが使われている場合に、dbrの抽象化されたメソッドが使われているばかりこの操作は何をやっているのかわかりにくいことがあります。

その際には、下のようなにするとクエリを確認することが可能になります。

go
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
go
db.Create(&Person{
		Name: "jack",
		Age:  21,
	})
  • dbr
go
_, err = sess.InsertInto(tableName).Columns("id", "name", "age").Record(user).Exec()

SELECT文は下のようになっています。

  • gorm
go
db.Find(&users)
  • dbr
go
_, err = sess.Select("*").From(tableName).Load(&users)

こう見るとgormはsqlの知識がなくても扱いやすいように抽象化されているのに対して、dbrはsqlの知識がある使い慣れている人からするとクエリを書くような感覚でコードが書けるので良いなと感じました。

gormとdbrどっちがいいのか

個人的にはSQLっぽく複雑なクエリを書きたいのであればdbr、SQLの知識が少なくdb操作がしたいのであればgormだと思いました。

総合的に考えると、gormの方が人気も高く、日本語での情報も多いため、gormを使うことを自分は薦めたいなと思いました。

ただ、自分はSQLの知識をしっかり身に付けたいのでdbrの方が今は好きだなと感じています。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?