style: remove old libary ggorm

This commit is contained in:
zhuyasen 2025-07-15 22:40:04 +08:00
parent a8d69f49e7
commit 4ed9820657
11 changed files with 0 additions and 1786 deletions

View File

@ -1,165 +0,0 @@
## ggorm
`ggorm` library wrapped in [gorm](gorm.io/gorm), with added features such as tracer, paging queries, etc.
Support `mysql`, `postgresql`, `sqlite`.
<br>
## Examples of use
### mysql
#### Initializing the connection
```go
import (
"github.com/go-dev-frame/sponge/pkg/ggorm"
)
var dsn = "root:123456@(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
// (1) connect to the database using the default settings
db, err := ggorm.InitMysql(dsn)
// (2) customised settings to connect to the database
db, err := ggorm.InitMysql(
dsn,
ggorm.WithLogging(logger.Get()), // print log
ggorm.WithLogRequestIDKey("request_id"), // print request_id
ggorm.WithMaxIdleConns(5),
ggorm.WithMaxOpenConns(50),
ggorm.WithConnMaxLifetime(time.Minute*3),
// ggorm.WithSlowThreshold(time.Millisecond*100), // only print logs that take longer than 100 milliseconds to execute
// ggorm.WithEnableTrace(), // enable tracing
// ggorm.WithRWSeparation(SlavesDsn, MastersDsn...) // read-write separation
// ggorm.WithGormPlugin(yourPlugin) // custom gorm plugin
)
```
<br>
#### Model
```go
package model
import (
"github.com/go-dev-frame/sponge/pkg/ggorm"
)
// UserExample object fields mapping table
type UserExample struct {
ggorm.Model `gorm:"embedded"`
Name string `gorm:"type:varchar(40);unique_index;not null" json:"name"`
Age int `gorm:"not null" json:"age"`
Gender string `gorm:"type:varchar(10);not null" json:"gender"`
}
// TableName get table name
func (table *UserExample) TableName() string {
return ggorm.GetTableName(table)
}
```
<br>
#### Transaction
```go
func createUser() error {
// note that you should use tx as the database handle when you are in a transaction
tx := db.Begin()
defer func() {
if err := recover(); err != nil { // rollback after a panic during transaction execution
tx.Rollback()
fmt.Printf("transaction failed, err = %v\n", err)
}
}()
var err error
if err = tx.Error; err != nil {
return err
}
if err = tx.Where("id = ?", 1).First(table).Error; err != nil {
tx.Rollback()
return err
}
panic("mock panic")
if err = tx.Create(&userExample{Name: "Mr Li", Age: table.Age + 2, Gender: "male"}).Error; err != nil {
tx.Rollback()
return err
}
return tx.Commit().Error
}
```
<br>
### Postgresql
```go
import (
"github.com/go-dev-frame/sponge/pkg/ggorm"
"github.com/go-dev-frame/sponge/pkg/utils"
)
func InitSqlite() {
opts := []ggorm.Option{
ggorm.WithMaxIdleConns(10),
ggorm.WithMaxOpenConns(100),
ggorm.WithConnMaxLifetime(time.Duration(10) * time.Minute),
ggorm.WithLogging(logger.Get()),
ggorm.WithLogRequestIDKey("request_id"),
}
dsn := "root:123456@127.0.0.1:5432/test"
dsn = utils.AdaptivePostgresqlDsn(dsn)
db, err := ggorm.InitPostgresql(dsn, opts...)
if err != nil {
panic("ggorm.InitPostgresql error: " + err.Error())
}
}
```
<br>
### Tidb
Tidb is mysql compatible, just use **InitMysql**.
<br>
### Sqlite
```go
import (
"github.com/go-dev-frame/sponge/pkg/ggorm"
)
func InitSqlite() {
opts := []ggorm.Option{
ggorm.WithMaxIdleConns(10),
ggorm.WithMaxOpenConns(100),
ggorm.WithConnMaxLifetime(time.Duration(10) * time.Minute),
ggorm.WithLogging(logger.Get()),
ggorm.WithLogRequestIDKey("request_id"),
}
dbFile: = "test.db"
db, err := ggorm.InitSqlite(dbFile, opts...)
if err != nil {
panic("ggorm.InitSqlite error: " + err.Error())
}
}
```
<br>
### gorm User Guide
- https://gorm.io/zh_CN/docs/index.html

View File

@ -1,45 +0,0 @@
package ggorm
import (
"reflect"
"time"
"github.com/huandu/xstrings"
"gorm.io/gorm"
)
// Model embedded structs, add `gorm: "embedded"` when defining table structs
type Model struct {
ID uint64 `gorm:"column:id;AUTO_INCREMENT;primary_key" json:"id"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"-"`
}
// Model2 embedded structs, json tag named is snake case
type Model2 struct {
ID uint64 `gorm:"column:id;AUTO_INCREMENT;primary_key" json:"id"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"-"`
}
// KV map type
type KV = map[string]interface{}
// GetTableName get table name
func GetTableName(object interface{}) string {
tableName := ""
typeof := reflect.TypeOf(object)
switch typeof.Kind() {
case reflect.Ptr:
tableName = typeof.Elem().Name()
case reflect.Struct:
tableName = typeof.Name()
default:
return tableName
}
return xstrings.ToSnakeCase(tableName)
}

View File

@ -1,248 +0,0 @@
// Package ggorm is a library wrapped on top of gorm.io/gorm, with added features such as link tracing, paging queries, etc.
package ggorm
import (
"context"
"database/sql"
"fmt"
"log"
"os"
"time"
"github.com/uptrace/opentelemetry-go-extra/otelgorm"
mysqlDriver "gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"gorm.io/plugin/dbresolver"
)
type DB = gorm.DB
const (
// DBDriverMysql mysql driver
DBDriverMysql = "mysql"
// DBDriverPostgresql postgresql driver
DBDriverPostgresql = "postgresql"
// DBDriverTidb tidb driver
DBDriverTidb = "tidb"
// DBDriverSqlite sqlite driver
DBDriverSqlite = "sqlite"
)
// InitMysql init mysql or tidb
func InitMysql(dsn string, opts ...Option) (*gorm.DB, error) {
o := defaultOptions()
o.apply(opts...)
sqlDB, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
sqlDB.SetMaxIdleConns(o.maxIdleConns) // set the maximum number of connections in the idle connection pool
sqlDB.SetMaxOpenConns(o.maxOpenConns) // set the maximum number of open database connections
sqlDB.SetConnMaxLifetime(o.connMaxLifetime) // set the maximum time a connection can be reused
db, err := gorm.Open(mysqlDriver.New(mysqlDriver.Config{Conn: sqlDB}), gormConfig(o))
if err != nil {
return nil, err
}
db.Set("gorm:table_options", "CHARSET=utf8mb4") // automatic appending of table suffixes when creating tables
// register trace plugin
if o.enableTrace {
err = db.Use(otelgorm.NewPlugin())
if err != nil {
return nil, fmt.Errorf("using gorm opentelemetry, err: %v", err)
}
}
// register read-write separation plugin
if len(o.slavesDsn) > 0 {
err = db.Use(rwSeparationPlugin(o))
if err != nil {
return nil, err
}
}
// register plugins
for _, plugin := range o.plugins {
err = db.Use(plugin)
if err != nil {
return nil, err
}
}
return db, nil
}
// InitPostgresql init postgresql
func InitPostgresql(dsn string, opts ...Option) (*gorm.DB, error) {
o := defaultOptions()
o.apply(opts...)
db, err := gorm.Open(postgres.Open(dsn), gormConfig(o))
if err != nil {
return nil, err
}
// register trace plugin
if o.enableTrace {
err = db.Use(otelgorm.NewPlugin())
if err != nil {
return nil, fmt.Errorf("using gorm opentelemetry, err: %v", err)
}
}
// register read-write separation plugin
if len(o.slavesDsn) > 0 {
err = db.Use(rwSeparationPlugin(o))
if err != nil {
return nil, err
}
}
// register plugins
for _, plugin := range o.plugins {
err = db.Use(plugin)
if err != nil {
return nil, err
}
}
return db, nil
}
// InitTidb init tidb
func InitTidb(dsn string, opts ...Option) (*gorm.DB, error) {
return InitMysql(dsn, opts...)
}
// InitSqlite init sqlite
func InitSqlite(dbFile string, opts ...Option) (*gorm.DB, error) {
o := defaultOptions()
o.apply(opts...)
dsn := fmt.Sprintf("%s?_journal=WAL&_vacuum=incremental", dbFile)
db, err := gorm.Open(sqlite.Open(dsn), gormConfig(o))
if err != nil {
return nil, err
}
db.Set("gorm:auto_increment", true)
// register trace plugin
if o.enableTrace {
err = db.Use(otelgorm.NewPlugin())
if err != nil {
return nil, fmt.Errorf("using gorm opentelemetry, err: %v", err)
}
}
// register plugins
for _, plugin := range o.plugins {
err = db.Use(plugin)
if err != nil {
return nil, err
}
}
return db, nil
}
// CloseDB close gorm db
func CloseDB(db *gorm.DB) error {
if db == nil {
return nil
}
sqlDB, err := db.DB()
if err != nil {
return err
}
checkInUse(sqlDB, time.Second*5)
return sqlDB.Close()
}
func checkInUse(sqlDB *sql.DB, duration time.Duration) {
ctx, _ := context.WithTimeout(context.Background(), duration) //nolint
for {
select {
case <-time.After(time.Millisecond * 250):
if v := sqlDB.Stats().InUse; v == 0 {
return
}
case <-ctx.Done():
return
}
}
}
// CloseSQLDB close sql db
func CloseSQLDB(db *gorm.DB) {
sqlDB, err := db.DB()
if err != nil {
return
}
_ = sqlDB.Close()
}
// gorm setting
func gormConfig(o *options) *gorm.Config {
config := &gorm.Config{
// disable foreign key constraints, not recommended for production environments
DisableForeignKeyConstraintWhenMigrating: o.disableForeignKey,
// removing the plural of an epithet
NamingStrategy: schema.NamingStrategy{SingularTable: true},
}
// print SQL
if o.isLog {
if o.gLog == nil {
config.Logger = logger.Default.LogMode(o.logLevel)
} else {
config.Logger = NewCustomGormLogger(o)
}
} else {
config.Logger = logger.Default.LogMode(logger.Silent)
}
// print only slow queries
if o.slowThreshold > 0 {
config.Logger = logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // use the standard output asWriter
logger.Config{
SlowThreshold: o.slowThreshold,
Colorful: true,
LogLevel: logger.Warn, // set the logging level, only above the specified level will output the slow query log
},
)
}
return config
}
func rwSeparationPlugin(o *options) gorm.Plugin {
slaves := []gorm.Dialector{}
for _, dsn := range o.slavesDsn {
slaves = append(slaves, mysqlDriver.New(mysqlDriver.Config{
DSN: dsn,
}))
}
masters := []gorm.Dialector{}
for _, dsn := range o.mastersDsn {
masters = append(masters, mysqlDriver.New(mysqlDriver.Config{
DSN: dsn,
}))
}
return dbresolver.Register(dbresolver.Config{
Sources: masters,
Replicas: slaves,
Policy: dbresolver.RandomPolicy{},
})
}

View File

@ -1,131 +0,0 @@
package ggorm
import (
"context"
"strings"
"time"
"go.uber.org/zap"
"gorm.io/gorm/logger"
"gorm.io/gorm/utils"
)
type gormLogger struct {
gLog *zap.Logger
requestIDKey string
logLevel logger.LogLevel
}
// NewCustomGormLogger custom gorm logger
func NewCustomGormLogger(o *options) logger.Interface {
return &gormLogger{
gLog: o.gLog,
requestIDKey: o.requestIDKey,
logLevel: o.logLevel,
}
}
// LogMode log mode
func (l *gormLogger) LogMode(level logger.LogLevel) logger.Interface {
l.logLevel = level
return l
}
// Info print info
func (l *gormLogger) Info(ctx context.Context, msg string, data ...interface{}) {
if l.logLevel >= logger.Info {
msg = strings.ReplaceAll(msg, "%v", "")
l.gLog.Info(msg, zap.Any("data", data), zap.String("line", utils.FileWithLineNum()), requestIDField(ctx, l.requestIDKey))
}
}
// Warn print warn messages
func (l *gormLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
if l.logLevel >= logger.Warn {
msg = strings.ReplaceAll(msg, "%v", "")
l.gLog.Warn(msg, zap.Any("data", data), zap.String("line", utils.FileWithLineNum()), requestIDField(ctx, l.requestIDKey))
}
}
// Error print error messages
func (l *gormLogger) Error(ctx context.Context, msg string, data ...interface{}) {
if l.logLevel >= logger.Error {
msg = strings.ReplaceAll(msg, "%v", "")
l.gLog.Warn(msg, zap.Any("data", data), zap.String("line", utils.FileWithLineNum()), requestIDField(ctx, l.requestIDKey))
}
}
// Trace print sql message
func (l *gormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
if l.logLevel <= logger.Silent {
return
}
elapsed := time.Since(begin)
sql, rows := fc()
var rowsField zap.Field
if rows == -1 {
rowsField = zap.String("rows", "-")
} else {
rowsField = zap.Int64("rows", rows)
}
var fileLineField zap.Field
fileLine := utils.FileWithLineNum()
ss := strings.Split(fileLine, "/internal/")
if len(ss) == 2 {
fileLineField = zap.String("file_line", ss[1])
} else {
fileLineField = zap.String("file_line", fileLine)
}
if err != nil {
l.gLog.Warn("Gorm msg",
zap.Error(err),
zap.String("sql", sql),
rowsField,
zap.Float64("ms", float64(elapsed.Nanoseconds())/1e6),
fileLineField,
requestIDField(ctx, l.requestIDKey),
)
return
}
if l.logLevel >= logger.Info {
l.gLog.Info("Gorm msg",
zap.String("sql", sql),
rowsField,
zap.Float64("ms", float64(elapsed.Nanoseconds())/1e6),
fileLineField,
requestIDField(ctx, l.requestIDKey),
)
return
}
if l.logLevel >= logger.Warn {
l.gLog.Warn("Gorm msg",
zap.String("sql", sql),
rowsField,
zap.Float64("ms", float64(elapsed.Nanoseconds())/1e6),
fileLineField,
requestIDField(ctx, l.requestIDKey),
)
}
}
func requestIDField(ctx context.Context, requestIDKey string) zap.Field {
if requestIDKey == "" {
return zap.Skip()
}
var field zap.Field
if requestIDKey != "" {
if v, ok := ctx.Value(requestIDKey).(string); ok {
field = zap.String(requestIDKey, v)
} else {
field = zap.Skip()
}
}
return field
}

View File

@ -1,61 +0,0 @@
package ggorm
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
"gorm.io/gorm/logger"
)
func TestNewCustomGormLogger(t *testing.T) {
zapLog, _ := zap.NewDevelopment()
l := NewCustomGormLogger(&options{
requestIDKey: "request_id",
gLog: zapLog,
logLevel: logger.Info,
})
l.LogMode(logger.Info)
ctx := context.WithValue(context.Background(), "request_id", "123")
l.Info(ctx, "info", "foo")
l.Warn(ctx, "warn", "bar")
l.Error(ctx, "error", "foo bar")
l.LogMode(logger.Silent)
l.Trace(ctx, time.Now(), nil, nil)
l.LogMode(logger.Info)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", 1
}, nil)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", -1
}, nil)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", 0
}, logger.ErrRecordNotFound)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", 0
}, errors.New("Error 1054: Unknown column 'test_column'"))
l.LogMode(logger.Warn)
l.Trace(ctx, time.Now(), func() (string, int64) {
return "sql statement", 0
}, logger.ErrRecordNotFound)
}
func Test_requestIDField(t *testing.T) {
ctx := context.WithValue(context.Background(), "request_id", "123")
field := requestIDField(ctx, "")
assert.Equal(t, zap.Skip(), field)
field = requestIDField(ctx, "your request id key")
assert.Equal(t, zap.Skip(), field)
field = requestIDField(ctx, "request_id")
assert.Equal(t, zap.String("request_id", "123"), field)
}

View File

@ -1,123 +0,0 @@
package ggorm
import (
"database/sql"
"fmt"
"gorm.io/gorm"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
var dsn = "root:123456@(192.168.3.37:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
func TestInitMysql(t *testing.T) {
db, err := InitMysql(dsn, WithEnableTrace())
if err != nil {
// ignore test error about not being able to connect to real mysql
t.Logf(fmt.Sprintf("connect to mysql failed, err=%v, dsn=%s", err, dsn))
return
}
defer CloseDB(db)
t.Logf("%+v", db.Name())
}
func TestInitTidb(t *testing.T) {
db, err := InitTidb(dsn)
if err != nil {
// ignore test error about not being able to connect to real tidb
t.Logf(fmt.Sprintf("connect to mysql failed, err=%v, dsn=%s", err, dsn))
return
}
defer CloseDB(db)
t.Logf("%+v", db.Name())
}
func TestInitSqlite(t *testing.T) {
dbFile := "test_sqlite.db"
db, err := InitSqlite(dbFile)
if err != nil {
// ignore test error about not being able to connect to real sqlite
t.Logf(fmt.Sprintf("connect to sqlite failed, err=%v, dbFile=%s", err, dbFile))
return
}
defer CloseDB(db)
t.Logf("%+v", db.Name())
}
func TestInitPostgresql(t *testing.T) {
dsn = "host=192.168.3.37 user=root password=123456 dbname=account port=5432 sslmode=disable TimeZone=Asia/Shanghai"
db, err := InitPostgresql(dsn, WithEnableTrace())
if err != nil {
// ignore test error about not being able to connect to real postgresql
t.Logf(fmt.Sprintf("connect to postgresql failed, err=%v, dsn=%s", err, dsn))
return
}
defer CloseDB(db)
t.Logf("%+v", db.Name())
}
func Test_gormConfig(t *testing.T) {
o := defaultOptions()
o.apply(
WithLogging(nil),
WithLogging(nil, 4),
WithSlowThreshold(time.Millisecond*100),
WithEnableTrace(),
WithMaxIdleConns(5),
WithMaxOpenConns(50),
WithConnMaxLifetime(time.Minute*3),
WithEnableForeignKey(),
WithLogRequestIDKey("request_id"),
WithRWSeparation([]string{
"root:123456@(192.168.3.37:3306)/slave1",
"root:123456@(192.168.3.37:3306)/slave2"},
"root:123456@(192.168.3.37:3306)/master"),
WithGormPlugin(nil),
)
c := gormConfig(o)
assert.NotNil(t, c)
err := rwSeparationPlugin(o)
assert.NotNil(t, err)
}
type userExample struct {
Model `gorm:"embedded"`
Name string `gorm:"type:varchar(40);unique_index;not null" json:"name"`
Age int `gorm:"not null" json:"age"`
Gender string `gorm:"type:varchar(10);not null" json:"gender"`
}
func TestGetTableName(t *testing.T) {
name := GetTableName(&userExample{})
assert.NotEmpty(t, name)
name = GetTableName(userExample{})
assert.NotEmpty(t, name)
name = GetTableName("table")
assert.Empty(t, name)
}
func TestCloseDB(t *testing.T) {
sqlDB := new(sql.DB)
checkInUse(sqlDB, time.Millisecond*100)
checkInUse(sqlDB, time.Millisecond*600)
db := new(gorm.DB)
defer func() { recover() }()
_ = CloseDB(db)
}
func TestCloseSqlDB(t *testing.T) {
db := new(gorm.DB)
defer func() { recover() }()
CloseSQLDB(db)
}

View File

@ -1,137 +0,0 @@
package ggorm
import (
"time"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// Option set the mysql options.
type Option func(*options)
type options struct {
isLog bool
slowThreshold time.Duration
maxIdleConns int
maxOpenConns int
connMaxLifetime time.Duration
disableForeignKey bool
enableTrace bool
requestIDKey string
gLog *zap.Logger
logLevel logger.LogLevel
slavesDsn []string
mastersDsn []string
plugins []gorm.Plugin
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// default settings
func defaultOptions() *options {
return &options{
isLog: false, // whether to output logs, default off
slowThreshold: time.Duration(0), // if greater than 0, only print logs that are longer than the threshold, higher priority than isLog
maxIdleConns: 3, // set the maximum number of connections in the idle connection pool
maxOpenConns: 50, // set the maximum number of open database connections
connMaxLifetime: 30 * time.Minute, // sets the maximum amount of time a connection can be reused
disableForeignKey: true, // disables the use of foreign keys, true is recommended for production environments, enabled by default
enableTrace: false, // whether to enable link tracing, default is off
requestIDKey: "", // request id key
gLog: nil, // custom logger
logLevel: logger.Info, // default logLevel
}
}
// WithLogging set log sql, If l=nil, the gorm log library will be used
func WithLogging(l *zap.Logger, level ...logger.LogLevel) Option {
return func(o *options) {
o.isLog = true
o.gLog = l
if len(level) > 0 {
o.logLevel = level[0]
}
o.logLevel = logger.Info
}
}
// WithSlowThreshold Set sql values greater than the threshold
func WithSlowThreshold(d time.Duration) Option {
return func(o *options) {
o.slowThreshold = d
}
}
// WithMaxIdleConns set max idle conns
func WithMaxIdleConns(size int) Option {
return func(o *options) {
o.maxIdleConns = size
}
}
// WithMaxOpenConns set max open conns
func WithMaxOpenConns(size int) Option {
return func(o *options) {
o.maxOpenConns = size
}
}
// WithConnMaxLifetime set conn max lifetime
func WithConnMaxLifetime(t time.Duration) Option {
return func(o *options) {
o.connMaxLifetime = t
}
}
// WithEnableForeignKey use foreign keys
func WithEnableForeignKey() Option {
return func(o *options) {
o.disableForeignKey = false
}
}
// WithEnableTrace use trace
func WithEnableTrace() Option {
return func(o *options) {
o.enableTrace = true
}
}
// WithLogRequestIDKey log request id
func WithLogRequestIDKey(key string) Option {
return func(o *options) {
if key == "" {
key = "request_id"
}
o.requestIDKey = key
}
}
// WithRWSeparation setting read-write separation
func WithRWSeparation(slavesDsn []string, mastersDsn ...string) Option {
return func(o *options) {
o.slavesDsn = slavesDsn
o.mastersDsn = mastersDsn
}
}
// WithGormPlugin setting gorm plugin
func WithGormPlugin(plugins ...gorm.Plugin) Option {
return func(o *options) {
o.plugins = plugins
}
}

View File

@ -1,102 +0,0 @@
package query
import "strings"
var defaultMaxSize = 1000
// SetMaxSize change the default maximum number of pages per page
func SetMaxSize(maxValue int) {
if maxValue < 10 {
maxValue = 10
}
defaultMaxSize = maxValue
}
// Page info
type Page struct {
page int // page number, starting from page 0
limit int // number per page
sort string // sort fields, default is id backwards, you can add - sign before the field to indicate reverse order, no - sign to indicate ascending order, multiple fields separated by comma
}
// Page get page value
func (p *Page) Page() int {
return p.page
}
// Limit number per page
func (p *Page) Limit() int {
return p.limit
}
// Size number per page
// Deprecated: use Limit instead
func (p *Page) Size() int {
return p.limit
}
// Sort get sort field
func (p *Page) Sort() string {
return p.sort
}
// Offset get offset value
func (p *Page) Offset() int {
return p.page * p.limit
}
// DefaultPage default page, number 20 per page, sorted by id backwards
func DefaultPage(page int) *Page {
if page < 0 {
page = 0
}
return &Page{
page: page,
limit: 20,
sort: "id DESC",
}
}
// NewPage custom page, starting from page 0.
// the parameter columnNames indicates a sort field, if empty means id descending,
// if there are multiple column names, separated by a comma,
// a '-' sign in front of each column name indicates descending order, otherwise ascending order.
func NewPage(page int, limit int, columnNames string) *Page {
if page < 0 {
page = 0
}
if limit > defaultMaxSize || limit < 1 {
limit = defaultMaxSize
}
return &Page{
page: page,
limit: limit,
sort: getSort(columnNames),
}
}
// convert to mysql sort, each column name preceded by a '-' sign, indicating descending order, otherwise ascending order, example:
//
// columnNames="name" means sort by name in ascending order,
// columnNames="-name" means sort by name descending,
// columnNames="name,age" means sort by name in ascending order, otherwise sort by age in ascending order,
// columnNames="-name,-age" means sort by name descending before sorting by age descending.
func getSort(columnNames string) string {
columnNames = strings.Replace(columnNames, " ", "", -1)
if columnNames == "" {
return "id DESC"
}
names := strings.Split(columnNames, ",")
strs := make([]string, 0, len(names))
for _, name := range names {
if name[0] == '-' && len(name) > 1 {
strs = append(strs, name[1:]+" DESC")
} else {
strs = append(strs, name+" ASC")
}
}
return strings.Join(strs, ", ")
}

View File

@ -1,233 +0,0 @@
// Package query is a library of custom condition queries, support for complex conditional paging queries.
package query
import (
"fmt"
"strings"
)
const (
// Eq equal
Eq = "eq"
// Neq not equal
Neq = "neq"
// Gt greater than
Gt = "gt"
// Gte greater than or equal
Gte = "gte"
// Lt less than
Lt = "lt"
// Lte less than or equal
Lte = "lte"
// Like fuzzy lookup
Like = "like"
// In include
In = "in"
// AND logic and
AND string = "and"
// OR logic or
OR string = "or"
)
var expMap = map[string]string{
Eq: " = ",
Neq: " <> ",
Gt: " > ",
Gte: " >= ",
Lt: " < ",
Lte: " <= ",
Like: " LIKE ",
In: " IN ",
"=": " = ",
"!=": " <> ",
">": " > ",
">=": " >= ",
"<": " < ",
"<=": " <= ",
}
var logicMap = map[string]string{
AND: " AND ",
OR: " OR ",
"&": " AND ",
"&&": " AND ",
"|": " OR ",
"||": " OR ",
"AND": " AND ",
"OR": " OR ",
}
// Params query parameters
type Params struct {
Page int `json:"page" form:"page" binding:"gte=0"`
Limit int `json:"limit" form:"limit" binding:"gte=1"`
Sort string `json:"sort,omitempty" form:"sort" binding:""`
Columns []Column `json:"columns,omitempty" form:"columns"` // not required
// Deprecated: use Limit instead in sponge version v1.8.6, will remove in the future
Size int `json:"size" form:"size"`
}
// Column query info
type Column struct {
Name string `json:"name" form:"name"` // column name
Exp string `json:"exp" form:"exp"` // expressions, default value is "=", support =, !=, >, >=, <, <=, like, in
Value interface{} `json:"value" form:"value"` // column value
Logic string `json:"logic" form:"logic"` // logical type, defaults to and when the value is null, with &(and), ||(or)
}
func (c *Column) checkValid() error {
if c.Name == "" {
return fmt.Errorf("field 'name' cannot be empty")
}
if c.Value == nil {
return fmt.Errorf("field 'value' cannot be nil")
}
return nil
}
// converting ExpType to sql expressions and LogicType to sql using characters
func (c *Column) convert() error {
if c.Exp == "" {
c.Exp = Eq
}
if v, ok := expMap[strings.ToLower(c.Exp)]; ok { //nolint
c.Exp = v
if c.Exp == " LIKE " {
c.Value = fmt.Sprintf("%%%v%%", c.Value)
}
if c.Exp == " IN " {
val, ok := c.Value.(string)
if !ok {
return fmt.Errorf("invalid value type '%s'", c.Value)
}
iVal := []interface{}{}
ss := strings.Split(val, ",")
for _, s := range ss {
iVal = append(iVal, s)
}
c.Value = iVal
}
} else {
return fmt.Errorf("unknown exp type '%s'", c.Exp)
}
if c.Logic == "" {
c.Logic = AND
}
if v, ok := logicMap[strings.ToLower(c.Logic)]; ok { //nolint
c.Logic = v
} else {
return fmt.Errorf("unknown logic type '%s'", c.Logic)
}
return nil
}
// ConvertToPage converted to page
func (p *Params) ConvertToPage() (order string, limit int, offset int) { //nolint
page := NewPage(p.Page, p.Limit, p.Sort)
order = page.sort
limit = page.limit
offset = page.page * page.limit
return //nolint
}
// ConvertToGormConditions conversion to gorm-compliant parameters based on the Columns parameter
// ignore the logical type of the last column, whether it is a one-column or multi-column query
func (p *Params) ConvertToGormConditions() (string, []interface{}, error) {
str := ""
args := []interface{}{}
l := len(p.Columns)
if l == 0 {
return "", nil, nil
}
isUseIN := true
if l == 1 {
isUseIN = false
}
field := p.Columns[0].Name
for i, column := range p.Columns {
if err := column.checkValid(); err != nil {
return "", nil, err
}
err := column.convert()
if err != nil {
return "", nil, err
}
symbol := "?"
if column.Exp == " IN " {
symbol = "(?)"
}
if i == l-1 { // ignore the logical type of the last column
str += column.Name + column.Exp + symbol
} else {
str += column.Name + column.Exp + symbol + column.Logic
}
args = append(args, column.Value)
// when multiple columns are the same, determine whether the use of IN
if isUseIN {
if field != column.Name {
isUseIN = false
continue
}
if column.Exp != expMap[Eq] {
isUseIN = false
}
}
}
if isUseIN {
str = field + " IN (?)"
args = []interface{}{args}
}
return str, args, nil
}
// Conditions query conditions
type Conditions struct {
Columns []Column `json:"columns" form:"columns" binding:"min=1"` // columns info
}
// CheckValid check valid
func (c *Conditions) CheckValid() error {
if len(c.Columns) == 0 {
return fmt.Errorf("field 'columns' cannot be empty")
}
for _, column := range c.Columns {
err := column.checkValid()
if err != nil {
return err
}
if column.Exp != "" {
if _, ok := expMap[column.Exp]; !ok {
return fmt.Errorf("unknown exp type '%s'", column.Exp)
}
}
if column.Logic != "" {
if _, ok := logicMap[column.Logic]; !ok {
return fmt.Errorf("unknown logic type '%s'", column.Logic)
}
}
}
return nil
}
// ConvertToGorm conversion to gorm-compliant parameters based on the Columns parameter
// ignore the logical type of the last column, whether it is a one-column or multi-column query
func (c *Conditions) ConvertToGorm() (string, []interface{}, error) {
p := &Params{Columns: c.Columns}
return p.ConvertToGormConditions()
}

View File

@ -1,541 +0,0 @@
package query
import (
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPage(t *testing.T) {
page := DefaultPage(-1)
t.Log(page.Page(), page.Limit(), page.Sort(), page.Offset())
SetMaxSize(1)
page = NewPage(-1, 100, "id")
t.Log(page.Page(), page.Limit(), page.Sort(), page.Offset())
}
func TestParams_ConvertToPage(t *testing.T) {
p := &Params{
Page: 1,
Limit: 50,
Sort: "age,-name",
}
order, limit, offset := p.ConvertToPage()
t.Logf("order=%s, limit=%d, offset=%d", order, limit, offset)
}
func TestParams_ConvertToGormConditions(t *testing.T) {
type args struct {
columns []Column
}
tests := []struct {
name string
args args
want string
want1 []interface{}
wantErr bool
}{
// --------------------------- only 1 column query ------------------------------
{
name: "1 column eq",
args: args{
columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
},
},
want: "name = ?",
want1: []interface{}{"ZhangSan"},
wantErr: false,
},
{
name: "1 column neq",
args: args{
columns: []Column{
{
Name: "name",
Value: "ZhangSan",
//Exp: "neq",
Exp: "!=",
},
},
},
want: "name <> ?",
want1: []interface{}{"ZhangSan"},
wantErr: false,
},
{
name: "1 column gt",
args: args{
columns: []Column{
{
Name: "age",
Value: 20,
//Exp: Gt,
Exp: ">",
},
},
},
want: "age > ?",
want1: []interface{}{20},
wantErr: false,
},
{
name: "1 column gte",
args: args{
columns: []Column{
{
Name: "age",
Value: 20,
//Exp: Gte,
Exp: ">=",
},
},
},
want: "age >= ?",
want1: []interface{}{20},
wantErr: false,
},
{
name: "1 column lt",
args: args{
columns: []Column{
{
Name: "age",
Value: 20,
//Exp: Lt,
Exp: "<",
},
},
},
want: "age < ?",
want1: []interface{}{20},
wantErr: false,
},
{
name: "1 column lte",
args: args{
columns: []Column{
{
Name: "age",
Value: 20,
//Exp: Lte,
Exp: "<=",
},
},
},
want: "age <= ?",
want1: []interface{}{20},
wantErr: false,
},
{
name: "1 column like",
args: args{
columns: []Column{
{
Name: "name",
Value: "Li",
Exp: Like,
},
},
},
want: "name LIKE ?",
want1: []interface{}{"%Li%"},
wantErr: false,
},
{
name: "1 column IN",
args: args{
columns: []Column{
{
Name: "name",
Value: "ab,cd,ef",
Exp: In,
},
},
},
want: "name IN (?)",
want1: []interface{}{[]interface{}{"ab", "cd", "ef"}},
wantErr: false,
},
// --------------------------- query 2 columns ------------------------------
{
name: "2 columns eq and",
args: args{
columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "gender",
Value: "male",
},
},
},
want: "name = ? AND gender = ?",
want1: []interface{}{"ZhangSan", "male"},
wantErr: false,
},
{
name: "2 columns neq and",
args: args{
columns: []Column{
{
Name: "name",
Value: "ZhangSan",
//Exp: Neq,
Exp: "!=",
},
{
Name: "name",
Value: "LiSi",
//Exp: Neq,
Exp: "!=",
},
},
},
want: "name <> ? AND name <> ?",
want1: []interface{}{"ZhangSan", "LiSi"},
wantErr: false,
},
{
name: "2 columns gt and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
},
{
Name: "age",
Value: 20,
//Exp: Gt,
Exp: ">",
},
},
},
want: "gender = ? AND age > ?",
want1: []interface{}{"male", 20},
wantErr: false,
},
{
name: "2 columns gte and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
},
{
Name: "age",
Value: 20,
//Exp: Gte,
Exp: ">=",
},
},
},
want: "gender = ? AND age >= ?",
want1: []interface{}{"male", 20},
wantErr: false,
},
{
name: "2 columns lt and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "female",
},
{
Name: "age",
Value: 20,
//Exp: Lt,
Exp: "<",
},
},
},
want: "gender = ? AND age < ?",
want1: []interface{}{"female", 20},
wantErr: false,
},
{
name: "2 columns lte and",
args: args{
columns: []Column{
{
Name: "gender",
Value: "female",
},
{
Name: "age",
Value: 20,
//Exp: Lte,
Exp: "<=",
},
},
},
want: "gender = ? AND age <= ?",
want1: []interface{}{"female", 20},
wantErr: false,
},
{
name: "2 columns range and",
args: args{
columns: []Column{
{
Name: "age",
Value: 10,
//Exp: Gte,
Exp: ">=",
},
{
Name: "age",
Value: 20,
//Exp: Lte,
Exp: "<=",
},
},
},
want: "age >= ? AND age <= ?",
want1: []interface{}{10, 20},
wantErr: false,
},
{
name: "2 columns eq or",
args: args{
columns: []Column{
{
Name: "name",
Value: "LiSi",
//Logic: OR,
Logic: "||",
},
{
Name: "gender",
Value: "female",
},
},
},
want: "name = ? OR gender = ?",
want1: []interface{}{"LiSi", "female"},
wantErr: false,
},
{
name: "2 columns neq or",
args: args{
columns: []Column{
{
Name: "name",
Value: "LiSi",
//Logic: OR,
Logic: "||",
},
{
Name: "gender",
Value: "male",
//Exp: Neq,
Exp: "!=",
},
},
},
want: "name = ? OR gender <> ?",
want1: []interface{}{"LiSi", "male"},
wantErr: false,
},
{
name: "2 columns eq and in",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
//Logic: "&",
},
{
Name: "name",
Value: "LiSi,ZhangSan,WangWu",
Exp: In,
},
},
},
want: "gender = ? AND name IN (?)",
want1: []interface{}{"male", []interface{}{"LiSi", "ZhangSan", "WangWu"}},
wantErr: false,
},
// ------------------------------ IN -------------------------------------------------
{
name: "3 columns eq and",
args: args{
columns: []Column{
{
Name: "name",
Value: "LiSi",
},
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "name",
Value: "WangWu",
},
},
},
want: "name IN (?)",
want1: []interface{}{[]interface{}{"LiSi", "ZhangSan", "WangWu"}},
wantErr: false,
},
// ---------------------------- error ----------------------------------------------
{
name: "exp type err",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
Exp: "xxxxxx",
},
},
},
want: "",
want1: nil,
wantErr: true,
},
{
name: "logic type err",
args: args{
columns: []Column{
{
Name: "gender",
Value: "male",
Logic: "xxxxxx",
},
},
},
want: "",
want1: nil,
wantErr: true,
},
{
name: "empty",
args: args{
columns: nil,
},
want: "",
want1: nil,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
params := &Params{
Columns: tt.args.columns,
}
got, got1, err := params.ConvertToGormConditions()
if (err != nil) != tt.wantErr {
t.Errorf("ConvertToGormConditions() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ConvertToGormConditions() got = %v, want %v", got, tt.want)
}
if !reflect.DeepEqual(got1, tt.want1) {
t.Errorf("ConvertToGormConditions() got1 = %v, want %v", got1, tt.want1)
}
got = strings.Replace(got, "?", "%v", -1)
t.Logf(got, got1...)
})
}
}
func TestConditions_ConvertToGorm(t *testing.T) {
c := Conditions{
Columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "gender",
Value: "male",
},
}}
str, values, err := c.ConvertToGorm()
if err != nil {
t.Error(err)
}
assert.Equal(t, "name = ? AND gender = ?", str)
assert.Equal(t, len(values), 2)
}
func TestConditions_checkValid(t *testing.T) {
// empty error
c := Conditions{}
err := c.CheckValid()
assert.Error(t, err)
// value is empty error
c = Conditions{
Columns: []Column{
{
Name: "foo",
Value: nil,
},
},
}
err = c.CheckValid()
assert.Error(t, err)
// exp error
c = Conditions{
Columns: []Column{
{
Name: "foo",
Value: "bar",
Exp: "unknown-exp",
},
},
}
err = c.CheckValid()
assert.Error(t, err)
// logic error
c = Conditions{
Columns: []Column{
{
Name: "foo",
Value: "bar",
Logic: "unknown-logic",
},
},
}
err = c.CheckValid()
assert.Error(t, err)
// success
c = Conditions{
Columns: []Column{
{
Name: "name",
Value: "ZhangSan",
},
{
Name: "gender",
Value: "male",
},
}}
err = c.CheckValid()
assert.NoError(t, err)
}

Binary file not shown.