diff --git a/Makefile b/Makefile index edf771c1..3a27ae19 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ include $(GOROOT)/src/Make.inc -TARG = github.com/coopernurse/gorp +TARG = github.com/go-gorp/gorp GOFILES = gorp.go dialect.go include $(GOROOT)/src/Make.pkg \ No newline at end of file diff --git a/README.md b/README.md index 81f93be6..7f66fc26 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,18 @@ -# Go Relational Persistence # +# Go Relational Persistence [![build status](https://secure.travis-ci.org/go-gorp/gorp.png)](http://travis-ci.org/go-gorp/gorp) -## Call for Maintainers - -Dec 7 2014 - -I've been slacking on keeping up with issues and PRs. I know there are -many of you out there who have active forks and some good ideas on how to -push the project forward. If you'd like to step up and either be added as -a committer to this repo, or would like to become the new canonical fork -of the project, please comment on #211 here: - -https://github.com/go-gorp/gorp/issues/211 - -Thank you! - - ----- - -I hesitate to call gorp an ORM. Go doesn't really have objects, at least -not in the classic Smalltalk/Java sense. There goes the "O". gorp doesn't -know anything about the relationships between your structs (at least not -yet). So the "R" is questionable too (but I use it in the name because, +I hesitate to call gorp an ORM. Go doesn't really have objects, at least +not in the classic Smalltalk/Java sense. There goes the "O". gorp doesn't +know anything about the relationships between your structs (at least not +yet). So the "R" is questionable too (but I use it in the name because, well, it seemed more clever). The "M" is alive and well. Given some Go structs and a database, gorp should remove a fair amount of boilerplate busy-work from your code. -I hope that gorp saves you time, minimizes the drudgery of getting data -in and out of your database, and helps your code focus on algorithms, +I hope that gorp saves you time, minimizes the drudgery of getting data +in and out of your database, and helps your code focus on algorithms, not infrastructure. * Bind struct fields to table columns via API or tag @@ -47,21 +30,33 @@ not infrastructure. * Use positional or named bind parameters in custom SELECT queries * Optional optimistic locking using a version column (for update/deletes) -## Installation ## +## Installation # install the library: - go get github.com/go-gorp/gorp + go get gopkg.in/gorp.v1 // use in your .go code: import ( - "github.com/go-gorp/gorp" + "gopkg.in/gorp.v1" ) -## API Documentation ## +## Versioning + +This project provides a stable release (v1.x tags) and a bleeding edge codebase (master). + +`gopkg.in/gorp.v1` points to the latest v1.x tag. The API's for v1 are stable and shouldn't change. Development takes place at the master branch. Althought the code in master should always compile and test successfully, it might break API's. We aim to maintain backwards compatibility, but API's and behaviour might be changed to fix a bug. Also note that API's that are new in the master branch can change until released as v2. -Full godoc output from the latest code in master is available here: +If you want to use bleeding edge, use `github.com/go-gorp/gorp` as import path. -http://godoc.org/github.com/go-gorp/gorp +## API Documentation + +Full godoc output from the latest v1 release is available here: + +https://godoc.org/gopkg.in/gorp.v1 + +For the latest code in master: + +https://godoc.org/github.com/go-gorp/gorp ## Quickstart @@ -70,7 +65,7 @@ package main import ( "database/sql" - "github.com/go-gorp/gorp" + "gopkg.in/gorp.v1" _ "github.com/mattn/go-sqlite3" "log" "time" @@ -183,9 +178,9 @@ func checkErr(err error, msg string) { } ``` -## Examples ## +## Examples -### Mapping structs to tables ### +### Mapping structs to tables First define some types: @@ -248,7 +243,7 @@ t2 := dbmap.AddTableWithName(Person{}, "person_test").SetKeys(true, "Id") t3 := dbmap.AddTableWithName(Product{}, "product_test").SetKeys(true, "Id") ``` -### Struct Embedding ### +### Struct Embedding gorp supports embedding structs. For example: @@ -287,7 +282,7 @@ dbmap.CreateTablesIfNotExists() dbmap.DropTables() ``` -### SQL Logging ### +### SQL Logging Optionally you can pass in a logger to trace all SQL statements. I recommend enabling this initially while you're getting the feel for what @@ -306,7 +301,7 @@ dbmap.TraceOn("[gorp]", log.New(os.Stdout, "myapp:", log.Lmicroseconds)) dbmap.TraceOff() ``` -### Insert ### +### Insert ```go // Must declare as pointers so optional callback hooks @@ -322,7 +317,7 @@ err := dbmap.Insert(inv1, inv2) fmt.Printf("inv1.Id=%d inv2.Id=%d\n", inv1.Id, inv2.Id) ``` -### Update ### +### Update Continuing the above example, use the `Update` method to modify an Invoice: @@ -331,7 +326,7 @@ Continuing the above example, use the `Update` method to modify an Invoice: count, err := dbmap.Update(inv1) ``` -### Delete ### +### Delete If you have primary key(s) defined for a struct, you can use the `Delete` method to remove rows: @@ -340,7 +335,7 @@ method to remove rows: count, err := dbmap.Delete(inv1) ``` -### Select by Key ### +### Select by Key Use the `Get` method to fetch a single row by primary key. It returns nil if no row is found. @@ -351,9 +346,9 @@ obj, err := dbmap.Get(Invoice{}, 99) inv := obj.(*Invoice) ``` -### Ad Hoc SQL ### +### Ad Hoc SQL -#### SELECT #### +#### SELECT `Select()` and `SelectOne()` provide a simple way to bind arbitrary queries to a slice or a single struct. @@ -413,7 +408,7 @@ if reflect.DeepEqual(list[0], expected) { } ``` -#### SELECT string or int64 #### +#### SELECT string or int64 gorp provides a few convenience methods for selecting a single string or int64. @@ -426,7 +421,7 @@ s, err := dbmap.SelectStr("select name from foo where blah=?", blahVal) ``` -#### Named bind parameters #### +#### Named bind parameters You may use a map or struct to bind parameters by name. This is currently only supported in SELECT queries. @@ -438,7 +433,7 @@ _, err := dbm.Select(&dest, "select * from Foo where name = :name and age = :age }) ``` -#### UPDATE / DELETE #### +#### UPDATE / DELETE You can execute raw SQL if you wish. Particularly good for batch operations. @@ -446,7 +441,7 @@ You can execute raw SQL if you wish. Particularly good for batch operations. res, err := dbmap.Exec("delete from invoice_test where PersonId=?", 10) ``` -### Transactions ### +### Transactions You can batch operations into a transaction: @@ -467,7 +462,7 @@ func InsertInv(dbmap *DbMap, inv *Invoice, per *Person) error { } ``` -### Hooks ### +### Hooks Use hooks to update data before/after saving to the db. Good for timestamps: @@ -512,7 +507,9 @@ Full list of hooks that you can implement: func (p *MyStruct) PostUpdate(s gorp.SqlExecutor) error -### Optimistic Locking ### +### Optimistic Locking + +#### Note that this behaviour has changed in v2. See [Migration Guide](#migration-guide). gorp provides a simple optimistic locking feature, similar to Java's JPA, that will raise an error if you try to update/delete a row whose `version` column @@ -565,7 +562,7 @@ if ok { } ``` -## Database Drivers ## +## Database Drivers gorp uses the Go 1 `database/sql` package. A full list of compliant drivers is available here: @@ -590,9 +587,9 @@ Note that these databases are not covered by CI and I (@coopernurse) have no goo test them locally. So please try them and send patches as needed, but expect a bit more unpredicability. -## Known Issues ## +## Known Issues -### SQL placeholder portability ### +### SQL placeholder portability Different databases use different strings to indicate variable placeholders in prepared SQL statements. Unlike some database abstraction layers (such as JDBC), @@ -617,7 +614,7 @@ err := dbmap.SelectOne(&val, "select * from foo where id = :id", map[string]interface{} { "id": 30}) ``` -### time.Time and time zones ### +### time.Time and time zones gorp will pass `time.Time` fields through to the `database/sql` driver, but note that the behavior of this type varies across database drivers. @@ -627,7 +624,7 @@ MySQL users should be especially cautious. See: https://github.com/ziutek/mymys To avoid any potential issues with timezone/DST, consider using an integer field for time data and storing UNIX time. -## Running the tests ## +## Running the tests The included tests may be run against MySQL, Postgresql, or sqlite3. You must set two environment variables so the test code knows which driver to @@ -649,19 +646,28 @@ Valid `GORP_TEST_DIALECT` values are: "mysql", "postgres", "sqlite3" See the `test_all.sh` script for examples of all 3 databases. This is the script I run locally to test the library. -## Performance ## +## Performance + +gorp uses reflection to construct SQL queries and bind parameters. See the BenchmarkNativeCrud vs BenchmarkGorpCrud in gorp_test.go for a simple perf test. On my MacBook Pro gorp is about 2-3% slower than hand written SQL. + +## Migration guide +#### Pre-v2 to v2 +Automatic mapping of the version column used in optimistic locking has been removed as it could cause problems if the type was not int. The version column must now explicitly be set with tablemap.SetVersionCol(). + +## Help/Support -gorp uses reflection to construct SQL queries and bind parameters. See the BenchmarkNativeCrud vs BenchmarkGorpCrud in gorp_test.go for a simple perf test. On my MacBook Pro gorp is about 2-3% slower than hand written SQL. +IRC: #gorp +Mailing list: gorp-dev@googlegroups.com +Bugs/Enhancements: Create a github issue ## Pull requests / Contributions Contributions are very welcome. Please follow these guidelines: -* Fork the `develop` branch and issue pull requests targeting the `develop` branch - * If you don't do this, I'll likely cherry pick your commit into develop -* If you are adding an enhancement, please open an issue first with your proposed change. +* Fork the `master` branch and issue pull requests targeting the `master` branch +* If you are adding an enhancement, please open an issue first with your proposed change. * Changes that break backwards compatibility in the public API are only accepted after we - discuss on a GitHub issue for a while. + discuss on a GitHub issue for a while. Thanks! diff --git a/gorp.go b/gorp.go index 1b7babea..a4821e00 100644 --- a/gorp.go +++ b/gorp.go @@ -7,7 +7,7 @@ // compliant database/sql driver. // // Source code and project home: -// https://github.com/coopernurse/gorp +// https://github.com/go-gorp/gorp // package gorp @@ -21,6 +21,8 @@ import ( "regexp" "strings" "time" + "log" + "os" ) // Oracle String (empty string is null) @@ -614,6 +616,12 @@ type Transaction struct { closed bool } +// Executor exposes the sql.DB and sql.Tx Exec function so that it can be used +// on internal functions that convert named parameters for the Exec function. +type executor interface { + Exec(query string, args ...interface{}) (sql.Result, error) +} + // SqlExecutor exposes gorp operations that can be run from Pre/Post // hooks. This hides whether the current operation that triggered the // hook is in a transaction. @@ -709,19 +717,19 @@ func (m *DbMap) AddTableWithNameAndSchema(i interface{}, schema string, name str } tmap := &TableMap{gotype: t, TableName: name, SchemaName: schema, dbmap: m} - tmap.Columns, tmap.version = m.readStructColumns(t) + tmap.Columns = m.readStructColumns(t) m.tables = append(m.tables, tmap) return tmap } -func (m *DbMap) readStructColumns(t reflect.Type) (cols []*ColumnMap, version *ColumnMap) { +func (m *DbMap) readStructColumns(t reflect.Type) (cols []*ColumnMap) { n := t.NumField() for i := 0; i < n; i++ { f := t.Field(i) if f.Anonymous && f.Type.Kind() == reflect.Struct { // Recursively add nested fields in embedded structs. - subcols, subversion := m.readStructColumns(f.Type) + subcols := m.readStructColumns(f.Type) // Don't append nested fields that have the same field // name as an already-mapped field. for _, subcol := range subcols { @@ -736,9 +744,6 @@ func (m *DbMap) readStructColumns(t reflect.Type) (cols []*ColumnMap, version *C cols = append(cols, subcol) } } - if subversion != nil { - version = subversion - } } else { columnName := f.Tag.Get("db") if columnName == "" { @@ -775,9 +780,6 @@ func (m *DbMap) readStructColumns(t reflect.Type) (cols []*ColumnMap, version *C if shouldAppend { cols = append(cols, cm) } - if cm.fieldName == "Version" { - version = cm - } } } return @@ -1044,7 +1046,7 @@ func (m *DbMap) Select(i interface{}, query string, args ...interface{}) ([]inte // This is equivalent to running: Exec() using database/sql func (m *DbMap) Exec(query string, args ...interface{}) (sql.Result, error) { m.trace(query, args...) - return m.Db.Exec(query, args...) + return exec(m, query, args...) } // SelectInt is a convenience wrapper around the gorp.SelectInt function @@ -1216,7 +1218,7 @@ func (t *Transaction) Select(i interface{}, query string, args ...interface{}) ( // Exec has the same behavior as DbMap.Exec(), but runs in a transaction. func (t *Transaction) Exec(query string, args ...interface{}) (sql.Result, error) { t.dbmap.trace(query, args...) - return t.tx.Exec(query, args...) + return exec(t, query, args...) } // SelectInt is a convenience wrapper around the gorp.SelectInt function. @@ -1658,6 +1660,27 @@ func rawselect(m *DbMap, exec SqlExecutor, i interface{}, query string, return list, nonFatalErr } +// Calls the Exec function on the executor, but attempts to expand any eligible named +// query arguments first. +func exec(e SqlExecutor, query string, args ...interface{}) (sql.Result, error) { + var dbMap *DbMap + var executor executor + switch m := e.(type) { + case *DbMap: + executor = m.Db + dbMap = m + case *Transaction: + executor = m.tx + dbMap = m.dbmap + } + + if len(args) == 1 { + query, args = maybeExpandNamedQuery(dbMap, query, args) + } + + return executor.Exec(query, args...) +} + // maybeExpandNamedQuery checks the given arg to see if it's eligible to be used // as input to a named query. If so, it rewrites the query to use // dialect-dependent bindvars and instantiates the corresponding slice of diff --git a/gorp_test.go b/gorp_test.go index 55182371..d4678e2f 100644 --- a/gorp_test.go +++ b/gorp_test.go @@ -63,7 +63,7 @@ func (me *InvoiceTag) Rand() { me.Updated = rand.Int63() } -// See: https://github.com/coopernurse/gorp/issues/175 +// See: https://github.com/go-gorp/gorp/issues/175 type AliasTransientField struct { Id int64 `db:"id"` Bar int64 `db:"-"` @@ -655,6 +655,19 @@ select * from PersistentUser if len(puArr) != 1 { t.Errorf("Expected one persistentuser, found none") } + + // Test to delete with Exec and named params. + result, err := dbmap.Exec("delete from PersistentUser where mykey = :Key", map[string]interface{}{ + "Key": 43, + }) + count, err := result.RowsAffected() + if err != nil { + t.Errorf("Failed to exec: %s", err) + t.FailNow() + } + if count != 1 { + t.Errorf("Expected 1 persistentuser to be deleted, but %d deleted", count) + } } func TestNamedQueryStruct(t *testing.T) { @@ -692,6 +705,21 @@ select * from PersistentUser if !reflect.DeepEqual(pu, puArr[0]) { t.Errorf("%v!=%v", pu, puArr[0]) } + + // Test delete self. + result, err := dbmap.Exec(` +delete from PersistentUser + where mykey = :Key + and PassedTraining = :PassedTraining + and Id = :Id`, pu) + count, err := result.RowsAffected() + if err != nil { + t.Errorf("Failed to exec: %s", err) + t.FailNow() + } + if count != 1 { + t.Errorf("Expected 1 persistentuser to be deleted, but %d deleted", count) + } } // Ensure that the slices containing SQL results are non-nil when the result set is empty. @@ -1402,7 +1430,7 @@ func testWithTime(t *testing.T) { } } -// See: https://github.com/coopernurse/gorp/issues/86 +// See: https://github.com/go-gorp/gorp/issues/86 func testEmbeddedTime(t *testing.T) { dbmap := newDbMap() dbmap.TraceOn("", log.New(os.Stdout, "gorptest: ", log.Lmicroseconds)) @@ -1904,7 +1932,7 @@ func initDbMap() *DbMap { dbmap.AddTableWithName(InvoiceTag{}, "invoice_tag_test").SetKeys(true, "myid") dbmap.AddTableWithName(AliasTransientField{}, "alias_trans_field_test").SetKeys(true, "id") dbmap.AddTableWithName(OverriddenInvoice{}, "invoice_override_test").SetKeys(false, "Id") - dbmap.AddTableWithName(Person{}, "person_test").SetKeys(true, "Id") + dbmap.AddTableWithName(Person{}, "person_test").SetKeys(true, "Id").SetVersionCol("Version") dbmap.AddTableWithName(WithIgnoredColumn{}, "ignored_column_test").SetKeys(true, "Id") dbmap.AddTableWithName(IdCreated{}, "id_created_test").SetKeys(true, "Id") dbmap.AddTableWithName(TypeConversionExample{}, "type_conv_test").SetKeys(true, "Id")