gorp 是一個Go開源ORM框架.
Go關系型數據庫持久化
我很含糊的稱gorp是一個ORM框架. Go還沒有真正的對象, 至少沒有一個經典的感覺和Smalltalk/Java一樣的,這是"O". gorp不知道任何你struct之間的關系(以后會實現,現在還在TODO list中). 所以"R"也是有質疑的(但是我使用這個名字,因為感覺上更貼切).
"M"是沒有問題的. 給定一些Go結構和一個數據庫, gorp應該可以減少你的重復代碼.
我希望gorp能節省你的時間並簡化從數據庫獲取數據的苦活來幫助你把精力投入到算法上而不是基礎設施上面.
數據庫驅動
gorp使用Go1 database/sql包. 一個完整可用的兼容驅動程序如下:
http://code.google.com/p/go-wiki/wiki/SQLDrivers
遺憾的是SQL數據庫很多不一樣的問題. gorp提供一個應該被每個數據庫廠商實現的Dialect接口, Dialect支持如下數據庫:
- MySQL
- PostgreSQL
- sqlite3
這三個數據庫都通過了測試, 請查看gorp_test.go, 例如針對這三個數據庫的DSN.
特點
- 通過API或tag綁定struct字段到表的列
- 支持事務
- 從struct建立db架構正向工程(來做更好的單元測試)
- 在insert/update/delete的前后提供hook
- 自動為struct生成generate insert/update/delete語句
- 在insert后自動綁定自增主鍵到struct
- 通過主鍵刪除
- 通過主鍵選擇
- 可選的SQL跟蹤日志
- 綁定任意SQL查詢到struct
- 可通過一個version column來為update和delete實現樂觀鎖
待辦事項
支持內嵌struct
安裝
# install the library: go get github.com/coopernurse/gorp // use in your .go code: import ( "github.com/coopernurse/gorp" )
運行測試
現在測試測試包括了MySQL.我需要為測試工具添加額外的驅動, 但是現在你可以克隆repo並設置一個環境變量來為運行"go test"做准備
# Set env variable with dsn using mymysql format. From the mymysql docs, # the format can be of 3 types: # # DBNAME/USER/PASSWD # unix:SOCKPATH*DBNAME/USER/PASSWD # tcp:ADDR*DBNAME/USER/PASSWD # # for example, on my box I use: export GORP_TEST_DSN=gomysql_test/gomysql_test/abc123 # run the tests go test # run the tests and benchmarks go test -bench="Bench" -benchtime 10
性能
gorp使用反射來構造SQL查詢和綁定參數. 在gorp_test.go中查看BenchmarkNativeCrud 對 BenchmarkGorpCrud的一個簡單的性能測試. 在我的MacBook Pro上它比手寫SQL慢大約2%-3%.
示例
首先定義一些類型:
type Invoice struct { Id int64 Created int64 Updated int64 Memo string PersonId int64 } type Person struct { Id int64 Created int64 Updated int64 FName string LName string } // Example of using tags to alias fields to column names // The 'db' value is the column name // // A hyphen will cause gorp to skip this field, similar to the // Go json package. // // This is equivalent to using the ColMap methods: // // table := dbmap.AddTableWithName(Product{}, "product") // table.ColMap("Id").Rename("product_id") // table.ColMap("Price").Rename("unit_price") // table.ColMap("IgnoreMe").SetTransient(true) // type Product struct { Id int64 `db:"product_id"` Price int64 `db:"unit_price"` IgnoreMe string `db:"-"` }
然后創建一個映射器, 一般你再app啟動時做一次.
// connect to db using standard Go database/sql API // use whatever database/sql driver you wish db, err := sql.Open("mymysql", "tcp:localhost:3306*mydb/myuser/mypassword") // construct a gorp DbMap dbmap := &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{"InnoDB", "UTF8"}} // register the structs you wish to use with gorp // you can also use the shorter dbmap.AddTable() if you // don't want to override the table name // // SetKeys(true) means we have a auto increment primary key, which // will get automatically bound to your struct post-insert // t1 := dbmap.AddTableWithName(Invoice{}, "invoice_test").SetKeys(true, "Id") t2 := dbmap.AddTableWithName(Person{}, "person_test").SetKeys(true, "Id") t3 := dbmap.AddTableWithName(Product{}, "product_test").SetKeys(true, "Id")
自動創建刪除已注冊的表
// create all registered tables dbmap.CreateTables() // drop dbmap.DropTables()
可選項:你可以傳入一個log.Logger來跟蹤全部的SQL語句:
// Will log all SQL statements + args as they are run // The first arg is a string prefix to prepend to all log messages dbmap.TraceOn("[gorp]", log.New(os.Stdout, "myapp:", log.Lmicroseconds)) // Turn off tracing dbmap.TraceOff()
然后保存一些數據:
// Must declare as pointers so optional callback hooks // can operate on your data, not copies inv1 := &Invoice{0, 100, 200, "first order", 0} inv2 := &Invoice{0, 100, 200, "second order", 0} // Insert your rows err := dbmap.Insert(inv1, inv2) // Because we called SetKeys(true) on Invoice, the Id field // will be populated after the Insert() automatically fmt.Printf("inv1.Id=%d inv2.Id=%d\n", inv1.Id, inv2.Id)
你可以執行原始的SQL語句.尤其是批量操作時.
res, err := dbmap.Exec("delete from invoice_test where PersonId=?", 10)
想要做join? 只寫SQL和struct, gorp將綁定他們:
// Define a type for your join // It *must* contain all the columns in your SELECT statement // // The names here should match the aliased column names you specify // in your SQL - no additional binding work required. simple. // type InvoicePersonView struct { InvoiceId int64 PersonId int64 Memo string FName string } // Create some rows p1 := &Person{0, 0, 0, "bob", "smith"} dbmap.Insert(p1) // notice how we can wire up p1.Id to the invoice easily inv1 := &Invoice{0, 0, 0, "xmas order", p1.Id} dbmap.Insert(inv1) // Run your query query := "select i.Id InvoiceId, p.Id PersonId, i.Memo, p.FName " + "from invoice_test i, person_test p " + "where i.PersonId = p.Id" list, err := dbmap.Select(InvoicePersonView{}, query) // this should test true expected := &InvoicePersonView{inv1.Id, p1.Id, inv1.Memo, p1.FName} if reflect.DeepEqual(list[0], expected) { fmt.Println("Woot! My join worked!") }
你也可以在一個事務中批量操作:
func InsertInv(dbmap *DbMap, inv *Invoice, per *Person) error { // Start a new transaction trans := dbmap.Begin() trans.Insert(per) inv.PersonId = per.Id trans.Insert(inv) // if the commit is successful, a nil error is returned return trans.Commit() }
在更新數據到數據庫(前/后)使用hook, 對時間戳很有效:
// implement the PreInsert and PreUpdate hooks func (i *Invoice) PreInsert(s gorp.SqlExecutor) error { i.Created = time.Now().UnixNano() i.Updated = i.Created return nil } func (i *Invoice) PreUpdate(s gorp.SqlExecutor) error { i.Updated = time.Now().UnixNano() return nil } // You can use the SqlExecutor to cascade additional SQL // Take care to avoid cycles. gorp won't prevent them. // // Here's an example of a cascading delete // func (p *Person) PreDelete(s gorp.SqlExecutor) error { query := "delete from invoice_test where PersonId=?" err := s.Exec(query, p.Id); if err != nil { return err } return nil }
你可以實現以下的hook
PostGet PreInsert PostInsert PreUpdate PostUpdate PreDelete PostDelete All have the same signature. for example: func (p *MyStruct) PostUpdate(s gorp.SqlExecutor) error
樂觀鎖 (有點像JPA)
// Version is an auto-incremented number, managed by gorp // If this property is present on your struct, update // operations will be constrained // // For example, say we defined Person as: type Person struct { Id int64 Created int64 Updated int64 FName string LName string // automatically used as the Version col // use table.SetVersionCol("columnName") to map a different // struct field as the version field Version int64 } p1 := &Person{0, 0, 0, "Bob", "Smith", 0} dbmap.Insert(p1) // Version is now 1 obj, err := dbmap.Get(Person{}, p1.Id) p2 := obj.(*Person) p2.LName = "Edwards" dbmap.Update(p2) // Version is now 2 p1.LName = "Howard" // Raises error because p1.Version == 1, which is out of date count, err := dbmap.Update(p1) _, ok := err.(gorp.OptimisticLockError) if ok { // should reach this statement // in a real app you might reload the row and retry, or // you might propegate this to the user, depending on the desired // semantics fmt.Printf("Tried to update row with stale data: %v\n", err) } else { // some other db error occurred - log or return up the stack fmt.Printf("Unknown db err: %v\n", err) }
至此結束.