15
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Go の DB アクセス用のパッケージを作った

Last updated at Posted at 2016-02-07

Go の DB アクセス用のパッケージを作った

現在では、次の3つのサブパッケージで構成されています。

  • SQL文を組み立てるためのサブパッケージ(cue Query Builder)
  • SQL文を実行して map や struct にマッピングするためのサブパッケージ(ef Execution Facade)
  • 上の2つのパッケージや sql.DB, Tx のラッパー(dw Database Wrapper)

通常は、最初のパッケージ cue と、最後のパッケージ dw を使うことになります。
この2つで、クエリビルダと ORM 相当の機能を使うことができます。

2番目の ef は、既に生の SQL 文があり、database/sql で開発してる(クエリビルダも余計なラッパーも要らない)状況で、ORM 相当だけがほしい場合に使います。
実際には、同様の機能を最後のパッケージ dw がラッピングしますので、直に ef を使うことは少ないと思います。

現時点でベンチマークをとった限りでは、クエリビルダとフェッチの速度は dbr のそれよりも高速なようです。メモリのアロケーション回数と使用量も少ないです。

コーディングのしやすさは…以下を参照ください。
コードのサンプルとともに、使い方を説明します。

使い方

一部を省略した形のコードで、使い方を説明します。

クエリビルダ

メソッドチェーンで SQL ライクに構築していきます。

import "bitbucket.org/shu/dk/cue"
sel := cue.Select().From("users").Where(cue.Eq("id", 100))
query, args := sel.BuildSQL()
// db.QueryRow(query, args...).Scan(~)

cue パッケージ内の関数として、cue.Eq, Neq, Geq, Gr, Like, In, Cmp といった比較を表現するものが用意されています。

最終的に .BuildSQL() とすると、クエリの本文(バインド引数のプレースホルダ付き)とパラメータを生成します。
これらは標準の sql パッケージに渡すことができます。

クエリ構築の部分にありがちな記述ミスを回避しやすくなります。

方言

そのほか、.Dialect(dialect.MySQL{}) とすることで、プレースホルダの形式を MySQL 形式(? 形式)にすることができます。
dialect.DefaultDialect に直接値を代入することで利用側の全体設定を変更することもできます。

import "bitbucket.org/shu/dk/dialect"
func init() {
    dialect.DefaultDialect = dialect.MySQL{}
}

現時点では、以下の方言をサポートしています。

方言 プレースホルダ
MySQL ?
Postgres $1
Oracle :1

フェッチ

dw パッケージの関数を使います。
struct もしくは map[string]interface{} のポインタを引数に FetchOne, FetchAll, FetchEach といった関数を呼び出します。

また、より golang 標準の sql パッケージと親和性の高い ef パッケージの同名関数(引数は多少多くなります)を使うこともできます。

下のコードには struct のフィールドにタグ db:"~" がついているものがあります。
このようにして、struct のフィールド名と同名にならないカラムの名前を指定します。

import "bitbucket.org/shu/dk/dw"
import "bitbucket.org/shu/dk/cue"
type User struct {
    ID       int
    UserName string `db:"name"`
    Age      int    `db:"-"`
    temp     string `db:"temp"` // 非パブリックフィールドはフェッチの対象外
}
var db *dw.DB = dw.Open( sql.Open と同じ引数 )
var u User
stmt := cue.Select().From("users").Where(cue.Geq("id", 100)).OrderBy("id")
if err := db.FetchOne(&u, stmt); err == nil {
    // u.ID == 100, u.UserName == "name100", u.Age == 0
}

struct に結果を格納する場合、SELECT 文のカラムと struct のフィールドのマッチングを行います。
マッチしないカラムがあった場合(例: SELECT Col1, Col2 と struct { Col1, Col9 })、その余剰となったカラム(Col2, Col9)は読み飛ばされます。

ちなみに、FetchAll の場合は、&[]struct か &[]map[string]interface{} を渡します。
これは、すべてのレコードを取得します。

var users []User
if err := db.FetchAll(&users, stmt); if err == nil {
    // users[0].ID, users[0].UserName, users[1].ID, ...
}

FetchEach を使えば、sql.Query のように、rows.Next メソッドを呼び出しながら走査して rows.Scan できます。

rows, _ := db.FetchEach(User{}, stmt)
for rows.Next() {
    var user User
    rows.Scan(&user)
    // user.ID, user.UserName, ...
}

db タグ

struct のフィールドにはタグをつけることができますが、この内 db タグにカラムの仕様を記述することで、dw パッケージや ef パッケージの Update, Insert, Create 関数を使うことができます。

次のようにタグをつけます。

type User struct {
	ID   int    `db:"id; type=serial; pk"`
	Name string `db:"name; type=varchar; size=10; default='nanashi'"`
	Age  int    `db:"age; type=integer; null"`
	Hoge int    `db:"-"`
}

; (セミコロン) で区切ります。
最初の要素が実際のカラム名です。
カラム名に ー (ハイフン) が指定されると、そのフィールドはどのカラムとも関連がないものとしてみなされます。

  • type = VALUE
  • size = VALUE
    • 桁数といった、()でくくられた部分
  • default = VALUE
    • デフォルト値を文字系の型なら 'hoge' のように指定します。
  • pk or primarykey
    • 主キーに指定します。
  • null
    • NULLを許容する場合に指定します。この指定が無い場合は NOT NULL が指定されたことになります。
  • serial or auto_increment
    • (or type:{serial 系の型})
    • 連番なので Insert の対象外としています。

実行

上のようにタグ付けがされていると、Update や Insert、Create が使えます。

// dw パッケージを使う場合
db := dw.Open( sql.Open と同じ引数 )
db.Create("users", User{})
db.Insert("users", User{ID:1, Name:"Ichi", Age:11}) // struct の定義で、ID は serial 型としているので、この ID:1 の内容は無視される
db.Update("users", User{ID:1, Name:"Ichiro", Age:23}) // pk である ID を条件として更新がおこわなれる
// ef パッケージを使う場合
ef.Create(db, "users", User{})
ef.Insert(db, "users", User{ID:1, Name:"Ichi", Age:11}) // struct の定義で、ID は serial 型としているので、この ID:1 の内容は無視される
ef.Update(db, "users", User{ID:1, Name:"Ichiro", Age:23}) // pk である ID を条件として更新がおこわなれる

他のパッケージとの連携

他の同等機能のパッケージと異なり、わざわざ 2 つのパッケージ(cue と ef)に分離したのは、作ろうとした機能が独立していたのと、テストのしやすくするため(複雑さの回避)です。
そういうわけで、2 つをくっつける必然性もなかった、というわけです。

(とか言っておきながら、その後、利便性を理由として dw パッケージを導入しましたが)

標準のパッケージ database/sql との親和性を持つように意識しています。
以下のように、標準や他パッケージと連携できます。

  • cue で構築したクエリとパラメータを db.QueryRow() に渡すことができます
  • 今まで手で構築して sql.Query() していた SQL 文を、ef を使って struct にマッピングすることもできます

そもそも、何故クエリビルダを使うのか(cue)

当然ながら、クエリを手で構築した方がパフォーマンスは良いです。
ただし、データベースへのアクセスの方が圧倒的に遅いため、「まぁ、ちょっとくらいなら、さらに遅くなってもバレないバレない」という感じです。

じゃあ、何のために少し遅くしてまでクエリビルダを使うのか。
次の理由が思い浮かびます。

  • メインの言語(= Go言語)でクエリを記述できる
  • タイプミスや記述ミスの削減
    • SQL のキーワードのタイプミス

      :worried: "sleect * " は実行時エラー

      :thumbsup: cue.Sleect() はコンパイルエラーになってくれる
    • 句の重複

      :worried: if 文で条件をつぎ足し→ "WHERE col1=1 WHERE col2=2"

      :thumbsup: cue.Select().Where(cue.Eq("col1", 1)).Where(cue.Eq("col2", 2)) は AND で結合されます。
    • バインド引数の個数一致、出現順序一致

      :worried: "SELECT * FROM users WHERE name = ? AND group <> ?", ["John"]

      :thumbsup: cue.Select().From("users").Where(cue.Eq("name", "John"), cue.Neq("group", 0))
      → "SELECT * ...", ["John", 0]
  • DBMS の依存が少ない共通化された構文

ORM 的なものはどうか(ef)

ef.Create のためには db タグの中に type=xxx が必要です。
他の ORM では、型の明示を省略した場合は Dialect 相当が Go型→DBMS型 の変換を担います。
この辺りは、もう少し開発の余地があるのかもしれません。 (が、特に CREATE 文の複雑さと DBMS 間の多様さから、あまり深入りはしたくないと思っています)

15
14
1

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
15
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?