diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2026-04-17 22:46:09 +0200 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2026-04-17 22:46:28 +0200 |
| commit | c1c8cc2f6b49741d33a83a534d244fc01b0e1b4a (patch) | |
| tree | 4ec9d8b15a4f528fbf3e64060a19d68fe04a2a48 | |
| parent | ba4f55e75e6b6ebc3ecfc744d94377e6f3417693 (diff) | |
| download | go-oblast-c1c8cc2f6b49741d33a83a534d244fc01b0e1b4a.tar.gz | |
benchmark: add Gorm
| -rw-r--r-- | benchmark/benchmark_test.go | 136 | ||||
| -rw-r--r-- | benchmark/go.mod | 10 | ||||
| -rw-r--r-- | benchmark/go.sum | 14 | ||||
| -rw-r--r-- | go.work.sum | 3 |
4 files changed, 126 insertions, 37 deletions
diff --git a/benchmark/benchmark_test.go b/benchmark/benchmark_test.go index 83832ba..c2d27b3 100644 --- a/benchmark/benchmark_test.go +++ b/benchmark/benchmark_test.go @@ -15,6 +15,8 @@ import ( "go.xyrillian.de/oblast" "go.xyrillian.de/oblast/internal/assert" "go.xyrillian.de/oblast/internal/must" + "gorm.io/driver/sqlite" + "gorm.io/gorm" ) var ( @@ -24,8 +26,9 @@ var ( batchSizesForUpdate = []int{1, 2, 4, 8, 16, 100} ) -func makeTestDB(t testing.TB, recordCount int) *sql.DB { - db := must.Return(sql.Open("sqlite3", fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())))(t) +func makeTestDB(t testing.TB, recordCount int) (db *sql.DB, dsn string) { + dsn = fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) + db = must.Return(sql.Open("sqlite3", dsn))(t) _ = must.Return(db.Exec(`CREATE TABLE entries (id INTEGER, message TEXT, PRIMARY KEY (id AUTOINCREMENT))`))(t) if recordCount > 0 { @@ -38,7 +41,7 @@ func makeTestDB(t testing.TB, recordCount int) *sql.DB { must.Succeed(t, stmt.Close()) } - return db + return db, dsn } type OblastEntry struct { @@ -51,23 +54,28 @@ type GorpEntry struct { Message string `db:"message"` } +type GormEntry struct { + ID int `gorm:"primaryKey"` + Message string +} + +func (GormEntry) TableName() string { return "entries" } + func BenchmarkSelectMany(b *testing.B) { - db := makeTestDB(b, totalRecordCountForSelect) + db, dsn := makeTestDB(b, totalRecordCountForSelect) // test with different sizes of resultsets (N=1 is an OLTP-like workload, // then the larger N lean more towards the OLAP side of things) for _, batchSize := range batchSizesForSelect { b.Run("N="+strconv.Itoa(batchSize), func(b *testing.B) { // prepare the functions that will be benched - store, err := oblast.NewStore[OblastEntry]( + store := oblast.MustNewStore[OblastEntry]( oblast.SqliteDialect(), oblast.TableNameIs("entries"), oblast.PrimaryKeyIs("id"), ) - if err != nil { - b.Fatal(err) - } - gdb := gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}} + gorpDB := gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}} + gormDB := must.Return(gorm.Open(sqlite.Open(dsn), &gorm.Config{}))(b) partialQuery := `id < ` + strconv.Itoa(batchSize) query := `SELECT * FROM entries WHERE ` + partialQuery @@ -83,7 +91,12 @@ func BenchmarkSelectMany(b *testing.B) { selectWithGorp := func(b *testing.B) { var records []GorpEntry - _ = must.Return(gdb.Select(&records, query))(b) + _ = must.Return(gorpDB.Select(&records, query))(b) + assert.Equal(b, len(records), batchSize) + } + + selectWithGorm := func(b *testing.B) { + records := must.Return(gorm.G[GormEntry](gormDB).Where(partialQuery).Find(b.Context()))(b) assert.Equal(b, len(records), batchSize) } @@ -107,11 +120,17 @@ func BenchmarkSelectMany(b *testing.B) { // run once to prewarm caches (if any) selectWithOblast(b) selectWithGorp(b) + selectWithGorm(b) if b.Failed() { b.FailNow() } // run actual benchmark + b.Run("via Gorm using Find", func(b *testing.B) { + for b.Loop() { + selectWithGorp(b) + } + }) b.Run("via Gorp using Select", func(b *testing.B) { for b.Loop() { selectWithGorp(b) @@ -137,21 +156,19 @@ func BenchmarkSelectMany(b *testing.B) { } func BenchmarkSelectOne(b *testing.B) { - db := makeTestDB(b, totalRecordCountForSelect) + db, dsn := makeTestDB(b, totalRecordCountForSelect) // grab a "random" record from the DB, not just the first or the last recordID := min(totalRecordCountForSelect*2/3, totalRecordCountForSelect) // prepare the functions that will be benched - store, err := oblast.NewStore[OblastEntry]( + store := oblast.MustNewStore[OblastEntry]( oblast.SqliteDialect(), oblast.TableNameIs("entries"), oblast.PrimaryKeyIs("id"), ) - if err != nil { - b.Fatal(err) - } - gdb := gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}} + gorpDB := gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}} + gormDB := must.Return(gorm.Open(sqlite.Open(dsn), &gorm.Config{}))(b) partialQuery := `id = ` + strconv.Itoa(recordID) query := `SELECT * FROM entries WHERE ` + partialQuery @@ -167,7 +184,12 @@ func BenchmarkSelectOne(b *testing.B) { selectWithGorp := func(b *testing.B) { var r GorpEntry - must.Succeed(b, gdb.SelectOne(&r, query)) + must.Succeed(b, gorpDB.SelectOne(&r, query)) + assert.Equal(b, r.ID, recordID) + } + + selectWithGorm := func(b *testing.B) { + r := must.Return(gorm.G[GormEntry](gormDB).Where(partialQuery).First(b.Context()))(b) assert.Equal(b, r.ID, recordID) } @@ -183,11 +205,17 @@ func BenchmarkSelectOne(b *testing.B) { // run once to prewarm caches (if any) selectWithOblast(b) selectWithGorp(b) + selectWithGorm(b) if b.Failed() { b.FailNow() } // run actual benchmark + b.Run("via Gorm using First", func(b *testing.B) { + for b.Loop() { + selectWithGorm(b) + } + }) b.Run("via Gorp using SelectOne", func(b *testing.B) { for b.Loop() { selectWithGorp(b) @@ -211,18 +239,16 @@ func BenchmarkSelectOne(b *testing.B) { } func BenchmarkInsertAndDelete(b *testing.B) { - db := makeTestDB(b, 0) + db, dsn := makeTestDB(b, 0) - store, err := oblast.NewStore[OblastEntry]( + store := oblast.MustNewStore[OblastEntry]( oblast.SqliteDialect(), oblast.TableNameIs("entries"), oblast.PrimaryKeyIs("id"), ) - if err != nil { - b.Fatal(err) - } - gdb := gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}} - gdb.AddTableWithName(GorpEntry{}, "entries").SetKeys(true, "id") + gorpDB := gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}} + gorpDB.AddTableWithName(GorpEntry{}, "entries").SetKeys(true, "id") + gormDB := must.Return(gorm.Open(sqlite.Open(dsn), &gorm.Config{}))(b) // test with different amounts of records for _, batchSize := range batchSizesForInsertDelete { @@ -247,13 +273,29 @@ func BenchmarkInsertAndDelete(b *testing.B) { for idx := range records { records[idx] = &GorpEntry{Message: "hello"} } - must.Succeed(b, gdb.Insert(records...)) + must.Succeed(b, gorpDB.Insert(records...)) for _, r := range records { if r.(*GorpEntry).ID == 0 { b.Errorf("ID was not filled!") } } - _ = must.Return(gdb.Delete(records...))(b) + _ = must.Return(gorpDB.Delete(records...))(b) + } + + insertAndDeleteWithGorm := func(b *testing.B) { + records := make([]GormEntry, batchSize) + for idx := range records { + records[idx] = GormEntry{Message: "hello"} + } + must.Succeed(b, gorm.G[GormEntry](gormDB).CreateInBatches(b.Context(), &records, batchSize)) + for _, r := range records { + if r.ID == 0 { + b.Errorf("ID was not filled!") + } + } + result := gormDB.Delete(&records) + assert.ErrEqual(b, result.Error, "<success>") + assert.Equal(b, result.RowsAffected, int64(batchSize)) } insertAndDeleteWithStraightSqlite := func(b *testing.B) { @@ -285,7 +327,13 @@ func BenchmarkInsertAndDelete(b *testing.B) { // run once to prewarm caches (if any) insertAndDeleteWithOblast(b) insertAndDeleteWithGorp(b) + insertAndDeleteWithGorm(b) + b.Run("via Gorm", func(b *testing.B) { + for b.Loop() { + insertAndDeleteWithGorm(b) + } + }) b.Run("via Gorp", func(b *testing.B) { for b.Loop() { insertAndDeleteWithGorp(b) @@ -311,18 +359,16 @@ func BenchmarkInsertAndDelete(b *testing.B) { } func BenchmarkUpdate(b *testing.B) { - db := makeTestDB(b, 0) + db, dsn := makeTestDB(b, 0) - store, err := oblast.NewStore[OblastEntry]( + store := oblast.MustNewStore[OblastEntry]( oblast.SqliteDialect(), oblast.TableNameIs("entries"), oblast.PrimaryKeyIs("id"), ) - if err != nil { - b.Fatal(err) - } - gdb := gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}} - gdb.AddTableWithName(GorpEntry{}, "entries").SetKeys(true, "id") + gorpDB := gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}} + gorpDB.AddTableWithName(GorpEntry{}, "entries").SetKeys(true, "id") + gormDB := must.Return(gorm.Open(sqlite.Open(dsn), &gorm.Config{}))(b) // test with different amounts of records for _, batchSize := range batchSizesForUpdate { @@ -338,6 +384,10 @@ func BenchmarkUpdate(b *testing.B) { for idx, r := range recordsForOblast { recordsForGorp[idx] = new(GorpEntry(r)) } + recordsForGorm := make([]GormEntry, batchSize) + for idx, r := range recordsForOblast { + recordsForGorm[idx] = GormEntry(r) + } // prepare the functions that will be benched updateWithOblast := func(b *testing.B, message string) { @@ -350,7 +400,15 @@ func BenchmarkUpdate(b *testing.B) { for _, r := range recordsForGorp { r.(*GorpEntry).Message = message } - _ = must.Return(gdb.Update(recordsForGorp...))(b) + _ = must.Return(gorpDB.Update(recordsForGorp...))(b) + } + updateWithGorm := func(b *testing.B, message string) { + for idx := range recordsForGorm { + recordsForGorm[idx].Message = message + } + result := gormDB.Save(&recordsForGorm) + assert.ErrEqual(b, result.Error, "<success>") + assert.Equal(b, result.RowsAffected, int64(batchSize)) } updateWithStraightSqlite := func(b *testing.B, message string) { for _, r := range recordsForOblast { @@ -370,9 +428,19 @@ func BenchmarkUpdate(b *testing.B) { } // run once to prewarm caches (if any) + updateWithGorm(b, "warming up") updateWithGorp(b, "warming up") updateWithOblast(b, "warming up") + b.Run("via Gorm", func(b *testing.B) { + idx := 0 + for b.Loop() { + idx++ + message := fmt.Sprintf("round %d", idx) + updateWithGorm(b, message) + checkRecordsUpdated(b, message) + } + }) b.Run("via Gorp", func(b *testing.B) { idx := 0 for b.Loop() { diff --git a/benchmark/go.mod b/benchmark/go.mod index 01e8126..75f27db 100644 --- a/benchmark/go.mod +++ b/benchmark/go.mod @@ -5,5 +5,13 @@ go 1.26.0 require ( github.com/go-gorp/gorp/v3 v3.1.0 github.com/mattn/go-sqlite3 v1.14.42 - go.xyrillian.de/oblast v0.0.0-20260410125639-bce3df549ff4 + go.xyrillian.de/oblast v0.0.0-20260417200949-ba4f55e75e6b + gorm.io/driver/sqlite v1.6.0 + gorm.io/gorm v1.31.1 +) + +require ( + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + golang.org/x/text v0.20.0 // indirect ) diff --git a/benchmark/go.sum b/benchmark/go.sum index e05abdd..5701940 100644 --- a/benchmark/go.sum +++ b/benchmark/go.sum @@ -4,6 +4,10 @@ github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo= @@ -14,7 +18,13 @@ github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -go.xyrillian.de/oblast v0.0.0-20260410125639-bce3df549ff4 h1:MJpcBoNsrY4e/eRpYfz1Gllc3aqIjwFxSPSyKToa+mQ= -go.xyrillian.de/oblast v0.0.0-20260410125639-bce3df549ff4/go.mod h1:lo6ekGOHTID0KeSWhNQV1gjYJ2BfhXgenUEBNBnZkBM= +go.xyrillian.de/oblast v0.0.0-20260417200949-ba4f55e75e6b h1:TalkSqODn/2HbLiwd/Tgb5KFXmzJa6O1BbassZyLnVU= +go.xyrillian.de/oblast v0.0.0-20260417200949-ba4f55e75e6b/go.mod h1:lo6ekGOHTID0KeSWhNQV1gjYJ2BfhXgenUEBNBnZkBM= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/go.work.sum b/go.work.sum index 847440b..6216e4f 100644 --- a/go.work.sum +++ b/go.work.sum @@ -3,5 +3,8 @@ github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3x github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/nelsam/hel/v2 v2.3.3/go.mod h1:1ZTGfU2PFTOd5mx22i5O0Lc2GY933lQ2wb/ggy+rL3w= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= |
