5.1 database/sqlインターフェース

GoがPHPと異なる部分は、Goには公式に提供されたデータベースドライバがない事です。開発者が開発するためにデータベースドライバで標準のインターフェースが定義されています。開発者は定義されているインターフェースに従って目的のデータベースドライバを開発することができます。これにはメリットがあります。標準のインターフェースを参照するだけでコードを開発できます。以降データベースに移行する時、どのような修正も必要ありません。では、Goはどのような標準インターフェースを定義しているのでしょうか?詳しく分析してみることにしましょう。

sql.Register

database/sqlに存在する関数はデータベースドライバを登録するためにあります。サードパーティの開発者がデータベースドライバを開発する時は、すべてinit関数を実装します。init関数ではこのRegister(name string, driver driver.Driver)をコールすることでこのドライバの登録を完了させます。

mymysql、sqlite3のドライバではどのようにコールしているのか見てみることにしましょう:

//https://github.com/mattn/go-sqlite3ドライバ
func init() {
    sql.Register("sqlite3", &SQLiteDriver{})
}

//https://github.com/mikespook/mymysqlドライバ
// Driver automatically registered in database/sql
var d = Driver{proto: "tcp", raddr: "127.0.0.1:3306"}
func init() {
    Register("SET NAMES utf8")
    sql.Register("mymysql", &d)
}

サードパーティのデータベースドライバはすべてこの関数をコールすることで自分のデータベースドライバの名前と目的のdriverを登録することがお分かりいただけたかと思います。database/sqlの内部ではひとつのmapを通してユーザが定義した目的のドライバを保存します。

var drivers = make(map[string]driver.Driver)

drivers[name] = driver

なぜならdatabase/sqlによって関数を登録できると同時に複数のデータベースドライバを登録することができるからです。重複させなければよいだけです。

database/sqlインターフェースとサードパーティライブラリを使用する時、よく以下のようになります:

   import (
       "database/sql"
        _ "github.com/mattn/go-sqlite3"
   )

新人はこの_にとても戸惑いがちです。実はこれはGoの絶妙な設計なのです。変数に値を代入する際、よくこの記号が現れます。これは変数を代入する時のプレースホルダの省略です。パッケージのインポートにこの記号を使っても同じような作用があります。ここで使用した_はインポートした後のパッケージ名で、このパッケージに定義されている関数、変数などのリソースを直接使用しない事を意味しています。

2.3節のフローと関数の章においてinit関数の初期化プロセスをご紹介しました。パッケージがインポートされる際はパッケージのinit関数が自動的にコールされ、このパッケージに対する初期化が完了します。そのため、上のデータベースドライバパッケージをインポートするとinit関数が自動的にコールされます。つぎに、init関数でこのデータベースドライバを登録し、以降のコードの中で直接このデータベースドライバを直接使用することができます。

driver.Driver

Driverはデータベースドライバのインターフェースです。methodがひとつ定義されています: Open(name string)、このメソッドはデータベースのConnインターフェースを一つ返します。

type Driver interface {
    Open(name string) (Conn, error)
}

返されるConnは一回のgoroutineの操作を行う事ができるだけです。このConnをGoの複数のgoroutineの中に使うことはできません。以下のコードはエラーが発生します。

...
go goroutineA (Conn)  //検索操作の実行
go goroutineB (Conn)  //挿入操作の実行
...

上のようなコードではGoにとってどのような操作がどのgoroutineによって行われたのか知り得ませんのでデータの混乱を招きます。たとえばgoroutineAで実行された検索操作の結果をgoroutineBに返す場合Bはこの結果を自分が実行した挿入データだと誤解してしまいます。

サードパーティドライバはすべてこの関数を定義しています。これはname引数を解析することによって目的のデータベースの接続情報を得ることができます。解析が終わると、この情報を使って、ひとつのConnを初期化し、それを返します。

driver.Conn

Connはデータベース接続のインターフェース定義です。これにはいくつかのメソッドが定義されています。このConnはひとつのgoroutineの中でしか使用することができず、複数のgoroutineの中では使用することができません。詳細は上の説明をご確認ください。

type Conn interface {
    Prepare(query string) (Stmt, error)
    Close() error
    Begin() (Tx, error)
}

Prepare関数は現在の接続と関連した実行されるSQL文の準備状態を返します。検索、削除等の操作を行うことができます。

Close関数は現在の接続を閉じます。接続が持っているリソースを開放するなど整理作業を行います。ドライバはdatabase/sqlの中のconn poolを実現しているので、問題を起こしやすいのです。

Begin関数はトランザクション処理を表すTxを返します。これを利用して検索、更新といった操作を行うことができます。またはトランザクションに対してロールバックやコミットを行います。

driver.Stmt

Stmtは準備が整った状態です。Connの関連性と、またひとつのgoroutineの中でしか使用することができません。複数のgoroutineに使用することはできません。

type Stmt interface {
    Close() error
    NumInput() int
    Exec(args []Value) (Result, error)
    Query(args []Value) (Rows, error)
}

Close関数は現在の接続状態を閉じます。ただし、もし現在実行されているqueryはrowsデータを返します。

NumInput関数は現在予約されている引数の個数を返します。>=0が返された時はデータベースドライバがインテリジェントに使用側の引数を検査します。データベースドライバパッケージが予約された引数を知らない場合は-1を返します。

Exec関数はPrepareで準備の整ったsqlを実行します。引数を渡し、update/insertといった操作を実行します。Resultデータを返します。

Query関数はPrepareで準備の整ったsqlを実行します。必要な引数を渡し、select操作を実行します。Rowsリザルトセットを返します。

driver.Tx

トランザクション処理には一般的に2つのプロセスがあります。コミットかロールバックです。データベースドライバの中ではこの2つの関数を実装すれば問題ありません。

type Tx interface {
    Commit() error
    Rollback() error
}

この2つの関数のうちひとつはコミットに使用され、もうひとつはロールバックに使用されます。

driver.Execer

これはConnが実装できるインターフェースです。

type Execer interface {
    Exec(query string, args []Value) (Result, error)
}

もしこのインターフェースの定義がなければ、DB.Execがコールされます。つまり、まずPrepareがコールされStmtを返し、その後StmtのExecが実行され、Stmtが閉じられます。

driver.Result

これはUpdate/Insertといった操作が行った結果を返すインターフェースの定義です。

type Result interface {
    LastInsertId() (int64, error)
    RowsAffected() (int64, error)
}

LastInsertId関数はデータベースによって実行された挿入操作によって得られるインクリメントIDを返します。

RowsAffected関数はquery操作で影響されるデータの数を返します。

driver.Rows

Rowsは実行された検索のリザルトセットのインターフェースの定義です

type Rows interface {
    Columns() []string
    Close() error
    Next(dest []Value) error
}

Columns関数はデータベースの検索におけるフィールド情報を返します。これが返すsliceとsql検索のフィールドは一つ一つが対応しており、すべての表のフィールドを返すわけではありません。

Close関数はRowsイテレータを閉じるために用いられます。

Next関数はひとつのデータを返すのに用いられます。データはdestに代入され、destの中の要素はstringを除いてdriver.Valueの値でなければなりません。返されるデータの中のすべてのstringは[]byteに変換される必要があります。もし最後にデータが無い場合、Next関数はio.EOFを返します。

driver.RowsAffected

RowsAffectedは実はint64のエイリアスです。しかしResultインターフェースを実装していますので、低レイヤーでResultの表示メソッドを実装するために用いられます。

type RowsAffected int64

func (RowsAffected) LastInsertId() (int64, error)

func (v RowsAffected) RowsAffected() (int64, error)

driver.Value

Valueは実は空のインターフェースです。どのようなデータも格納することができます。

type Value interface{}

driveのValueはドライバが必ず操作できるValueです。Valueがnilでなければ、下のいずれかとなります

int64
float64
bool
[]byte
string   [*]Rows.Nextが返すものを除いてstringではありません。
time.Time

driver.ValueConverter

ValueConverterインターフェースはどのように普通の値をdriver.Valueのインターフェースの変換するか定義されています。

type ValueConverter interface {
    ConvertValue(v interface{}) (Value, error)
}

開発しているデータベースドライバパッケージではこのインターフェースの関数が多くの場所で利用されています。このValueConverterにはメリットがたくさんあります:

  • driver.valueはデータベース表の対応するフィールドに特化されています。たとえばint64のデータがどのようにデータベース表のunit16フィールドに変換されるかといったことです。
  • データベースの検索結果をdriver.Value値に変換します。
  • scan関数ではどのようにしてdriver.Valueの値をユーザが定義した値に変換するか

driver.Valuer

Valueインターフェースではdriver.Valueをひとつ返すメソッドが定義されています。

type Valuer interface {
    Value() (Value, error)
}

たくさんの型がこのValueメソッドを実装しています。自分自身とdriver.Valueに特化して利用されます。 

上の説明によって、ドライバの開発について基本的なことがお分かりいただけたかとおもいます。このドライバはただこれらインターフェースを実装して追加・削除・検索・修正といった基本操作を可能にするだけです。あとは対応するデータベースに対してデータをやりとりするなど細かい問題が残っています。ここでは細かく述べることはしません。

database/sql

database/sqlではdatabase/sql/driverにて提供されるインターフェースの基礎の上にいくつかもっと高い階層のメソッドを定義しています。データベース操作を容易にし、内部でconn poolを実装しています。

type DB struct {
    driver      driver.Driver
    dsn         string
    mu       sync.Mutex // protects freeConn and closed
    freeConn []driver.Conn
    closed   bool
}

Open関数がDBオブジェクトを返しています。この中にはfreeConnがあり、これがまさに簡単な接続プールのことです。この実装はとても簡単でまた簡素です。Db.prepareを実行する際defer db.putConn(ci, err)を行います。つまりこの接続を接続プールに放り込むのです。毎回connをコールする際はまずfreeConnの長さが0よりも大きいか確認し、0よりも大きかった場合connを再利用してもよいことを示しています。直接使ってかまいません。もし0以下であった場合はconnを作成してこれを返します。

results matching ""

    No results matching ""