From 00190abad8b411d935d09f8e6dd23de9b583dd74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=92=B8=E9=B1=BC=E8=80=81=E7=BD=97?= Date: Thu, 1 Jun 2023 21:33:40 +0800 Subject: [PATCH] add method `ScanAndCount` and `AllAndCount` for Model package `gdb` (#2635) --- contrib/drivers/sqlite/sqlite_model_test.go | 259 ++++++++++++++++++++ database/gdb/gdb_model_select.go | 92 +++++++ 2 files changed, 351 insertions(+) diff --git a/contrib/drivers/sqlite/sqlite_model_test.go b/contrib/drivers/sqlite/sqlite_model_test.go index bd77855b1..c9acfbd62 100644 --- a/contrib/drivers/sqlite/sqlite_model_test.go +++ b/contrib/drivers/sqlite/sqlite_model_test.go @@ -20,6 +20,8 @@ import ( "github.com/gogf/gf/v2/container/gvar" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/os/gtime" @@ -571,6 +573,100 @@ func Test_Model_All(t *testing.T) { }) } +func Test_Model_AllAndCount(t *testing.T) { + table := createInitTable() + defer dropTable(table) + tableName2 := "user_" + gtime.Now().TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` + CREATE TABLE %s ( + id INTEGER PRIMARY KEY AUTOINCREMENT + UNIQUE + NOT NULL, + name varchar(45) NULL, + age int(10) + ); + `, tableName2, + )); err != nil { + gtest.AssertNil(err) + } + defer dropTable(tableName2) + r, err := db.Insert(ctx, tableName2, g.Map{ + "id": 1, + "name": "table2_1", + "age": 18, + }) + gtest.AssertNil(err) + n, _ := r.RowsAffected() + gtest.Assert(n, 1) + + // AllAndCount with all data + gtest.C(t, func(t *gtest.T) { + result, count, err := db.Model(table).AllAndCount(false) + t.AssertNil(err) + t.Assert(len(result), TableSize) + t.Assert(count, TableSize) + }) + // AllAndCount with no data + gtest.C(t, func(t *gtest.T) { + result, count, err := db.Model(table).Where("id<0").AllAndCount(false) + t.Assert(result, nil) + t.AssertNil(err) + t.Assert(count, 0) + }) + // AllAndCount with page + gtest.C(t, func(t *gtest.T) { + result, count, err := db.Model(table).Page(1, 5).AllAndCount(false) + t.AssertNil(err) + t.Assert(len(result), 5) + t.Assert(count, TableSize) + }) + // AllAndCount with normal result + gtest.C(t, func(t *gtest.T) { + result, count, err := db.Model(table).Where("id=?", 1).AllAndCount(false) + t.AssertNil(err) + t.Assert(count, 1) + t.Assert(result[0]["id"], 1) + t.Assert(result[0]["nickname"], "name_1") + t.Assert(result[0]["passport"], "user_1") + }) + // AllAndCount with distinct + gtest.C(t, func(t *gtest.T) { + result, count, err := db.Model(table).Fields("DISTINCT nickname").AllAndCount(true) + t.AssertNil(err) + t.Assert(count, TableSize) + t.Assert(result[0]["nickname"], "name_1") + t.AssertNil(result[0]["id"]) + }) + // AllAndCount with Join + gtest.C(t, func(t *gtest.T) { + all, count, err := db.Model(table).As("u1"). + LeftJoin(tableName2, "u2", "u2.id=u1.id"). + Fields("u1.passport,u1.id,u2.name,u2.age"). + Where("u1.id<2"). + AllAndCount(false) + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(len(all[0]), 4) + t.Assert(all[0]["id"], 1) + t.Assert(all[0]["age"], 18) + t.Assert(all[0]["name"], "table2_1") + t.Assert(all[0]["passport"], "user_1") + t.Assert(count, 1) + }) + // AllAndCount with Join return CodeDbOperationError + gtest.C(t, func(t *gtest.T) { + all, count, err := db.Model(table).As("u1"). + LeftJoin(tableName2, "u2", "u2.id=u1.id"). + Fields("u1.passport,u1.id,u2.name,u2.age"). + Where("u1.id<2"). + AllAndCount(true) + t.AssertNE(err, nil) + t.AssertEQ(gerror.Code(err), gcode.CodeDbOperationError) + t.Assert(count, 0) + t.Assert(all, nil) + }) +} + func Test_Model_Fields(t *testing.T) { tableName1 := createInitTable() defer dropTable(tableName1) @@ -1080,6 +1176,169 @@ func Test_Model_Scan(t *testing.T) { }) } +func Test_Model_ScanAndCount(t *testing.T) { + table := createInitTable() + defer dropTable(table) + tableName2 := "user_" + gtime.Now().TimestampNanoStr() + if _, err := db.Exec(ctx, fmt.Sprintf(` + CREATE TABLE %s ( + id INTEGER PRIMARY KEY AUTOINCREMENT + UNIQUE + NOT NULL, + name varchar(45) NULL, + age int(10) + ); + `, tableName2, + )); err != nil { + gtest.AssertNil(err) + } + defer dropTable(tableName2) + r, err := db.Insert(ctx, tableName2, g.Map{ + "id": 1, + "name": "table2_1", + "age": 18, + }) + gtest.AssertNil(err) + n, _ := r.RowsAffected() + gtest.Assert(n, 1) + + // ScanAndCount with normal struct result + gtest.C(t, func(t *gtest.T) { + type User struct { + Id int + Passport string + Password string + NickName string + CreateTime *gtime.Time + } + user := new(User) + var count int + err := db.Model(table).Where("id=1").ScanAndCount(user, &count, true) + t.AssertNil(err) + t.Assert(user.NickName, "name_1") + t.Assert(user.CreateTime.String(), CreateTime) + t.Assert(count, 1) + }) + // ScanAndCount with normal array result + gtest.C(t, func(t *gtest.T) { + type User struct { + Id int + Passport string + Password string + NickName string + CreateTime gtime.Time + } + var users []User + var count int + err := db.Model(table).Order("id asc").ScanAndCount(&users, &count, true) + t.AssertNil(err) + t.Assert(len(users), TableSize) + t.Assert(users[0].Id, 1) + t.Assert(users[1].Id, 2) + t.Assert(users[2].Id, 3) + t.Assert(users[0].NickName, "name_1") + t.Assert(users[1].NickName, "name_2") + t.Assert(users[2].NickName, "name_3") + t.Assert(users[0].CreateTime.String(), CreateTime) + t.Assert(count, len(users)) + }) + // sql.ErrNoRows + gtest.C(t, func(t *gtest.T) { + type User struct { + Id int + Passport string + Password string + NickName string + CreateTime *gtime.Time + } + var ( + user = new(User) + users = new([]*User) + ) + var count1 int + var count2 int + err1 := db.Model(table).Where("id < 0").ScanAndCount(user, &count1, true) + err2 := db.Model(table).Where("id < 0").ScanAndCount(users, &count2, true) + t.Assert(count1, 0) + t.Assert(count2, 0) + t.Assert(err1, nil) + t.Assert(err2, nil) + }) + // ScanAndCount with page + gtest.C(t, func(t *gtest.T) { + type User struct { + Id int + Passport string + Password string + NickName string + CreateTime gtime.Time + } + var users []User + var count int + err := db.Model(table).Order("id asc").Page(1, 3).ScanAndCount(&users, &count, true) + t.AssertNil(err) + t.Assert(len(users), 3) + t.Assert(count, TableSize) + }) + // ScanAndCount with distinct + gtest.C(t, func(t *gtest.T) { + type User struct { + Id int + Passport string + Password string + NickName string + CreateTime gtime.Time + } + var users []User + var count int + err = db.Model(table).Fields("distinct id").ScanAndCount(&users, &count, true) + t.AssertNil(err) + t.Assert(len(users), 10) + t.Assert(count, TableSize) + t.Assert(users[0].Id, 1) + }) + // ScanAndCount with join + gtest.C(t, func(t *gtest.T) { + type User struct { + Id int + Passport string + Name string + Age int + } + var users []User + var count int + err = db.Model(table).As("u1"). + LeftJoin(tableName2, "u2", "u2.id=u1.id"). + Fields("u1.passport,u1.id,u2.name,u2.age"). + Where("u1.id<2"). + ScanAndCount(&users, &count, false) + t.AssertNil(err) + t.Assert(len(users), 1) + t.Assert(count, 1) + t.AssertEQ(users[0].Name, "table2_1") + }) + // ScanAndCount with join return CodeDbOperationError + gtest.C(t, func(t *gtest.T) { + type User struct { + Id int + Passport string + Name string + Age int + } + var users []User + var count int + err = db.Model(table).As("u1"). + LeftJoin(tableName2, "u2", "u2.id=u1.id"). + Fields("u1.passport,u1.id,u2.name,u2.age"). + Where("u1.id<2"). + ScanAndCount(&users, &count, true) + t.AssertNE(err, nil) + t.Assert(gerror.Code(err), gcode.CodeDbOperationError) + t.Assert(count, 0) + t.AssertEQ(users, nil) + }) +} + func Test_Model_Scan_NilSliceAttrWhenNoRecordsFound(t *testing.T) { table := createTable() defer dropTable(table) diff --git a/database/gdb/gdb_model_select.go b/database/gdb/gdb_model_select.go index c6f96c57c..c7f7239d3 100644 --- a/database/gdb/gdb_model_select.go +++ b/database/gdb/gdb_model_select.go @@ -30,6 +30,48 @@ func (m *Model) All(where ...interface{}) (Result, error) { return m.doGetAll(ctx, false, where...) } +// AllAndCount retrieves all records and the total count of records from the model. +// If useFieldForCount is true, it will use the fields specified in the model for counting; +// otherwise, it will use a constant value of 1 for counting. +// It returns the result as a slice of records, the total count of records, and an error if any. +// The where parameter is an optional list of conditions to use when retrieving records. +// +// Example: +// +// var model Model +// var result Result +// var count int +// where := []interface{}{"name = ?", "John"} +// result, count, err := model.AllAndCount(true) +// if err != nil { +// // Handle error. +// } +// fmt.Println(result, count) +func (m *Model) AllAndCount(useFieldForCount bool) (result Result, totalCount int, err error) { + // Clone the model for counting + countModel := m.Clone() + + // If useFieldForCount is false, set the fields to a constant value of 1 for counting + if !useFieldForCount { + countModel.fields = "1" + } + + // Get the total count of records + totalCount, err = countModel.Count() + if err != nil { + return + } + + // If the total count is 0, there are no records to retrieve, so return early + if totalCount == 0 { + return + } + + // Retrieve all records + result, err = m.doGetAll(m.GetCtx(), false) + return +} + // Chunk iterates the query result with given `size` and `handler` function. func (m *Model) Chunk(size int, handler ChunkHandler) { page := m.start @@ -235,6 +277,56 @@ func (m *Model) Scan(pointer interface{}, where ...interface{}) error { } } +// ScanAndCount scans a single record or record array that matches the given conditions and counts the total number of records that match those conditions. +// If useFieldForCount is true, it will use the fields specified in the model for counting; +// The pointer parameter is a pointer to a struct that the scanned data will be stored in. +// The pointerCount parameter is a pointer to an integer that will be set to the total number of records that match the given conditions. +// The where parameter is an optional list of conditions to use when retrieving records. +// +// Example: +// +// var count int +// user := new(User) +// err := db.Model("user").Where("id", 1).ScanAndCount(user,&count,true) +// fmt.Println(user, count) +// +// Example Join: +// +// type User struct { +// Id int +// Passport string +// Name string +// Age int +// } +// var users []User +// var count int +// db.Model(table).As("u1"). +// LeftJoin(tableName2, "u2", "u2.id=u1.id"). +// Fields("u1.passport,u1.id,u2.name,u2.age"). +// Where("u1.id<2"). +// ScanAndCount(&users, &count, false) +func (m *Model) ScanAndCount(pointer interface{}, totalCount *int, useFieldForCount bool) (err error) { + // support Fileds with *, example: .Fileds("a.*, b.name"). Count sql is select count(1) from xxx + countModel := m.Clone() + // If useFieldForCount is false, set the fields to a constant value of 1 for counting + if !useFieldForCount { + countModel.fields = "1" + } + + // Get the total count of records + *totalCount, err = countModel.Count() + if err != nil { + return err + } + + // If the total count is 0, there are no records to retrieve, so return early + if *totalCount == 0 { + return + } + err = m.Scan(pointer) + return +} + // ScanList converts `r` to struct slice which contains other complex struct attributes. // Note that the parameter `listPointer` should be type of *[]struct/*[]*struct. //