From 3ac577205937f818f283b6331cd093d1c0d982cb Mon Sep 17 00:00:00 2001 From: John Guo Date: Sun, 6 Jun 2021 23:06:39 +0800 Subject: [PATCH] add UNION/UNION ALL feature for package gdb --- database/gdb/gdb.go | 6 + database/gdb/gdb_core.go | 36 ++++++ database/gdb/gdb_model.go | 3 +- database/gdb/gdb_model_condition.go | 2 + database/gdb/gdb_model_select.go | 26 ++++- database/gdb/gdb_model_time.go | 3 + database/gdb/gdb_z_mysql_union_test.go | 146 +++++++++++++++++++++++++ 7 files changed, 215 insertions(+), 7 deletions(-) create mode 100644 database/gdb/gdb_z_mysql_union_test.go diff --git a/database/gdb/gdb.go b/database/gdb/gdb.go index ab415021a..7949dcf71 100644 --- a/database/gdb/gdb.go +++ b/database/gdb/gdb.go @@ -121,6 +121,8 @@ type DB interface { GetStruct(objPointer interface{}, sql string, args ...interface{}) error // See Core.GetStruct. GetStructs(objPointerSlice interface{}, sql string, args ...interface{}) error // See Core.GetStructs. GetScan(objPointer interface{}, sql string, args ...interface{}) error // See Core.GetScan. + Union(unions ...*Model) *Model // See Core.Union. + UnionAll(unions ...*Model) *Model // See Core.UnionAll. // =========================================================================== // Master/Slave specification support. @@ -252,6 +254,10 @@ type ( ) const ( + queryTypeNormal = 0 + queryTypeCount = 1 + unionTypeNormal = 0 + unionTypeAll = 1 insertOptionDefault = 0 insertOptionReplace = 1 insertOptionSave = 2 diff --git a/database/gdb/gdb_core.go b/database/gdb/gdb_core.go index 84d50bbd9..fbace582e 100644 --- a/database/gdb/gdb_core.go +++ b/database/gdb/gdb_core.go @@ -224,6 +224,42 @@ func (c *Core) GetCount(sql string, args ...interface{}) (int, error) { return value.Int(), nil } +// Union does "(SELECT xxx FROM xxx) UNION (SELECT xxx FROM xxx) ..." statement. +func (c *Core) Union(unions ...*Model) *Model { + return c.doUnion(unionTypeNormal, unions...) +} + +// UnionAll does "(SELECT xxx FROM xxx) UNION ALL (SELECT xxx FROM xxx) ..." statement. +func (c *Core) UnionAll(unions ...*Model) *Model { + return c.doUnion(unionTypeAll, unions...) +} + +func (c *Core) doUnion(unionType int, unions ...*Model) *Model { + var ( + unionTypeStr string + composedSqlStr string + composedArgs = make([]interface{}, 0) + ) + if unionType == unionTypeAll { + unionTypeStr = "UNION ALL" + } else { + unionTypeStr = "UNION" + } + for _, v := range unions { + sqlWithHolder, holderArgs := v.getFormattedSqlAndArgs(queryTypeNormal, false) + if composedSqlStr == "" { + composedSqlStr += fmt.Sprintf(`(%s)`, sqlWithHolder) + } else { + composedSqlStr += fmt.Sprintf(` %s (%s)`, unionTypeStr, sqlWithHolder) + } + composedArgs = append(composedArgs, holderArgs...) + } + model := c.db.Model() + model.rawSql = composedSqlStr + model.extraArgs = composedArgs + return model +} + // PingMaster pings the master node to check authentication or keeps the connection alive. func (c *Core) PingMaster() error { if master, err := c.db.Master(); err != nil { diff --git a/database/gdb/gdb_model.go b/database/gdb/gdb_model.go index 627051adb..68532f646 100644 --- a/database/gdb/gdb_model.go +++ b/database/gdb/gdb_model.go @@ -17,10 +17,11 @@ import ( "github.com/gogf/gf/text/gstr" ) -// Model is the DAO for ORM. +// Model is core struct implementing the DAO for ORM. type Model struct { db DB // Underlying DB interface. tx *TX // Underlying TX interface. + rawSql string // rawSql is the raw SQL string which marks a raw SQL based Model not a table based Model. schema string // Custom database schema. linkType int // Mark for operation on master or slave. tablesInit string // Table names when model initialization. diff --git a/database/gdb/gdb_model_condition.go b/database/gdb/gdb_model_condition.go index 7a7d41e5e..ecfba71d6 100644 --- a/database/gdb/gdb_model_condition.go +++ b/database/gdb/gdb_model_condition.go @@ -62,6 +62,8 @@ func (m *Model) WherePri(where interface{}, args ...interface{}) *Model { } // Wheref builds condition string using fmt.Sprintf and arguments. +// Note that if the number of `args` is more than the place holder in `format`, +// the extra `args` will be used as the where condition arguments of the Model. func (m *Model) Wheref(format string, args ...interface{}) *Model { var ( placeHolderCount = gstr.Count(format, "?") diff --git a/database/gdb/gdb_model_select.go b/database/gdb/gdb_model_select.go index ff366f218..2a9c02926 100644 --- a/database/gdb/gdb_model_select.go +++ b/database/gdb/gdb_model_select.go @@ -18,11 +18,6 @@ import ( "github.com/gogf/gf/util/gconv" ) -const ( - queryTypeNormal = "NormalQuery" - queryTypeCount = "CountQuery" -) - // Select is alias of Model.All. // See Model.All. // Deprecated, use All instead. @@ -458,6 +453,16 @@ func (m *Model) FindScan(pointer interface{}, where ...interface{}) error { return m.Scan(pointer) } +// Union does "(SELECT xxx FROM xxx) UNION (SELECT xxx FROM xxx) ..." statement for the model. +func (m *Model) Union(unions ...*Model) *Model { + return m.db.Union(unions...) +} + +// UnionAll does "(SELECT xxx FROM xxx) UNION ALL (SELECT xxx FROM xxx) ..." statement for the model. +func (m *Model) UnionAll(unions ...*Model) *Model { + return m.db.UnionAll(unions...) +} + // doGetAllBySql does the select statement on the database. func (m *Model) doGetAllBySql(sql string, args ...interface{}) (result Result, err error) { cacheKey := "" @@ -501,7 +506,7 @@ func (m *Model) doGetAllBySql(sql string, args ...interface{}) (result Result, e return result, err } -func (m *Model) getFormattedSqlAndArgs(queryType string, limit1 bool) (sqlWithHolder string, holderArgs []interface{}) { +func (m *Model) getFormattedSqlAndArgs(queryType int, limit1 bool) (sqlWithHolder string, holderArgs []interface{}) { switch queryType { case queryTypeCount: countFields := "COUNT(1)" @@ -519,6 +524,15 @@ func (m *Model) getFormattedSqlAndArgs(queryType string, limit1 bool) (sqlWithHo default: conditionWhere, conditionExtra, conditionArgs := m.formatCondition(limit1, false) + // Raw SQL Model, especially for UNION/UNION ALL featured SQL. + if m.rawSql != "" { + sqlWithHolder = fmt.Sprintf( + "%s%s", + m.rawSql, + conditionWhere+conditionExtra, + ) + return sqlWithHolder, conditionArgs + } // DO NOT quote the m.fields where, in case of fields like: // DISTINCT t.user_id uid sqlWithHolder = fmt.Sprintf( diff --git a/database/gdb/gdb_model_time.go b/database/gdb/gdb_model_time.go index e4047e4a1..76c1f0667 100644 --- a/database/gdb/gdb_model_time.go +++ b/database/gdb/gdb_model_time.go @@ -173,6 +173,9 @@ func (m *Model) getConditionOfTableStringForSoftDeleting(s string) string { // getPrimaryTableName parses and returns the primary table name. func (m *Model) getPrimaryTableName() string { + if m.tables == "" { + return "" + } array1 := gstr.SplitAndTrim(m.tables, ",") array2 := gstr.SplitAndTrim(array1[0], " ") array3 := gstr.SplitAndTrim(array2[0], ".") diff --git a/database/gdb/gdb_z_mysql_union_test.go b/database/gdb/gdb_z_mysql_union_test.go new file mode 100644 index 000000000..34df871b4 --- /dev/null +++ b/database/gdb/gdb_z_mysql_union_test.go @@ -0,0 +1,146 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package gdb_test + +import ( + "github.com/gogf/gf/frame/g" + "testing" + + "github.com/gogf/gf/test/gtest" +) + +func Test_Union(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Union( + db.Model(table).Where("id", 1), + db.Model(table).Where("id", 2), + db.Model(table).WhereIn("id", g.Slice{1, 2, 3}).OrderDesc("id"), + ).OrderDesc("id").All() + + t.AssertNil(err) + + t.Assert(len(r), 3) + t.Assert(r[0]["id"], 3) + t.Assert(r[1]["id"], 2) + t.Assert(r[2]["id"], 1) + }) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Union( + db.Model(table).Where("id", 1), + db.Model(table).Where("id", 2), + db.Model(table).WhereIn("id", g.Slice{1, 2, 3}).OrderDesc("id"), + ).OrderDesc("id").One() + + t.AssertNil(err) + + t.Assert(r["id"], 3) + }) +} + +func Test_UnionAll(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + r, err := db.UnionAll( + db.Model(table).Where("id", 1), + db.Model(table).Where("id", 2), + db.Model(table).WhereIn("id", g.Slice{1, 2, 3}).OrderDesc("id"), + ).OrderDesc("id").All() + + t.AssertNil(err) + + t.Assert(len(r), 5) + t.Assert(r[0]["id"], 3) + t.Assert(r[1]["id"], 2) + t.Assert(r[2]["id"], 2) + t.Assert(r[3]["id"], 1) + t.Assert(r[4]["id"], 1) + }) + + gtest.C(t, func(t *gtest.T) { + r, err := db.UnionAll( + db.Model(table).Where("id", 1), + db.Model(table).Where("id", 2), + db.Model(table).WhereIn("id", g.Slice{1, 2, 3}).OrderDesc("id"), + ).OrderDesc("id").One() + + t.AssertNil(err) + + t.Assert(r["id"], 3) + }) +} + +func Test_Model_Union(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table).Union( + db.Model(table).Where("id", 1), + db.Model(table).Where("id", 2), + db.Model(table).WhereIn("id", g.Slice{1, 2, 3}).OrderDesc("id"), + ).OrderDesc("id").All() + + t.AssertNil(err) + + t.Assert(len(r), 3) + t.Assert(r[0]["id"], 3) + t.Assert(r[1]["id"], 2) + t.Assert(r[2]["id"], 1) + }) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table).Union( + db.Model(table).Where("id", 1), + db.Model(table).Where("id", 2), + db.Model(table).WhereIn("id", g.Slice{1, 2, 3}).OrderDesc("id"), + ).OrderDesc("id").One() + + t.AssertNil(err) + + t.Assert(r["id"], 3) + }) +} + +func Test_Model_UnionAll(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table).UnionAll( + db.Model(table).Where("id", 1), + db.Model(table).Where("id", 2), + db.Model(table).WhereIn("id", g.Slice{1, 2, 3}).OrderDesc("id"), + ).OrderDesc("id").All() + + t.AssertNil(err) + + t.Assert(len(r), 5) + t.Assert(r[0]["id"], 3) + t.Assert(r[1]["id"], 2) + t.Assert(r[2]["id"], 2) + t.Assert(r[3]["id"], 1) + t.Assert(r[4]["id"], 1) + }) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table).UnionAll( + db.Model(table).Where("id", 1), + db.Model(table).Where("id", 2), + db.Model(table).WhereIn("id", g.Slice{1, 2, 3}).OrderDesc("id"), + ).OrderDesc("id").One() + + t.AssertNil(err) + + t.Assert(r["id"], 3) + }) +}