Genmai と Gorp プチ比較 + おまけ
Go の簡易 ORM(Object-Relational Mapping)である genmaiと gorpについて、古い情報が散見されるということもあり、ライトユーザーとして気になる範囲でちょっと調べてみました。 比較というより、仕様メモみたくなってしまいました。
-
対象 gorpバージョンは v1ではなく、Pre v2 (go-gorp/gorp)です。安定版ではなく作業中ということなので、本稿の賞味期限は短いかもしれません(2016年6月投稿)。
-
transaction や migration、スピード、その他調べてないです。 双方ともtransactionには対応してるみたいです。 また、migrationのサポートはgenmaiにはありませんが、gorpには何がしかあるようです。
-
gorp,gormその他の機能比較は、http://present.go-steel-programmers.org/talk-review-orms のスライドが要領よく纏まっていると思います(矢印キー使用)。
-
環境:
Go 1.6.2
Windows8 64bit, (CentOS7 64bit MySQLのみ確認)
MySQL5.6、InnoDB、github.com/go-sql-driver/mysql
PostgreSQL9.5、github.com/lib/pq -
本稿では,DBテーブルに直接紐づけ・変換される構造体を 「テーブル構造体」、データを受け取るための構造体を「受け側構造体」と言うことがあります。 これは役割上の表現なので、プログラム上は同じこともあります。テーブル構造体名は XxxxxTbl の形にしてます。 また、表記上「構造体」と「構造体変数」の区別がありませんので適宜読み替えてください。
-
genmaiや gorp実行時にメッセージが出力される場合はエラー扱いしています。 メッセージが出力されてもデータはちゃんと設定されるから無視して出力を利用しようという方は、公式ドキュメント上保障されている使い方か否かよくご確認ください。 たとえば、gorp実行でメッセージが出力されている場合に、
"SELECT * "
のときはOKだが"SELECT カラム名"
とすると設定されない、ということもあります。 -
見やすさを優先して、query文字列中のテーブル名やカラム名を quoteしてないことや、一部MySQL用とPostgreSQL用が混在しています。また、リストを縦に縮めるためと、当方の好みで標準のgo記法に則っていないことがあります。
-
genmaiの githubの公式ドキュメントには、テーブル生成では ORMらしからぬ例しかのってないので、4.1.1 に当方の環境(ローカル)での簡易プログラム例を記載しておきました。ついでなので 4.2.1 に gorpの簡易例も。
*本稿に記した内容やプログラムの利用とその結果生じる事態について、
当方は一切責任を負いかねます。 ご自身の責任においてご利用ください。
#1章 構造体 <-> DBテーブルの静的対応例(MySQL)
1.1 genmai
type Test00 struct {
Val32x uint32
}
*** MySQL , UTF-8 , Windows8 64bit ***
type OtherNameTbl struct { Field Type Null Key Default
+- - - - - - +- - - - - - - - +- - -+- - -+- - - +- - - -
Id int64 `db:"pk"` | id | bigint(20) | NO | PRI | NULL | auto_increment
Str0 string `db:"unique"` | str0 | varchar(255) | NO | UNI | NULL |
Str1 string `default:"'abc'"` | str1 | varchar(255) | NO | | abc |
Str32 string `size:"32"` | str32 | varchar(32) | NO | | NULL |
Str255 string `size:"255"` | str255 | varchar(255) | NO | | NULL |
Str1024 string `size:"1024"` | str1024 | varchar(1024) | NO | | NULL |
Str65536 string `size:"65536"` | str65536 | mediumtext | NO | | NULL |
StrNull sql.NullString | str_null | varchar(255) | YES | | NULL |
StrPtr *string `size:"2048"` | str_ptr | varchar(2048) | YES | | NULL |
Slice0 []byte | slice0 | varbinary(255) | YES | | NULL |
Slice32 []byte `size:"32"` | slice32 | varbinary(32) | YES | | NULL |
Slice255 []byte `size:"255"` | slice255 | varbinary(255) | YES | | NULL |
Slice1024 []byte `size:"1024"` | slice1024 | varbinary(1024)| YES | | NULL |
Slice65536 []byte `size:"65536"` | slice65536 | mediumblob | YES | | NULL |
Val8 int8 | val8 | smallint(6) | NO | | NULL |
Val8u uint8 | val8u | smallint(6) | NO | | NULL |
Val32 int32 `default:"15"` | val32 | int(11) | NO | | 15 |
Val32u uint32 | val32u | int(11) | NO | | NULL |
Val64Null sql.NullInt64 | val64_null | bigint(20) | YES | | NULL |
Val8Ptr *int8 | val8_ptr | smallint(6) | YES | | NULL |
// ValF32 float32 // panic
// ValF64 float64 // panic
// ValF64Null sql.NullFloat64 // panic
ValF32G genmai.Float32 | val_f32_g | double | NO | | NULL |
ValF64G genmai.Float64 | val_f64_g | double | NO | | NULL |
Date time.Time | date | datetime | NO | | NULL |
genmai.TimeStamp | created_at | datetime | NO | | NULL |
| updated_at | datetime | NO | | NULL |
Test00 | val32x | int(11) | NO | | NULL |
ValName int `column:"reNamed"` | reNamed | int(11) | NO | | NULL |
Nothing int `db:"-"` // 非カラム
lowerName int // 非カラム
}
// []byte size= 0: "VARBINARY(255)"、< 約64KB: "VARBINARY(size)"
// < 16MB: "MEDIUMBLOB" 、>= : "LONGBLOB"
db.Insert(&OtherNameTbl{}); db.Select(&rslt); fmt.Println(rslt) の出力(一部編集あり)
0 [{1 { false} <nil>
[] [] [] [] []
0 0 0 0 {0 false} <nil>
0 0
0001-01-01 00:00:00 +0000 UTC
{2016-05-23 02:37:17 +0000 UTC 2016-05-23 02:37:17 +0000 UTC}
{0}
0 0 0}]
1.2 gorp
type Test00 struct {
Val32x uint32
}
*** MySQL , UTF-8 , Windows8 64bit ***
type OtherNameTbl struct { Field Type Null Key Default
+- - - - - - +- - - - - - - - - +- - +- - +- - - +- - - -
Id int64 `db:",primarykey,autoincrement"`| Id | bigint(20) | NO | PRI| NULL | auto_incremen
Str0 string /* unique指定はコード*/ | Str0 | varchar(255) | YES | | NULL |
Str1 string `db:",default:\"abc\""`| Str1 | varchar(255) | YES | | NULL |
Str32 string `db:",size:32"` | Str32 | varchar(32) | YES | | NULL |
Str255 string `db:",size:255"` | Str255 | varchar(255) | YES | | NULL |
Str1024 string `db:",size:1024"` | Str1024 | text | YES | | NULL |
Str65536 string `db:",size:65536"` | Str65536 | text | YES | | NULL |
StrNull sql.NullString | StrNull | varchar(255) | YES | | NULL |
StrPtr *string `db:",size:2048"` | StrPtr | text | YES | | NULL |
Slice0 []byte | Slice0 | mediumblob | YES | | NULL |
Slice32 []byte `db:",size:32"` | Slice32 | mediumblob | YES | | NULL |
Slice255 []byte `db:",size:255"` | Slice255 | mediumblob | YES | | NULL |
Slice1024 []byte `db:",size:1024"` | Slice1024 | mediumblob | YES | | NULL |
Slice65536 []byte `db:",size:65536"` | Slice65536 | mediumblob | YES | | NULL |
Val8 int8 | Val8 | tinyint(4) | YES | | NULL |
Val8u uint8 | Val8u | tinyint(3) unsigned| YES | | NULL |
Val32 int32 `db:",default:0xf"` | Val32 | int(11) | YES | | NULL |
Val32u uint32 | Val32u | int(10) unsigned | YES | | NULL |
Val64Null sql.NullInt64 | Val64Null | bigint(20) | YES | | NULL |
Val8Ptr *int8 | Val8Ptr | tinyint(4) | YES | | NULL |
ValF32 float32 | ValF32 | double | YES | | NULL |
ValF64 float64 | ValF64 | double | YES | | NULL |
ValF64Null sql.NullFloat64 | ValF64Null | double | YES | | NULL |
Date time.Time | Date | datetime | YES | | NULL |
Test00 | Val32x | int(10) unsigned | YES | | NULL |
ValName int `db:"reNamed"` | reNamed | int(11) | YES | | NULL |
Nothing int `db:"-"` // 非カラム
//lowerName int // panic
}
dbmap.Insert(&OtherNameTbl{}); dbmap.Select(&rslt,"SELECT * FROM OtherNameTbl"); fmt.Println(rslt)の出力(一部編集あり)
0 [{1 abc { false} <nil>
[] [] [] [] []
0 0 15 0 {0 false} <nil>
0 0 {0 false}
0001-01-01 00:00:00 +0000 UTC
{0}
0 0}]
1.3 比較
展開されるデータ型はシステムによって異なりますので、参考程度にお読みください。 以下は主に MySQL,Windows64bitでの話です。
1. genmaiでは、テーブル構造体のフィールド名はデフォルトでは snake_case変換されてカラム名になりますが、タグを使って異なるカラム名をつけることもできます。 gorpでは、タグあるいはコード(動的)でカラム名を指定できます。
2. genmaiではテーブル名は、デフォルトでは構造体名をsnake_case変換したものになりますが、構造体に TableNamerインタフェースを実装することで別名にできます。 gorpでは、テーブル名はデフォルトでは構造体名と同じですが、コードで別名を指定できます。
type OtherTbl struct {
Id int64 `db:"pk"`
}
func (tbl *OtherTbl) TableName() string { // テーブル名
return "another_tbl" // other_tbl -> another_tbl
}
3. genmaiでもgorpでも、テーブル構造体・受け側構造体ともに構造体の埋込み(フィールド名なし)に対応しています。 フィールド名をつける場合には、テーブル構造体では非カラム(db:"-"
)にする必要があります。 つまり、フィールド名のついた構造体フィールドはDBテーブルのカラム定義用途には使えません。
(2016-05の時点でgorpでは、 フィールド名で varchar(255)のカラムが作られます)
(2016-05の時点でgenmaiでは、多段の埋め込みに問題があります。修正コードを本節最後に記します)
type PsnlData struct {
Name string
Age int
}
type UserTbl struct {
Id int64 `db:"pk"` // `db:",primarykey,autoincrement"`
PsnlData // 埋め込み
// Person PsnlData // 不可
Person PsnlData `db:"-"` // 非カラム
}
4. genmaiでは string,intなどに別の型名をつけてカラムにすることはできません(テーブル生成時panic)。 gorpではカラムは作られ、int系は OKに見えますが、 stringは現状ではInsert( )時に sql: converting Exec argument #21's type: unsupported type main.PsnStr, a string
と出力され、Insert( )できません。
type PsnStr string
type PsnVal int
type UserTbl struct {
Id int64 `db:"pk"` // `db:",primarykey,autoincrement"`
// Name PsnStr // 不可
// Age PsnVal // genmaiでは不可
}
5. 小文字で始まるテーブル構造体のフィールドは、genmaiでは非カラム扱いです。 gorpではカラムは作られますが、insert()で panicを起こすことがあります。
6. どちらも基本的には配列表記に対応しておらず、スライス [] uint8
または [] byte
のみ可能なようです。
7. どちらも(not)null、index関連はコード(ダイナミック)で設定できます。
デフォルトでは、genmaiでは NullXXXや ポインタ、スライスでなければ 基本的に NOT NULL、gorpでは、基本的に NULL値可と設定されます。
8. タグ指定default値の扱いは大きく異なり、genmaiではDBテーブル上でカラムデフォルト値として設定されますが "TestTbl{}"
をinsertすると、(当然だが)そのデフォルト値にはなりません。
逆に gorpでは、DBテーブル上でカラムデフォルト値には設定されませんが、"TestTbl{}"
をinsertすると、DB上そのデフォルト値となります。 ただし gorpでは、タグでデフォルト値を指定していると Insert( )時に指定したオブジェクトの値が無視されて、そのデフォルト値がDBに反映されるのは奇妙です(2016-05時点)。
9. テーブル構造体のフィールド名(カラム名)と埋め込まれた構造体のフィールド名(カラム名)に重複があった場合、テーブル生成時にgenmaiではエラーとなりますが、gorpでは エラーにならないことがあるので意図してないのなら注意が必要です。
type UserTbl struct { Field int ; Field2 int }
type UserATbl struct { Field int ; fielda int ; User }
・・・
dbmap.AddTable( UserATbl )
dbmap.CreateTable() // ---> カラムは Field,fielda,Field2 ができる
10. 時間型については jsonやドライバーとの関係でいろいろ微妙なものがあるようですが、未調査です。 genmaiには 下記のTimeStamp型があり、生成時やupdate時に何がしかのサポートがあるみたいです。
type TimeStamp struct {
// 生成時刻。 このフィールドはBeforeInsertにより自動的に設定される
CreatedAt time.Time `json:"created_at"`
// update時刻。 このフィールドはBeforeUpdateにより自動的に設定される
UpdatedAt time.Time `json:"updated_at"`
}
gorpには、「time.Timeフィールドを database/sqlドライバーへ渡すが、この型の振舞いはドライバーによる。 MySQLユーザはとくに注意要。 timezone/DSTの潜在的問題を避けるには、timeデータ用に整数フィールドを用い UNIXタイムをストアすることも考えるみるべし」みたいなことが書いてあります。
*genmai 多段埋め込み対処
・・・
func (db *DB) fieldIndexByName(t reflect.Type, name string, index []int) []int {
・・・
if field.Anonymous {
if idx := db.fieldIndexByName(field.Type, name, append(index, i)); len(idx) > 0 {
//return append(index, idx...) *** アペンドせずに idxをそのまま返す ***
return idx
}
}
・・・
}
#2章 テーブル生成・QUERYの方式
本章では、次のようにしておきます。
// テーブル構造体 <---> DBテーブル // ------ gorp --------
type UserTbl struct { // 変名 --> user_tbl
Id int64 `db:"pk"` // `db:"id,primarykey,autoincrement"`
Name string // `db:"name"`
Age int // `db:"age"`
}
// 受け側構造体:DBと直接の紐づけなし
type UserData struct { //
Age int //
lowerDmy int //
Name2 string `column:"name"` // `db:"name"`
}
・・・
db.Insert( &UserTbl{ Name: "Alice", Age:22 })
db.Insert( &UserTbl{ Name: "John" , Age:33 })
2.1 genmai
2.1.1 方式
1. テーブル構造体に基づく DBテーブル作成時、その他 DBとのやり取り時は
指定された構造体を毎回解析し、リード・ライトします。
2. クエリは関数形式で組み立てます
var users []UserTbl
db.Select( &users ) // 全行・全カラム
db.Select( &users, db.Where( "age",">",30 )) // 全カラム、条件付き
db.Select( &users, []string{ "name","age" }) // 一部カラム(name,age)
・・・
db.Update( &users ) // primaryKeyに基づく
-
カラムの指定は基本的にはDB上の名称を用います。 ALIAS指定できません。
-
Select( )の第一パラメータはカラム値を受け取るためのもので、公式ドキュメントでは「スライスへのポインタで、その要素型は構造体でなければならない」となっています。 しかし、ソース上は次のどちらかでもOKにみえます。
a. スライスへのポインタで、その要素型は構造体または構造体へのポインタ。
データは 上書きではない です。
b. int,string等(非構造体)へのポインタで、カラム指定は1つであること。
最初に見つかった行の値が設定される。
// '構造体へのポインタ'のスライスへのポインタ
var data [](*UserData)
data = append( data,&UserData{ Name2:"Bob-0",Age:15 } )
data = append( data,&UserData{ Name2:"Bob-1",Age:15 } )
data = append( data,&UserData{ Name2:"Bob-2",Age:15 } )
err = db.Select( &data, []string{"name","age"},db.From(&UserTbl{}))
fmt.Println( data ) // --> [0xc08209f0c0 0xc08209f120]
fmt.Println( *data[0],*data[1] ) // --> {22 0 Alice} {33 0 John}
// stringへのポインタ
var name string
err = db.Select( &name, "name", db.From(&UserTbl{}),
db.Where( "age",">",1 ).OrderBy("id", "DESC"))
fmt.Println( name ) // --> John
var user UserTbl;
db.Select( &user,db.From(&UserTbl{}),db.Limit(1) ) // エラー(構造体へのポインタ)
var ids []int64 ;
db.Select( &ids, "id",From(&UserTbl{})) // エラー(非構造体のスライス)
- On( )での記述は次のパターンです
On( "id" ) From() or 受け側構造体テーブル.id = Joinテーブル.id
On( "id", "=", 1 ) From() or 受け側構造体テーブル.id = 1
On( &Table{}, "id" ) table.id = Joinテーブル.id
On( &Table{},"id","=", 1 ) table.id = 1, Table{} はポインタでなくても可
- Where( )、And( )、Or( )等での記述は次のパターンです
Or( db.Where(・・・).And(・・・)) 条件のネスト
Where( "id" ) あとに.Between()その他 Conditionが続く
Where( &Table{}, "id" ) あとに.Between()その他 Conditionが続く
Where( "id", "=", 1 ) パラメータ 3つ
Where( &Table{}, "id", "=", 1 ) パラメータ 4つ
2.1.2 genmai.Select( )の受け側構造体とDBテーブルの関係
1. From( )でテーブル構造体を指定しない場合、その受け側構造体に対応する
DBテーブルの処理が実行されます。 (テーブル構造体=受け側構造体)
2. From( )でテーブル構造体を指定した場合
- テーブル構造体とは異なる構造体(受け側構造体)でDBデータを受けとれます。
DBからリードしたカラム名と一致するフィールドが受け側構造体から検索されますが、受け側構造体のフィールドにタグによるカラム名指定(column:"xxx"
)があるなら、フィールドはその名前として扱われ、カラム指定がないなら、デフォルトのsnake_case変換された名前として扱われます。
var data []UserData
db.Select( &data, []string{"name","age"},db.From(&UserTbl{}))
fmt.Println( data ) // --> [{22 0 Alice} {33 0 John}]
-
受け側構造体の小文字で始まるフィールドでは、カラム値を受け取れません。
-
受け側構造体には、ユーザが定義したフィールド名付きの構造体型(埋め込みでない)があってもかまいませんが、カラム値は受け取れません。
-
受け側構造体では、タグによる非カラム(
db:"-"
)指定のあるフィールドでもカラム値を受け取れます。 (小文字で始まるフィールドを除く) -
カラム名に対応する受け側構造体フィールドがみつからない場合はエラーが返ります。
type UserData2 struct {
Age int
}
type UserData3 struct {
Name string
}
type UserData4 struct { // 受け側構造体:DBと直接の紐づけなし
UserData2 // 受け取り可
User UserData3 // 受け取り不可(フィールド名あり)
// name string // 受け取り不可(小文字で始まる)
Name string
Id int64 `db:"-"` // 受け取り可(非カラム化でも)
}
・・・
var data4 []UserData4
db.Select( &data4, []string{"name","age","id"},db.From(&UserTbl{}))
fmt.Println( data4 ) // --> [{{22} {} Alice 1 } {{33} {} John 2 }]
2.2 gorp
2.2.1 方式
1. はじめにDBデータと構造体の紐づけ情報を作成登録し、以後、その情報をもとに
処理を行います。
-
プログラム起動時には毎回、情報の登録作業(AddTable)するのが基本となります(登録しただけではDB上のデータに直接的な影響はありません)。
-
Select系、insert等では、登録していなくてもDBとデータをやり取りできます。
Select( )ではそうすることで受け側構造体の自由度が増します。 しかし insert( )の場合は、例えば テーブル名やカラム名を動的に変名していた場合、次回起動後に未登録の状態で使えるのか不明です(未調査)。 -
通常は使いませんが、テーブル名を指定しないDropTables( )や TrucateTables( ) は、DbMapに登録されたテーブル情報をもとに処理を行います。 従って、あらかじめ DbMapに AddTableされていないDB上のデータには影響しません。 テストプログラム等を書くときにはちょっとご注意。
また、DropTables( )あるいは、構造体を指定した DropTable( )をコールしても、DbMap中の登録情報の方は消えないようです。 DbMap中の登録情報の一部だけを削除するメソッドはないみたいです(全削除=新規作成)。
2. Select( )ではクエリをSQL文で記述します
var users []UserTbl // PostgreSQLでは ? -> $1
dbmap.Select( &users, `SELECT * FROM user_tbl WHERE age > ?`,30 )
・・・
dbmap.Update( &users ) // primaryKeyに基づく
-
カラムの指定は基本的にはDBテーブル上の名称を用います。 ALIAS指定もできます。
-
Select( )の第一パラメータが
a. スライスへのポインタの場合(その要素型は構造体でなくてもOK、ポインタもOK)、
結果はそのスライスに 付加 されます。
b. 構造体 or 構造体へのポインタの場合、結果はメソッドの戻り値としてポインタの
スライスで返されます。 元の構造体(第一パラメータ)には影響はありません。
// スライスへのポインタ
users := []UserTbl{ {100,"Bob",44} }
dbmap.Select( &users,`SELECT * FROM user_tbl` )
fmt.Println( users ) // -->[{100 Bob 44} {1 Alice 22} {2 John 33}] 付加
// スライスへのポインタ
ids := []int64{100,200}
dbmap.Select( &ids,`SELECT id FROM user_tbl` )
fmt.Println( ids ) // -->[100 200 1 2] 付加
// 構造体
var usp2 []interface{}
usp2,_ = dbmap.Select( UserTbl{},`SELECT * FROM user_tbl` )
fmt.Println( usp2 ) // --> [0xc0820058c0 0xc082005940]
fmt.Println( usp2[0],usp2[1] ) // --> &{1 Alice 22} &{2 John 33}
// 構造体へのポインタ
user := UserTbl{500,"Mick",11}
usp2,_ = dbmap.Select( &user,`SELECT * FROM user_tbl` )
fmt.Println( usp2 ) // --> [0xc082084cc0 0xc082084d40]
fmt.Println( usp2[0],usp2[1] ) // --> &{1 Alice 22} &{2 John 33}
fmt.Println( user ) // -->[{500 Mick 11}] 変化なし
- 対象となる行が1行の場合は SelectOne( )の第一パラメータで、求める値が1つの int64 や stringその他の場合は SelectInt( ),SelectStr( )、その他の戻り値でカラム値を受け取れます。
var user UserTbl // PostgreSQLでは ? -> $1
dbmap.SelectOne( &user,`SELECT * FROM user_tbl WHERE id=?`,1 )
fmt.Println( user ) // -->{1 Alice 22}
- クエリ文に仕込むプレースホルダの拡張として、
":id"
の形の、名前による埋め込み方法が用意されています。
// マップによる置換
var users4 []UserTbl
replMap := map[string]interface{}{ "name": "Alice", "age": 20 }
dbmap.Select( &users4, `SELECT * FROM user_tbl
WHERE name = :name AND age > :age`, replMap )
fmt.Println( users4 ) // -->[{1 Alice 22}]
// 構造体による置換
type DataStr struct {
UserName string
Age int
}
users4 = []UserTbl{}
replStr := DataStr{ UserName:"Alice", Age: 20 }
dbmap.Select( &users4, `SELECT * FROM user_tbl WHERE name = :UserName
AND age > :Age`, &replStr )
fmt.Println( users4 ) // -->[{1 Alice 22}]
2.2.2 gorp.Select( )の受け側構造体とDBテーブルの関係
Select( )の受け側構造体とDBテーブルは直接には関係ありません。 事前のAddTable登録がなくても動きます。 DBからリードしたカラム名と一致するフィールドが受け側構造体から検索されますが、受け側構造体のフィールドにタグによるカラム名指定(db:"xxx"
)があるならフィールドはその名前として扱われ、カラム指定がないならデフォルトの名前のまま扱われます。
なお、ここでのフィールド名・カラム名は 大小文字の区別は無い みたいです(ソースをチラ見した限りでは小文字変換して比較しているようだし、狭い範囲での実験でもそうなりますが、あまり自信はないです)。
genmaiと同様、次の制限があります
-
受け側構造体の小文字で始まるフィールドで、カラム値を受け取ろうとするとpanicとなります。
-
受け側構造体には、ユーザが定義したフィールド名付きの構造体型(埋め込みでない)があってもかまいませんが、カラム値は受け取れません。
-
受け側構造体では、タグによる非カラム(
db:"-"
)指定のあるフィールドではカラム値は受け取れません。 -
カラム名に対応する受け側構造体フィールドがみつからない場合はエラーが返ります。
3章 genmaiの拡張
genmaiとgorpの最大の違いは、クエリに関数形式を用いるか、SQL文を使うかにあると思います。 genmaiで簡単な場合には関数形式を使っていても、複雑な場合は標準のdatabase/sqlを取り出して使えばいいのですが、それでは、構造体との連携がとれません。 そこで woremacxさんが genmaiでSQL文を使えるように SelectSql( )を公開しました。 (http://qiita.com/woremacx/items/ac3585d786f5f7e52318)
私も氏に倣って、genmaiライブラリを少しいじって、SelectSql2( )を作ってみました。 具体例は4章 参照。
なお、本拡張は、簡単なことは簡単にできるようにと考えたであろうgenmai作者の意に反するかもしれないことを記しておきます。
3.1 SelectSql2( )
gorpと同様のクエリSQL文方式です。
受け側を複数指定でき、また AS を使ってカラム値を柔軟に処理できます。 使い方は4章参照
func (db *DB) SelectSql2( args ...interface{} ) error
例:
err = db.SelectSql2( &users,&camps,
`SELECT * FROM user_tbl JOIN campany_tbl
ON user_tbl.campany_id = campany_tbl.id
WHERE age > ? `,20 ) // PostgreSQLでは ? -> $1
1. 受け側データの指定様式は次のどれか1つで混在は不可。
a. スライスへのポインタで、その要素型は構造体(ポインタ不可)。 複数指定可。
b. スライスへのポインタで、その要素型は非構造体(ポインタ不可)。 複数指定可
ですが、パラメータ指定順にカラム値を設定します。型不一致時の動作は不定。
c. int,string等(非構造体)へのポインタ。 複数指定可ですが、パラメータ指定順
にカラム値を設定します。 型不一致時の動作は不定。
a. ではテーブル構造体とは異なる(受け側)構造体で値を受けとれます。
受け取り時に検索される名称は、フィールド名に最外の構造体名(*1)を前置したものと、しないものの両方です。 埋め込まれた(フィールド名のない)構造体の構造体名は無視されます。
カラム値を受け取るときの検索対象名は、つぎの4パターンとなります。
`最外の構造体名(*1).非構造体要素フィールド名(*2)`
`非構造体要素フィールド名(*2)`
`最外の構造体名(*1).埋め込まれた構造体内の非構造体要素フィールド名(*2)`
`埋め込まれた構造体内の非構造体要素フィールド名(*2)`
例:
UserB struct { ValB int ; }
UserA struct { ValA int ; UserB } // 受け側構造体
のとき、UserA を受け側構造体にすると
val_a
user_a.val_a
val_b
user_a.val_b
が検索可能となります。
(*1) TableNamerインタフェースを実装していればそれで定義したテーブル名、
実装していなければsnake_case変換された名称
(*2) タグによるカラム名指定があればその名称、なければsnake_case変換
された名称
非カラム化(db:"-"
)は無効(つまりあっても検索される)。
小文字で始まるフィールドをカラム値の受け取りに指定できません。
2. 受け取るフィールドが見つからなかった場合、あるいは受け取るフィールド候補が複数見つかった場合は、エラーが返ります。
3.2 ReplaceQueryKey( )
クエリなどの文字列に対して、"{key}"
の形を、文字列置換できます。
func (db *DB) ReplaceQueryKey( query string, quote bool,
args ...interface{} ) (string,error)
渡されたqueryから "{"+key+"}"
を単純に探し、あればすべて置き換え、なければ何もしません。 quoteがtrueなら置換文字列をquoteします。
安易・単純な文字列置換であって、gorpのような プレースホルダへの展開ではありません。 また、それが文字列データ中かとか、その他の判別も一切しません。
1. stringを keyとした、map あるいは(ユーザ定義の)構造体へのポインタを渡
してください。 mapでも構造体でも、string型、または complex以外の数値型
のみ設定可能です(他は無視)。
2. 構造体は埋め込み(フィールド名なし)のみ可、フィールド名はcolumn指定有効
ですが、デフォルトでは snake_case変換されます。
// マップによる置換
repls := map[string]interface{}{
"tbl": "`user_tbl`", "age": "11,22" }
query,_ := db.ReplaceQueryKey( `SELECT * FROM {tbl}
WHERE age IN({age})`,
false,repls )
fmt.Println( query )// --> "SELECT * FROM `user_tbl` WHERE age IN(11,22)"
// 構造体による置換
type AgeInt int
type DataStr struct {
UserName string `column:"u_name"` // "{u_name}" を置換
UserAge AgeInt // "{user_age}" を置換
}
data := DataStr{ UserName:"Alice", UserAge: 31 }
query,_ = db.ReplaceQueryKey( `SELECT * FROM user_tbl
WHERE name = {u_name} AND age < {user_age}`,
true,&data )
fmt.Println( query ) // --> SELECT * FROM user_tbl
// WHERE name = "Alice" AND age < 31
・・・
db.SelectSql2( &users,query )
本関数を sql直接呼出し等で prepareをまたいで利用するようなことはできません。 genmaiでは prepareを外に見せていません。
3.3 Select2( )
SelectSql2( )となるべく整合がとれるように、Select( )も少しだけ変えてみました。
func (db *DB) Select2( output interface{},
args ...interface{} ) error
1. パラメータにFrom( )でテーブル構造体が指定されていない場合は、
オリジナルの genmai.Select( )と全く同じです。
2. パラメータにFrom( )でテーブル構造体が指定されている場合
- 先頭のパラメータ群は、SelectSql2( )と同じで受けを複数指定できます。
- SELECTするカラム指定だけは、ALIAS指定できます。
- SELECTでカラムを指定する際は基本的に
"user_tbl.*"
や"user_tbl.column"
のようにテーブル名を明示してください。 この様式は オリジナルの Select()とは異なります。 単純な"*"
の意味は オリジナルSelect()とは異なります。
3. Join( ),Where( ),On( )その他は、オリジナルgenmaiと全く同じです。
複数テーブルが関与する時はカラム指定がうまくいかないことがあります。 問題のある場合や細かいことをされる方は SelectSql2( )をご利用ください。
squirrel や、MySQLなら sqlbuilderのようなクエリビルダーの利用もありと思います(よく知りませんが)
3.4 ColumnAlias2( )
func (db *DB) ColumnAlias2( pref bool,tbl interface{} ) []string
指定された構造体を受け側構造体に見立てて、DB上のカラムと受け側で検索されるフィールド名を紐づける ALIASのスライスを生成します。 使い方は 4.1参照
1. 受け側構造体のポインタを渡してください。
指定された構造体の各フィールドについて、prefが trueか、あるいは他の構造体のフィールド名と重複するときは
"最外構造体名(*1)"."フィールド名(*2)" AS "最外構造体名(*1).フィールド名(*2)"
pref が false かつ重複しないなら
"フィールド名(*2)" AS "フィールド名(*2)"
を出力します。
(*1) TableNamerインタフェースを実装していればそれで定義したテーブル名、
実装していなければsnake_case変換された名称
(*2) タグによるカラム名指定があればその名称、なければsnake_case変換
された名称
2. フィールド名を持たない埋め込まれた構造体の構造体名は無視されます。
3. 小文字で始まるフィールド、非カラム(db:"-"
)指定のあるフィールド、
フィールド名を持つネストした構造体等(time.Timeやsql.NullXxxxを除く)は出力しません。
User struct { Val int ; }
UserA struct { ValA int ; ValX int ; User }
UserB struct { ValB int ; ValX int : User }
clmAs,_ := db.ColumnAlias2( false,&UserDataA{},&UserDataB{} )
fmt.Prinfln( clmAs )//-->[ `val_a` AS `val_a`
// `user_a`.`val_x` AS `user_a.val_x`
// `user_a`.`val` AS `user_a.val`
// `val_b` AS `val_b`
// `user_b`.`val_x` AS `user_b.val_x`
// `user_b`.`val` AS `user_b.val` ] (編集あり)
4章 JOIN
genmai/gorpどちらもJOINに対応していない/使えないと(昔は)言われていましたが、決してそういうわけではないです。
-
本章での課題については私の試した範囲では、genmaiオリジナルの Join処理(LeftJoinではない)では、From()で指定したテーブル名が SELECTカラムの前に前置されるため、Join()するテーブルのカラムを指定できませんでした。 また、SELECTで
"*"
を指定した時は、 Joinでも LeftJoin でも Fromテーブルのカラム値しか取得できませんでした(2016-05時点)。 以降では、拡張genmaiについて話を進めます。 -
ここでは、それぞれの課題に対して簡易な1方法をあげただけです。 他にもいろいろなやり方、例えばフックメソッドの利用等もあると思います。
-
ソースプログラムを解析してユーザが直接定義していない構造体やヘルパー関数を作成するようなことは考えてません・・・そういった方式のほうがいい気もしますが。
-
ちょっと長くなりますが、4.1.1、4.2.1 に当方の環境(ローカル)での簡易プログラム例をあまり省略せずに記載しました。
4.1 拡張 genmai
メインのDBテーブル+JOINするDBテーブルの一部または全カラム値の取得方法を考えてみます。
4.1.1 新規の受け側構造体でうける
必要なカラムが少ないときや余計な名前を出したくない時は、受け側構造体を必要にあわせて定義するのが簡単でしょう。 カラム名に重複がある場合は ASを使って振り分ける必要があります。
package main
import (
_ "database/sql"
"fmt"
"os"
_ "time"
_ "strings"
// _ "github.com/mattn/go-sqlite3"
_ "github.com/go-sql-driver/mysql"
// _ "github.com/lib/pq"
"github.com/naoina/genmai"
)
type UserTbl struct {
Id int64 `db:"pk"`
Name string // 重複
Age int
CampanyId int64
}
type CampanyTbl struct { // テーブル構造体
Id int64 `db:"pk"`
Name string // 重複
}
type UserData struct { // 受け側構造体
Name string // <- user_tbl.name
Age int // <- user_tbl.age
CampanyName string // <- campany_tbl.name
}
//---------------------------------------------------------------------
func main() {
db,err := genmai.New( &genmai.MySQLDialect{},"user:password@/test?parseTime=true" )
//db,err := genmai.New( &genmai.SQLite3Dialect{}, ":memory:")
//db,err := genmai.New( &genmai.PostgresDialect{},
// "user=postgres password=postgresql host=localhost dbname=test sslmode=disable" )
if err != nil { fmt.Println( err ) }
defer db.Close()
db.SetLogOutput( os.Stderr )
err = db.SetLogFormat( `--{{.time.Format "15:04:05"}}--{{.query}}` )
err = db.DropTable( &UserTbl{} ) // DBテーブル削除
err = db.DropTable( &CampanyTbl{} ) // DBテーブル削除
err = db.CreateTable( &UserTbl{} ) // DBテーブル削除
err = db.CreateTable( &CampanyTbl{} ) // DBテーブル削除
if err != nil { fmt.Println( err ) }
obj1 := CampanyTbl{ Name: "camp-1" }; _,err = db.Insert( &obj1 );
obj2 := CampanyTbl{ Name: "camp-2" }; _,err = db.Insert( &obj2 );
obj3 := CampanyTbl{ Name: "camp-3" }; _,err = db.Insert( &obj3 );
_,err = db.Insert( &UserTbl{ Name: "Alice1", Age:11, CampanyId: obj1.Id })
_,err = db.Insert( &UserTbl{ Name: "Bob2" , Age:22, CampanyId: obj1.Id })
_,err = db.Insert( &UserTbl{ Name: "John3" , Age:33, CampanyId: obj3.Id })
if err != nil { fmt.Println( err ) }
var data []UserData
err = db.SelectSql2( &data,`SELECT user_tbl.name AS name ,
age ,
campany_tbl.name AS campany_name
FROM user_tbl JOIN campany_tbl
ON user_tbl.campany_id = campany_tbl.id ` )
if err != nil { fmt.Println( err ) }
fmt.Println( data ) // --> [{Alice1 11 camp-1}
// {Bob2 22 camp-1}
// {John3 33 camp-3}]
err = db.Select2( &data, []string{
"user_tbl.name AS name" ,
"age" ,
"campany_tbl.name AS campany_name" } ,
db.From( &UserTbl{} ),
db.Join( &CampanyTbl{} ).On( "campany_id","=","id" ))
}
AS句の手書きは間違いのもとですが、次のように受け側フィールドに検索対象名をタグカラム指定すれば、3章 ColumnAlias2( )を使って確実に処理できます。
type UserData2 struct { // 受け側専用構造体(非テーブル構造体)
Name string `column:"user_tbl.name"`
Age int `column:"user_tbl.age"` // 重複しない場合も指定のこと
CampanyName string `column:"campany_tbl.name"`
}
clmAs,_ = db.ColumnAlias2( false,&UserData2{} )
var data2 []UserData2
db.SelectSql2( &data2,`SELECT ` + strings.Join( clmAs,", " ) +
` FROM user_tbl JOIN campany_tbl
ON user_tbl.campany_id = campany_tbl.id` )
db.Select2( &data2, clmAs ,db.From( &UserTbl{} ),
db.Join( &CampanyTbl{} ).On( "campany_id","=","id" ))
他に、ややトリッキーでお勧めしませんが、受け側構造体フィールドの非カラム化(db:"-"
)が無効となる(つまり検索される)ことを利用して、テーブル構造体の中に取得したい他テーブルのカラムフィールドを潜り込ませる方法もあります(Select2( )ではFrom( )指定要)。
type UserTbl struct { // テーブル構造体
Id int64 `db:"pk"`
Name string
Age int64
CampanyId int64
CampanyName string `db:"-"` // 非カラム <-- 別テーブルのデータ受け取り
}
4.1.2 データ個別にうける
同時にリードするカラムが少ないときは、データを個別に受けるのが簡単です。
カラムとデータの指定順に注意願います。 型不一致時の動作は不定です。
・・・
var userNames []string
var ages []int
var campNames []string
db.SelectSql2( &userNames,&ages,&campNames,`SELECT
user_tbl.name , age ,campany_tbl.name
FROM user_tbl JOIN campany_tbl
ON user_tbl.campany_id = campany_tbl.id` )
fmt.Println( userNames ) // --> [Alice1 Bob2 John3]
fmt.Println( ages ) // --> [11 22 33]
fmt.Println( campNames ) // --> [camp-1 camp-1 camp-3]
db.Select2( &userNames,&ages,&campNames,[]string{
"user_tbl.name AS name" ,
"age" ,
"campany_tbl.name AS campany_name" } ,
db.From( &UserTbl{} ),
db.Join( &CampanyTbl{} ).On( "campany_id","=","id" ))
4.1.3 テーブル構造体でうける
1. カラム名に重複がない場合
テーブル構造体をそのまま使ってカラム値を取得する一番簡単・確実な方法は、同時にリードするフィールド名をすべて異なる名前にしてへたに変名せず(デフォルトのsnake_case変換に任せる)、それらテーブル構造体を1つの受け側構造体に埋め込むか、そのまま複数のテーブル構造体でうける方式です。
type UserTbl struct {
Id int64 `db:"pk"`
Name string
Age int64
CampanyId int64
}
type CampanyTbl struct {
Id int64 `db:"pk"`
CampanyName string
}
type UserData struct {
UserTbl // 埋め込み
CampanyTbl // 埋め込み
}
・・・
// 1つの構造体で受ける
var data []UserData
db.SelectSql2( &data,`SELECT user_tbl.name,
user_tbl.age,
campany_tbl.campany_name
FROM user_tbl JOIN campany_tbl
ON user_tbl.campany_id = campany_tbl.id` )
fmt.Println( data ) // -->[ {{0 Alice1 11 0} {0 camp-1}}
// {{0 Bob2 22 0} {0 camp-1}}
// {{0 John3 33 0} {0 camp-3}}]
db.Select2( &data, []string{ "user_tbl.name",
"user_tbl.age",
"campany_tbl.campany_name" } ,
db.From( &UserTbl{} ),
db.Join( &CampanyTbl{} ).On( "campany_id","=","id" ))
// 複数の構造体で受ける
var users []UserTbl
var camps []CampanyTbl
db.SelectSql2( &users,&camps, `SELECT user_tbl.name,
user_tbl.age,
campany_tbl.campany_name
FROM user_tbl JOIN campany_tbl
ON user_tbl.campany_id = campany_tbl.id` )
fmt.Println( users ) // --> [{0 Alice1 11 0} {0 Bob2 22 0} {0 John3 33 0}]
fmt.Println( camps ) // --> [{0 camp-1} {0 camp-1} {0 camp-3}]
db.Select2( &users,&camps, []string{ "user_tbl.name",
"user_tbl.age",
"campany_tbl.campany_name" } ,
db.From( &UserTbl{} ),
db.Join( &CampanyTbl{} ).On( "campany_id","=","id" ))
2. カラム名に重複がある場合
一般的には、カラム名やフィールド名は重複するでしょう。 この場合にテーブル構造体をそのまま使うには、各テーブル構造体を並べ、ASで振り分けます。
type UserTbl struct {
Id int64 `db:"pk"`
Name string // 重複
Age int
CampanyId int64
}
type CampanyTbl struct {
Id int64 `db:"pk"`
Name string // 重複
}
・・・
var users []UserTbl
var camps []CampanyTbl
// 一部カラム
db.SelectSql2( &users,&camps ,`SELECT ` +
`user_tbl.name AS "user_tbl.name", ` +
`user_tbl.age AS "user_tbl.age", ` +
`campany_tbl.name AS "campany_tbl.name" ` +
` FROM user_tbl JOIN campany_tbl
ON user_tbl.campany_id = campany_tbl.id` )
fmt.Println( users )// --> [{0 Alice1 11 0} {0 Bob2 22 0} {0 John3 33 0}]
fmt.Println( camps )// --> [{0 camp-1} {0 camp-1} {0 camp-3}]
db.Select2( &users,&camps, []string{
`user_tbl.name AS "user_tbl.name"`,
`user_tbl.age AS "user_tbl.age"`,
`campany_tbl.name AS "campany_tbl.name"` },
db.From( &UserTbl{} ),
db.Join( &CampanyTbl{} ).On( "campany_id","=","id" ))
もし、全カラムを受けても構わないなら、カラム指定は 3章 ColumnAlias2( )を使って次のようにできます。
// 全カラム
clmAs,_ = db.ColumnAlias2( true,&UserTbl{},&CampanyTbl{} )
db.SelectSql2( &users,&camps , "SELECT " + strings.Join( clmAs,"," ) +
` FROM user_tbl JOIN campany_tbl
ON user_tbl.campany_id = campany_tbl.id` )
db.Select2( &users,&camps, clmAs ,db.From( &UserTbl{} ),
db.Join( &CampanyTbl{} ).
On( "campany_id","=","id" ))
4.2 gorp
4.1と同様に、メインのDBテーブル+JOINするDBテーブルの一部または全カラムの取得方法を考えてみます。
4.2.1 新規の受け側構造体でうける
必要なカラムが少ないときや余計な名前を出したくない時は、受け側構造体を必要にあわせて定義するのが簡単でしょう。 カラム名に重複がある場合は ASを使って振り分ける必要があります。
package main
import (
"os"
"log"
"fmt"
_ "time"
_ "log"
// _ "github.com/mattn/go-sqlite3"
_ "github.com/go-sql-driver/mysql"
// _ "github.com/lib/pq"
"database/sql"
"github.com/go-gorp/gorp"
)
type UserTbl struct {
Id int64 `db:"id,primarykey,autoincrement"`
Name string `db:"name"` // 重複
Age int `db:"age"`
CampanyId int64 `db:"campany_id"`
}
type CampanyTbl struct {
Id int64 `db:"id,primarykey,autoincrement"`
Name string `db:"name"` // 重複
}
type UserData struct { // 受け側構造体
Name string // <- UserTbl.Name
Age int // <- UserTbl.Age
CampanyName string // <- CampanyTbl.Name
}
//---------------------------------------------------------------------
func main() {
dialect := gorp.MySQLDialect{"InnoDB", "UTF8"}
dbm,err := sql.Open( "mysql", "user:password@/test?parseTime=true" ) // []byte -> time.Time変換
//dialect := gorp.PostgresDialect{}
//dbm,err := sql.Open( "postgres","user=postgres password=postgresql host=localhost dbname=test sslmode=disable" )
//dialect := gorp.SqliteDialect{}
//dbm,err := sql.Open( "sqlite3", ":memory:")
if err != nil { fmt.Println( err ) }
dbmap := &gorp.DbMap{Db: dbm, Dialect: dialect}
defer dbmap.Db.Close()
dbmap.TraceOn( "[gorp]",log.New( os.Stderr,"",log.Ltime ))
dbmap.AddTableWithName( UserTbl{},"user_tbl" ) // テーブル名 UserTbl -> user_tbl
dbmap.AddTableWithName( CampanyTbl{},"campany_tbl" )// テーブル名 CampanyTbl -> campany_tbl
err = dbmap.DropTablesIfExists() // テーブル削除
if err != nil { fmt.Println( err ) }
err = dbmap.CreateTables()
if err != nil { fmt.Println( err ) }
obj1 := CampanyTbl{ Name: "camp-1" }; err = dbmap.Insert( &obj1 );
obj2 := CampanyTbl{ Name: "camp-2" }; err = dbmap.Insert( &obj2 );
obj3 := CampanyTbl{ Name: "camp-3" }; err = dbmap.Insert( &obj3 );
if err != nil { fmt.Println( err ) }
err = dbmap.Insert( &UserTbl{ Name: "Alice1", Age:11, CampanyId: obj1.Id })
err = dbmap.Insert( &UserTbl{ Name: "Bob2" , Age:22, CampanyId: obj1.Id })
err = dbmap.Insert( &UserTbl{ Name: "John3" , Age:33, CampanyId: obj3.Id })
if err != nil { fmt.Println( err ) }
var data []UserData
_,err = dbmap.Select( &data,`SELECT user_tbl.name AS "Name" ,
age AS "Age" ,
campany_tbl.name AS "CampanyName"
FROM user_tbl JOIN campany_tbl
ON user_tbl.campany_id = campany_tbl.id ` )
if err != nil { fmt.Println( err ) }
fmt.Println( data ) // --> [{Alice1 11 camp-1}
// {Bob2 22 camp-1}
// {John3 33 camp-3}]
}
genmai 4.1.1 の最後でやったような、元のテーブル構造体に別テーブルのカラムを潜ませる手は使えません。 しかし次は有効なので、genmai.ColumnAlias2()と同じものを自作すれば、AS句を手書きしなくても済みます。
type UserData2 struct { // 受け側専用構造体(非テーブル構造体)
Name string `db:"user_tbl.name"`
Age int `db:"user_tbl.age"`
CampanyName string `db:"campany_tbl.name"`
}
var data2 []UserData2
dbmap.Select( &data2,`SELECT ` + `user_tbl.name AS "user_tbl.name" , ` +
`user_tbl.age AS "user_tbl.age" ,` +
`campany_tbl.name AS "campany_tbl.name" ` +
` FROM user_tbl JOIN campany_tbl
ON user_tbl.campany_id = campany_tbl.id` )
fmt.Println( data2 ) // --> 上記と同じ
4.2.2 データ個別にうける
必要なカラムが1つのときには、SelectOne( )でも可能ですが、拡張genmaiのような複数指定はできません。
4.2.3 テーブル構造体でうける
1. カラム名に重複がない場合
テーブル構造体をそのまま使ってカラム値を取得する簡単な方法は、同時にリードするフィールド名に異なる名前を与え、(一般的にはカラム名を小文字として)、それらテーブル構造体を1つの受け側構造体に埋め込む方式です。
type UserTbl struct {
Id int64 `db:"id,primarykey,autoincrement"`
Name string `db:"name"`
Age int64 `db:"age"`
CampanyId int64 `db:"campany_id"`
}
type CampanyTbl struct {
Id int64 `db:"id,primarykey,autoincrement"`
CampanyName string `db:"campany_name"`
}
type UserData struct {
UserTbl // 埋め込み
CampanyTbl // 埋め込み
}
・・・
_,err = dbmap.Select( &data,`SELECT user_tbl.name,
user_tbl.age,
campany_tbl.campany_name
FROM user_tbl JOIN campany_tbl
ON user_tbl.campany_id = campany_tbl.id` )
if err != nil { fmt.Println( err ) }
fmt.Println( data ) // -->[ {{0 Alice1 11 0} {0 camp-1}}
// {{0 Bob2 22 0} {0 camp-1}}
// {{0 John3 33 0} {0 camp-3}}]
2. カラム名に重複がある場合
テーブル構造体をそのまま使うには、受け側構造体を複数指定できないため、1つの受け側構造体に複数のテーブル構造体を埋め込む必要があります。 カラム名に重複がある時、同時にリードしなければ問題ありませんが、重複したものを同時にリードして振り分けることは、私の試した範囲ではできませんでした。
次は、表題に反しますが、カラム名重複分だけ定義しなおす例です。
type UserTbl struct {
IdUser int64 `db:"id,primarykey,autoincrement"`
Name string `db:"name"` // 重複
Age int64 `db:"age"`
CampanyId int64 `db:"campany_id"`
}
type CampanyTbl struct {
IdCampany int64 `db:"id,primarykey,autoincrement"`
Name string `db:"name"` // 重複
}
type UserData struct { //新規 受け側構造体
UserTbl
CampanyTbl
UserName string // リードするカラム名重複の分
CampanyName string //
}
・・・
dbmap.Select( &data,
`SELECT age ,
user_tbl.name AS "UserName",
campany_tbl.name AS "CampanyName"
FROM user_tbl INNER JOIN campany_tbl
ON user_tbl.campany_id = campany_tbl.id` )
if err != nil { fmt.Println( err ) }
fmt.Println( data ) // --> [ {{0 11 0} {0 } Alice1 camp-1}
// {{0 22 0} {0 } Bob2 camp-1}
// {{0 33 0} {0 } John3 camp-3}]
*上例よりも 4.2.1 や gorp公式ドキュメントの例のように、必要なカラム分
だけを定義する方がベターかもしれません。
5章 まとめ
-
標準ライブラリで十分という人も多いと思いますが、小規模プログラムでもテーブルは10個以上にはなりますし、開発段階で何度も手直しが入ることを考えると、どちらでも十分便利ではないでしょうか。
-
世界的に見て情報量・人気ともgorpに軍配があがりますが、genmaiあるいは 拡張genmaiはスピードに対する懸念はあるかもしれませんが、デフォルトのsnake_case変換も寄与して、シンプルでありながら機能的にもそこそこ太刀打ちできるものと思います。
★★注意★★
本稿は、まだ十分に検証されているとは言えません。 利用には十分ご注意ください。 誤り等あればご報告いただけると有難いです。
6章 ソースリスト
インストールは godepをつかうのが普通かもしれませんが、私は適当に済ませました。 お勧めしませんが、下記の Goファイル(genmai2.go)を ($GOPATH)/src/github.com/naoina/genmai
に置いて、
($GOPATH)/src/github.com/naoina にて
$ go install -v ./genmai
で再インストールすればとりあえずは動きます。
Windows 64bitのときには、mingw 64であることや gccのパスにご注意ください。
以下の追加プログラムは実際のところ、ほとんどnaoinaさんのそれ(よく似た関数名)と同じです。 いずれドキュメントを整えて、githubにgenmaiの別forkとしてあげたいと思っています。
// Copyright (c) 2016 Norifumi Mizuo
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// Package genmai provides simple, better and easy-to-use Object-Relational Mapper.
package genmai
import (
"database/sql"
_ "errors"
"fmt"
_ "io"
"reflect"
"runtime"
_ "sort"
_ "strconv"
"strings"
_ "sync"
_ "github.com/naoina/go-stringutil"
)
//-----------------------------------------------------------------------------------
// SelectSql2() works like Select() but it requires SQL query string.
// you can specify the output parameter multiply
func (db *DB) SelectSql2( args ...interface{}) (err error) {
defer func() {
if e := recover(); e != nil {
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
err = fmt.Errorf("genmai:%v\n%v", e, string(buf[:n]))
}
}()
if len(args) < 2 { return fmt.Errorf( "genmai.SelectSql2: you need data-receiver and query string") }
var rTypes []reflect.Type
var rValues []reflect.Value
var query string
var offs int
var paramId int = -1 // 0:slice of struct all , 1:slice of non-struct all
// 2: non-slice & non-struct all
for i,arg := range args {
offs = i
rv := reflect.ValueOf(arg)
if rv.Kind() == reflect.String {
query = arg.(string)
break
}
if rv.Kind() != reflect.Ptr { return fmt.Errorf( "genmai.SelectSql2: data-receiver must be a pointer") }
rv = rv.Elem()
rValues = append( rValues,rv )
rTypes = append( rTypes,rv.Type() )
switch rv.Kind() {
case reflect.Ptr :
return fmt.Errorf( "genmai.SelectSql2: `%v` duplicated pointer not supported",rv.Type() )
case reflect.Slice :
t := rv.Type().Elem()
if t.Kind() == reflect.Ptr {
return fmt.Errorf( "genmai.SelectSql2: `%v` slice of pointer not supported",t )
}
if t.Kind() == reflect.Struct {
if paramId == -1 { paramId = 0 }
if paramId != 0 {
return fmt.Errorf( "genmai.SelectSql2: `%v` data-reciever must be slice all / non-slice all / simple type all",rv.Type() )
}
} else {
if paramId == -1 { paramId = 1 }
if paramId != 1 {
return fmt.Errorf( "genmai.SelectSql2: `%v` data-reciever must be slice all / non-slice all / simple type all",rv.Type() )
}
}
case reflect.Invalid:
return fmt.Errorf("genmai.SelectSql2: nil pointer dereference")
default:
if paramId == -1 { paramId = 2 }
if paramId != 2 {
return fmt.Errorf( "genmai.SelectSql2: `%v` data-reciever must be slice all / non-slice all / simple type all",rv.Type() )
}
}
}
if paramId == -1 { return fmt.Errorf( "genmai.SelectSql2: you need data-receiver" ) }
if query == "" { return fmt.Errorf( "genmai.SelectSql2: you need query string" ) }
args = args[offs+1:]
//---------------------------------
stmt, err := db.prepare(query)
if err != nil { return err }
defer stmt.Close()
rows, err := stmt.Query(args...)
if err != nil { return err }
defer rows.Close()
//---------------------------------
var values []reflect.Value
switch paramId {
case 0 : values,err = db.selectToSliceStruct4( rows,rTypes )
case 1 : values,err = db.selectToSliceValues4( rows,rTypes )
case 2 : values,err = db.selectToValues4( rows,rTypes )
}
for i,value := range values { rValues[i].Set(value) }
if err != nil { return err }
return nil
}
//---------------------------------------------------------------------
// Select2() works like Select() but you can specify the output parameter multiply
// when you specify the From()
func (db *DB) Select2( args ...interface{} ) (err error) {
defer func() {
e := recover()
if e != nil {
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
err = fmt.Errorf("genmai:%v\n%v", e, string(buf[:n]))
}
}()
//---------------------------------
var tableName string
var rTypes []reflect.Type
var rValues []reflect.Value
var offs int
var paramId int = -1 // 0:slice of struct all , 1:slice of non-struct all
// 2: non-slice & non-struct all
//---------------------------------
for _, arg := range args { // No From() ---> Select()
if f, ok := arg.(*From); ok {
if tableName != "" { return fmt.Errorf("Select2: From statement specified more than once") }
tableName = f.TableName
}
}
if tableName == "" { return db.Select( args ) }
//---------------------------------
if len(args) < 1 { return fmt.Errorf( "genmai.Select2: you need data-receiver") }
for i,arg := range args { // ptr,[ptr], [string/[]string],[non-ptr],[non-ptr]
offs = i
rv := reflect.ValueOf(arg)
if rv.Kind() != reflect.Ptr { break }
rv = rv.Elem()
rValues = append( rValues,rv )
rTypes = append( rTypes,rv.Type() )
switch rv.Kind() {
case reflect.Ptr :
return fmt.Errorf( "genmai.Select2: `%v` duplicated pointer not supported",rv.Type() )
case reflect.Slice :
t := rv.Type().Elem()
if t.Kind() == reflect.Ptr {
return fmt.Errorf( "genmai.Select2: `%v` slice of pointer not supported",t )
}
if t.Kind() == reflect.Struct {
if paramId == -1 { paramId = 0 }
if paramId != 0 {
return fmt.Errorf( "genmai.Select2: `%v` data-reciever must be slice all / non-slice all / simple type all",rv.Type() )
}
} else {
if paramId == -1 { paramId = 1 }
if paramId != 1 {
return fmt.Errorf( "genmai.Select2: `%v` data-reciever must be slice all / non-slice all / simple type all",rv.Type() )
}
}
case reflect.Invalid:
return fmt.Errorf("genmai.Select2: nil pointer dereference")
default:
if paramId == -1 { paramId = 2 }
if paramId != 2 {
return fmt.Errorf( "genmai.Select2: `%v` data-reciever must be slice all / non-slice all / simple type all",rv.Type() )
}
}
}
if paramId == -1 { return fmt.Errorf( "genmai.Select2: you need data-receiver (pointer)" ) }
args = args[offs:]
//---------------------------------
col, from, conditions, err := db.classify2(tableName, args)
if err != nil { return err }
queries := []string{`SELECT`, col, `FROM`, db.dialect.Quote(from)}
var values []interface{}
for _, cond := range conditions {
q, a := cond.build(0, false)
queries = append(queries, q...)
values = append(values, a...)
}
query := strings.Join(queries, " ")
stmt, err := db.prepare(query, values...)
if err != nil { return err }
defer stmt.Close()
rows, err := stmt.Query(values...)
if err != nil { return err }
defer rows.Close()
//---------------------------------
var vals []reflect.Value
switch paramId {
case 0 : vals,err = db.selectToSliceStruct4( rows,rTypes )
case 1 : vals,err = db.selectToSliceValues4( rows,rTypes )
case 2 : vals,err = db.selectToValues4( rows,rTypes )
}
for i,value := range vals { rValues[i].Set(value) }
if err != nil { return err }
return nil
}
//-----------------------------------------------------------------------------------
// selectToSlice returns a slice value fetched from rows.
func (db *DB) classify2(tableName string, args []interface{}) (column, from string, conditions []*Condition, err error) {
if len(args) == 0 {
//return columnName2(db.dialect, tableName, "*"), tableName, nil, nil
return columnName2(db.dialect, "", "*"), tableName, nil, nil
}
offset := 1
switch t := args[0].(type) {
case string:
if t != "" {
//column = columnNameAs2(db.dialect, tableName, t)
column = columnNameAs2(db.dialect, "", t)
}
case []string:
//column = db.columnsAs2(tableName, ToInterfaceSlice(t))
column = db.columnsAs2( "", ToInterfaceSlice(t))
case *Distinct:
//column = fmt.Sprintf("DISTINCT %s", db.columns2(tableName, ToInterfaceSlice(t.columns)))
column = fmt.Sprintf("DISTINCT %s", db.columns2("", ToInterfaceSlice(t.columns)))
case *Function:
var col string
if len(t.Args) == 0 {
col = "*"
} else {
//col = db.columns2( tableName, t.Args)
col = db.columns2( "", t.Args)
}
column = fmt.Sprintf("%s(%s)", t.Name, col)
default:
offset--
}
for i := offset; i < len(args); i++ {
switch t := args[i].(type) {
case *Condition:
t.tableName = tableName
conditions = append(conditions, t)
case string, []string:
return "", "", nil, fmt.Errorf("genmai:argument of %T type must be before the *Condition arguments", t)
case *From:
// ignore.
case *Function:
return "", "", nil, fmt.Errorf("genmai:%s function must be specified to the first argument", t.Name)
default:
return "", "", nil, fmt.Errorf("genmai:unsupported argument type: %T", t)
}
}
if column == "" {
//column = columnName2(db.dialect, tableName, "*")
column = columnName2(db.dialect, "", "*")
}
return column, tableName, conditions, nil
}
// columnAs2 returns the comma-separated column name with quoted.
func (db *DB) columnsAs2(tableName string, columns []interface{}) string {
if len(columns) == 0 {
return columnName2(db.dialect, tableName, "*")
}
names := make([]string, len(columns))
for i, col := range columns {
switch c := col.(type) {
case Raw:
names[i] = fmt.Sprint(*c)
case string:
names[i] = columnNameAs2(db.dialect, tableName, c)
case *Distinct:
names[i] = fmt.Sprintf("DISTINCT %s", db.columns2(tableName, ToInterfaceSlice(c.columns)))
default:
panic(fmt.Errorf("genmai:column name must be string, Raw or *Distinct, got %T", c))
}
}
return strings.Join(names, ", ")
}
// columns returns the comma-separated column name with quoted.
func (db *DB) columns2(tableName string, columns []interface{}) string {
if len(columns) == 0 {
return columnName2(db.dialect, tableName, "*")
}
names := make([]string, len(columns))
for i, col := range columns {
switch c := col.(type) {
case Raw:
names[i] = fmt.Sprint(*c)
case string:
names[i] = columnName2(db.dialect, tableName, c)
case *Distinct:
names[i] = fmt.Sprintf("DISTINCT %s", db.columns2(tableName, ToInterfaceSlice(c.columns)))
default:
panic(fmt.Errorf("genmai:column name must be string, Raw or *Distinct, got %T", c))
}
}
return strings.Join(names, ", ")
}
// columnName returns the column name that added the table name with quoted if needed.
func columnNameAs2(d Dialect, tname, cname string) string {
cname = strings.TrimSpace(cname)
strs := strings.Fields( cname )
if len(strs) == 2 {
return fmt.Sprintf("%s %s", columnName2(d,tname,strs[0]), quoteIfNot( d,strs[1]) )
}
if len(strs) == 3 && strings.ToUpper(strs[1]) == "AS" {
return fmt.Sprintf("%s AS %s", columnName2(d,tname,strs[0]), quoteIfNot( d,strs[2]) )
}
return columnName2( d,tname,cname )
}
func quoteIfNot( d Dialect, name string) string {
return d.Quote( strings.Trim( name,"'`\"") ) // firsr == last ?
} // Attention: columnQuoteDot()
func columnName2(d Dialect, tname, cname string) string {
//cname = strings.TrimSpace(cname)
cname = strings.Trim( cname,"'`\" ")
if cname == "*" {
return cname
//if tname == "" { return cname }
//return fmt.Sprintf("%s.%s", d.Quote(tname), cname)
}
strs := strings.SplitN( cname,".",2 )
if len(strs) == 2 {
strs[0] = strings.Trim( strs[0],"'`\" ")
strs[1] = strings.Trim( strs[1],"'`\" ")
if strs[1] == "*" { return quoteIfNot(d,strs[0]) + ".*" }
return quoteIfNot(d,strs[0]) + "." + quoteIfNot(d,strs[1])
}
cname = quoteIfNot(d,cname)
if tname == "" { return cname }
return fmt.Sprintf( "%s.%s", quoteIfNot(d,tname), cname)
}
//---------------------------------------------------------------------
// ReplaceQuery substiutes the string "{key}" for the value of map/struct data.
func (db *DB) ReplaceQueryKey( query string, stringQuote bool,
args ...interface{} ) (q string,err error) {
for _, arg := range args {
rVal := reflect.ValueOf(arg)
if rVal.Kind() == reflect.Ptr { rVal = rVal.Elem() }
switch rVal.Kind() {
case reflect.Struct: query,err = db.replQueryKeyStruct( query,stringQuote,rVal )
case reflect.Map: query,err = db.replQueryKeyMap( query,stringQuote,rVal )
default:
return query,fmt.Errorf( "genmai.ReplaceQueryKey: type `%v` is not supported",
reflect.TypeOf( arg ).String() )
}
if err != nil { return query,err }
}
return query,nil
}
//---------------------------------------------------------------------
func (db *DB) replQueryKeyMap( query string, strQuote bool,
rVal reflect.Value ) (q string,err error) {
for _,key := range rVal.MapKeys() {
val := rVal.MapIndex( key )
query,err = db.replQuery( query,strQuote,fmt.Sprintf( "%s",key ),
reflect.ValueOf(val.Interface()))
if err != nil { return query,err }
}
return query,err
}
//---------------------------------------------------------------------
func (db *DB) replQueryKeyStruct( query string, strQuote bool,
rVal reflect.Value ) (q string,err error) {
for i := 0; i < rVal.NumField(); i++ {
rTyp := rVal.Type()
fieldVal := rVal.Field(i)
fieldTyp := rTyp.Field(i)
switch fieldVal.Kind() {
case reflect.Struct:
if fieldTyp.Anonymous {
query,err = db.replQueryKeyStruct( query,strQuote,fieldVal )
if err != nil { return query,err }
}
case reflect.Ptr:
default:
candidate := db.columnFromTag( fieldTyp )
query,err = db.replQuery( query,strQuote,candidate,fieldVal )
//query,err = db.replQuery( query,strQuote,fieldTyp.Name,fieldVal )
if err != nil { return query,err }
}
}
return query,err
}
//---------------------------------------------------------------------
func (db *DB) replQuery( query string, strQuote bool,src string, rVal reflect.Value ) (q string,err error) {
dest := fmt.Sprintf( "%v",rVal )
switch rVal.Kind() {
case reflect.String:
if strQuote { dest = db.Quote( dest ) }
case reflect.Bool, reflect.Int,reflect.Int8,reflect.Int16,
reflect.Int32,reflect.Int64,
reflect.Uint,reflect.Uint8,reflect.Uint16,
reflect.Uint32,reflect.Uint64,
reflect.Float32,reflect.Float64:
default:
return query,nil
}
return strings.Replace( query,"{"+src+"}",dest,-1 ),nil
}
//-----------------------------------------------------------------------------------
// selectToSlice returns a slice value fetched from rows.
// elements are struct all / simple-type all
func (db *DB) selectToSliceStruct4(rows *sql.Rows,
ts []reflect.Type) ([]reflect.Value, error) {
var ptrN [255]int // maybe len(ts) < 255
columns, err := rows.Columns()
if err != nil { return []reflect.Value{}, err }
for j := 0 ; j < len(ts) ; j++ {
ts[j] = ts[j].Elem()
if ts[j].Kind() == reflect.Ptr { ptrN[j] = 1 ; ts[j] = ts[j].Elem() }
if ts[j].Kind() == reflect.Ptr {
return []reflect.Value{} , fmt.Errorf( "genmai.SelectXXX2: duplicated pointer isn't supported %v", ts[j] )
}
}
tsIndex := make( []int,len(columns) )
fieldIndexes := make([][]int, len(columns))
for i, column := range columns {
var findTs = -1
var findIndex []int
for j := 0 ; j < len(ts) ; j++ {
tmp,err := db.fieldIndexByName2(ts[j], column,"", nil)
if err != nil { return []reflect.Value{}, err }
if len(tmp) < 1 {
if ts[j].Kind() == reflect.Struct {
//strName := stringutil.ToSnakeCase(ts[j].Name())
strName := db.tableName( ts[j] ) // most outer structure's name
tmp,err = db.fieldIndexByName2(ts[j], column,strName + ".", nil)
if err != nil { return []reflect.Value{}, err }
}
}
if len(tmp) >= 1 {
if findTs >= 0 { return []reflect.Value{}, fmt.Errorf("genmai.SelectXXX2: `%v` field is ambigous", column ) }
findTs = j
findIndex = tmp
}
}
//if findTs < 0 { return []reflect.Value{}, fmt.Errorf("genmai.SelectXXX2: `%v` field isn't defined in embedded struct", stringutil.ToUpperCamelCase(column) ) }
if findTs < 0 { return []reflect.Value{}, fmt.Errorf("genmai.SelectXXX2: `%v` field isn't defined in embedded struct", column ) }
tsIndex[i] = findTs
fieldIndexes[i] = findIndex
}
dest := make([]interface{}, len(columns))
vs := make([]reflect.Value,len(ts))
result := make([][]reflect.Value,len(ts))
for j := 0 ; j < len(ts) ; j++ {
result[j] = make( []reflect.Value,0 )
}
for rows.Next() {
for j := 0 ; j < len(ts) ; j++ {
vs[j] = reflect.New(ts[j]).Elem()
}
for i, index := range fieldIndexes {
field := vs[tsIndex[i]].FieldByIndex(index)
dest[i] = field.Addr().Interface()
}
err = rows.Scan(dest...)
if err != nil { return []reflect.Value{}, err }
for j := 0 ; j < len(ts) ; j++ {
result[j] = append( result[j], vs[j] )
}
}
err = rows.Err()
if err != nil { return []reflect.Value{}, err }
slice := make([]reflect.Value,len(ts))
for j := 0 ; j < len(ts) ; j++ {
if ptrN[j] == 1 { ts[j] = reflect.PtrTo(ts[j]) }
slice[j] = reflect.MakeSlice( reflect.SliceOf(ts[j]), len(result[j]), len(result[j]))
for line, v := range result[j] {
if ptrN[j] == 1 { v = v.Addr() }
slice[j].Index(line).Set(v)
}
}
//fmt.Printf( "slice[0][0]:%v \n",slice[0].Index(0).Interface() )
//fmt.Printf( "slice[0][1]:%v \n",slice[0].Index(1).Interface() )
//fmt.Printf( "slice[0][2]:%v \n",slice[0].Index(2).Interface() )
return slice, nil
}
//-----------------------------------------------------------------------------------
// selectToSlice returns a slice value fetched from rows.
// elements are simple-type all
func (db *DB) selectToSliceValues4(rows *sql.Rows,
ts []reflect.Type) ([]reflect.Value, error) {
var ptrN [255]int // maybe len(ts) < 255
columns, err := rows.Columns()
if err != nil { return []reflect.Value{}, err }
if len(ts) != len(columns) {
return []reflect.Value{} , fmt.Errorf( "genmai.SelectXXX2: number of receive-data unmatch to column" )
}
for j := 0 ; j < len(ts) ; j++ {
ts[j] = ts[j].Elem()
if ts[j].Kind() == reflect.Ptr { ptrN[j] = 1 ; ts[j] = ts[j].Elem() }
if ts[j].Kind() == reflect.Ptr {
return []reflect.Value{} , fmt.Errorf( "genmai.SelectXXX2: duplicated pointer isn't supported %v", ts[j] )
}
}
dest := make([]interface{}, len(ts))
vs := make([]reflect.Value,len(ts))
result := make([][]reflect.Value,len(ts))
for j := 0 ; j < len(ts) ; j++ {
result[j] = make( []reflect.Value,0 )
}
for rows.Next() {
for j := 0 ; j < len(ts) ; j++ {
vs[j] = reflect.New(ts[j]).Elem()
}
for i := 0 ; i < len(ts) ; i++ {
field := vs[i]
dest[i] = field.Addr().Interface()
}
err = rows.Scan(dest...)
if err != nil { return []reflect.Value{}, err }
for j := 0 ; j < len(ts) ; j++ {
result[j] = append( result[j], vs[j] )
}
}
err = rows.Err()
if err != nil { return []reflect.Value{}, err }
slice := make([]reflect.Value,len(ts))
for j := 0 ; j < len(ts) ; j++ {
if ptrN[j] == 1 { ts[j] = reflect.PtrTo(ts[j]) }
slice[j] = reflect.MakeSlice( reflect.SliceOf(ts[j]), len(result[j]), len(result[j]))
for line, v := range result[j] {
if ptrN[j] == 1 { v = v.Addr() }
slice[j].Index(line).Set(v)
}
}
return slice, nil
}
//-----------------------------------------------------------------------------------
// selectToValues4
func (db *DB) selectToValues4( rows *sql.Rows,
ts []reflect.Type) ([]reflect.Value, error) {
var ptrN [256]int // maybe len(ts) < 256
var err error
columns, err := rows.Columns()
if err != nil { return []reflect.Value{}, err }
for j := 0 ; j < len(ts) && j < len(columns) ; j++ {
if ts[j].Kind() == reflect.Ptr { ptrN[j] = 1 ; ts[j] = ts[j].Elem() }
switch ts[j].Kind() {
case reflect.Ptr :
return []reflect.Value{} , fmt.Errorf( "genmai.SelectXXX2: duplicated pointer isn't supported %v", ts[j] )
case reflect.Struct :
return []reflect.Value{} , fmt.Errorf( "genmai.SelectXXX2: non-slice of struct isn't supported %v", ts[j] )
}
}
if len(ts) != len(columns) { // after pointer/struct checking
return []reflect.Value{} , fmt.Errorf( "genmai.SelectXXX2: number of receive-data arear unmatch to column" )
}
dest := make([]interface{}, len(ts))
vs := make([]reflect.Value,len(ts))
for rows.Next() {
for j := 0 ; j < len(ts) ; j++ {
vs[j] = reflect.New(ts[j]).Elem()
}
for i := 0 ; i < len(ts) ; i++ {
dest[i] = vs[i].Addr().Interface()
}
err = rows.Scan(dest...)
if err != nil { return []reflect.Value{}, err }
}
err = rows.Err()
if err != nil { return []reflect.Value{}, err }
slice := make([]reflect.Value,len(ts))
for j := 0 ; j < len(ts) ; j++ {
if ptrN[j] == 1 { vs[j] = vs[j].Addr() }
slice[j] = vs[j]
}
return slice, nil
}
//-----------------------------------------------------------------------------------
// fieldIndexByName2 returns the index sequence corresponding to the nested field
func (db *DB) fieldIndexByName2(t reflect.Type,
name string, pref string, index []int) ([]int,error) {
find := 0
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
candidate := db.columnFromTag( field );
if pref + candidate == name {
if find > 0 { return nil, fmt.Errorf( "genmai.SelectXXX2: `%v` receive field is ambigous",name ) }
index = append( index,i )
find++
continue
}
if field.Anonymous {
idx,err := db.fieldIndexByName2( field.Type,name,pref,append( index,i ) );
if err != nil { return nil,err }
if len(idx) > 0 {
if find > 0 { return nil, fmt.Errorf( "genmai.SelectXXX2: `%v` receive field is ambigous",name ) }
index = idx
find++
continue
}
}
}
if find > 0 { return index,nil }
return nil,nil
}
//-----------------------------------------------------------------------------------
// ColumnAlias returns the slice of AS-clause which asciates the table column and the struct field.
func (db *DB) ColumnAlias2( pref bool,args ...interface{} ) ([]string,error) {
var fieldAs []string
var name,name1,name2,strName string // stack over? pq runtime: VirtualAlloc error
var i,j,x1,x2,y1,y2 int //
var err error
fields := make([][]string,len(args))
dup := make([][]bool,len(args))
for i,arg := range args {
rv := reflect.ValueOf(arg)
if rv.Kind() == reflect.Ptr { rv = rv.Elem() }
if rv.Kind() != reflect.Struct {
return nil, fmt.Errorf( "genmai.ColumnAlias2:parameter must be a struct or a pointer of struct",rv.Type().Name() )
}
fields[i],err = db.collectTableFieldsAs2( rv.Type() )
// fmt.Println( fields )
if err != nil { return nil,err }
}
if pref == false {
//---------------- check the duplication between other struct's field
for x1,_ = range fields { dup[x1] = make([]bool,len(fields[x1])) }
for x1,_ = range fields {
for y1,name1 = range fields[x1] {
for x2 = x1+1 ; x2 < len(fields) ; x2++ {
for y2,name2 = range fields[x2] {
if name1 == name2 {
dup[x1][y1] = true
dup[x2][y2] = true
break
}
}
}
}
}
}
//------------------------------------------------------------------
// duplicated : "table"."column" AS "table.column"
// not duplicated : "column" AS "column"
//------------------------------------------------------------------
for i,_ = range fields {
for j,name = range fields[i] {
if pref == true || dup[i][j] {
rv := reflect.ValueOf(args[i])
if rv.Kind() == reflect.Ptr { rv = rv.Elem() }
t := rv.Type()
//strName := stringutil.ToSnakeCase(t.Name())
strName = db.tableName( t )
name2 = db.Quote(strName + "." + name)
name1 = db.Quote(strName) + "." + db.Quote(name)
name = name1 + " AS " + name2
} else {
//name = name + " AS " + db.Quote(name)
name = columnName2(db.dialect,"",name) + " AS " + db.Quote(name)
}
fieldAs = append( fieldAs, name )
}
}
return fieldAs,nil
}
//-------------------------------------------------------------------------------
func (db *DB) collectTableFieldsAs2(t reflect.Type) (fields []string, err error) {
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if IsUnexportedField(field) { continue }
if db.hasSkipTag(&field) { continue }
if field.Anonymous {
fs, err := db.collectTableFieldsAs2(field.Type)
if err != nil { return nil, err }
fields = append(fields, fs...)
continue
}
t := field.Type
if t.Kind() == reflect.Struct {
switch t.String() {
case "sql.NullString","sql.NullBool","sql.NullInt64","sql.NullFloat64":
case "time.Time":
default:
continue
}
}
fields = append( fields,db.columnFromTag(field) )
//fields = append( fields,db.dialect.Quote(db.columnFromTag(field)) )
}
return fields, nil
}
naoinaさん、woremacxさん、ありがとうございます。