diff --git a/.github/workflows/docker-compose.yml b/.github/workflows/docker-compose.yml index 758de0dcb..fc75ef92d 100644 --- a/.github/workflows/docker-compose.yml +++ b/.github/workflows/docker-compose.yml @@ -1,17 +1,18 @@ -version: '2' +version: "2" services: redis-master: container_name: redis-master - image: 'bitnami/redis:latest' + image: "loads/redis:7.0" environment: - REDIS_REPLICATION_MODE=master - REDIS_PASSWORD=111111 ports: - 6380:6379 + redis-slave1: container_name: redis-slave1 - image: 'bitnami/redis:latest' + image: "loads/redis:7.0" environment: - REDIS_REPLICATION_MODE=slave - REDIS_MASTER_HOST=redis-master @@ -23,9 +24,10 @@ services: - redis-master links: - redis-master + redis-slave2: container_name: redis-slave2 - image: 'bitnami/redis:latest' + image: "loads/redis:7.0" environment: - REDIS_REPLICATION_MODE=slave - REDIS_MASTER_HOST=redis-master @@ -37,9 +39,10 @@ services: - redis-master links: - redis-master + redis-sentinel-1: container_name: redis-sentinel-1 - image: 'bitnami/redis-sentinel:latest' + image: "loads/redis-sentinel:7.0" environment: - REDIS_MASTER_HOST=redis-master - REDIS_MASTER_PORT_NUMBER=6379 @@ -57,7 +60,7 @@ services: redis-sentinel-2: container_name: redis-sentinel-2 - image: 'bitnami/redis-sentinel:latest' + image: "loads/redis-sentinel:7.0" environment: - REDIS_MASTER_HOST=redis-master - REDIS_MASTER_PORT_NUMBER=6379 @@ -73,10 +76,9 @@ services: ports: - 26380:26379 - redis-sentinel-3: container_name: redis-sentinel-3 - image: 'bitnami/redis-sentinel:latest' + image: "loads/redis-sentinel:7.0" environment: - REDIS_MASTER_HOST=redis-master - REDIS_MASTER_PORT_NUMBER=6379 diff --git a/cmd/gf/internal/cmd/cmd_gen_dao.go b/cmd/gf/internal/cmd/cmd_gen_dao.go index af8ab107f..f805350b6 100644 --- a/cmd/gf/internal/cmd/cmd_gen_dao.go +++ b/cmd/gf/internal/cmd/cmd_gen_dao.go @@ -1,814 +1,15 @@ package cmd import ( - "bytes" - "context" - "fmt" - "strings" - - "github.com/gogf/gf/cmd/gf/v2/internal/consts" - "github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog" - "github.com/gogf/gf/cmd/gf/v2/internal/utility/utils" - "github.com/gogf/gf/v2/container/garray" - "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/os/gfile" - "github.com/gogf/gf/v2/os/gtime" - "github.com/gogf/gf/v2/text/gregex" - "github.com/gogf/gf/v2/text/gstr" - "github.com/gogf/gf/v2/util/gtag" - "github.com/olekukonko/tablewriter" - _ "github.com/gogf/gf/contrib/drivers/mssql/v2" _ "github.com/gogf/gf/contrib/drivers/mysql/v2" _ "github.com/gogf/gf/contrib/drivers/pgsql/v2" _ "github.com/gogf/gf/contrib/drivers/sqlite/v2" //_ "github.com/gogf/gf/contrib/drivers/oracle/v2" + + "github.com/gogf/gf/cmd/gf/v2/internal/cmd/gendao" ) -const ( - cGenDaoConfig = `gfcli.gen.dao` - cGenDaoUsage = `gf gen dao [OPTION]` - cGenDaoBrief = `automatically generate go files for dao/do/entity` - cGenDaoEg = ` -gf gen dao -gf gen dao -l "mysql:root:12345678@tcp(127.0.0.1:3306)/test" -gf gen dao -p ./model -g user-center -t user,user_detail,user_login -gf gen dao -r user_ -` - - cGenDaoAd = ` -CONFIGURATION SUPPORT - Options are also supported by configuration file. - It's suggested using configuration file instead of command line arguments making producing. - The configuration node name is "gfcli.gen.dao", which also supports multiple databases, for example(config.yaml): - gfcli: - gen: - dao: - - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" - tables: "order,products" - jsonCase: "CamelLower" - - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/primary" - path: "./my-app" - prefix: "primary_" - tables: "user, userDetail" -` - cGenDaoBriefPath = `directory path for generated files` - cGenDaoBriefLink = `database configuration, the same as the ORM configuration of GoFrame` - cGenDaoBriefTables = `generate models only for given tables, multiple table names separated with ','` - cGenDaoBriefTablesEx = `generate models excluding given tables, multiple table names separated with ','` - cGenDaoBriefPrefix = `add prefix for all table of specified link/database tables` - cGenDaoBriefRemovePrefix = `remove specified prefix of the table, multiple prefix separated with ','` - cGenDaoBriefStdTime = `use time.Time from stdlib instead of gtime.Time for generated time/date fields of tables` - cGenDaoBriefWithTime = `add created time for auto produced go files` - cGenDaoBriefGJsonSupport = `use gJsonSupport to use *gjson.Json instead of string for generated json fields of tables` - cGenDaoBriefImportPrefix = `custom import prefix for generated go files` - cGenDaoBriefDaoPath = `directory path for storing generated dao files under path` - cGenDaoBriefDoPath = `directory path for storing generated do files under path` - cGenDaoBriefEntityPath = `directory path for storing generated entity files under path` - cGenDaoBriefOverwriteDao = `overwrite all dao files both inside/outside internal folder` - cGenDaoBriefModelFile = `custom file name for storing generated model content` - cGenDaoBriefModelFileForDao = `custom file name generating model for DAO operations like Where/Data. It's empty in default` - cGenDaoBriefDescriptionTag = `add comment to description tag for each field` - cGenDaoBriefNoJsonTag = `no json tag will be added for each field` - cGenDaoBriefNoModelComment = `no model comment will be added for each field` - cGenDaoBriefGroup = ` -specifying the configuration group name of database for generated ORM instance, -it's not necessary and the default value is "default" -` - cGenDaoBriefJsonCase = ` -generated json tag case for model struct, cases are as follows: -| Case | Example | -|---------------- |--------------------| -| Camel | AnyKindOfString | -| CamelLower | anyKindOfString | default -| Snake | any_kind_of_string | -| SnakeScreaming | ANY_KIND_OF_STRING | -| SnakeFirstUpper | rgb_code_md5 | -| Kebab | any-kind-of-string | -| KebabScreaming | ANY-KIND-OF-STRING | -` - - tplVarTableName = `{TplTableName}` - tplVarTableNameCamelCase = `{TplTableNameCamelCase}` - tplVarTableNameCamelLowerCase = `{TplTableNameCamelLowerCase}` - tplVarPackageImports = `{TplPackageImports}` - tplVarImportPrefix = `{TplImportPrefix}` - tplVarStructDefine = `{TplStructDefine}` - tplVarColumnDefine = `{TplColumnDefine}` - tplVarColumnNames = `{TplColumnNames}` - tplVarGroupName = `{TplGroupName}` - tplVarDatetimeStr = `{TplDatetimeStr}` -) - -var ( - createdAt = gtime.Now() -) - -func init() { - gtag.Sets(g.MapStrStr{ - `cGenDaoConfig`: cGenDaoConfig, - `cGenDaoUsage`: cGenDaoUsage, - `cGenDaoBrief`: cGenDaoBrief, - `cGenDaoEg`: cGenDaoEg, - `cGenDaoAd`: cGenDaoAd, - `cGenDaoBriefPath`: cGenDaoBriefPath, - `cGenDaoBriefLink`: cGenDaoBriefLink, - `cGenDaoBriefTables`: cGenDaoBriefTables, - `cGenDaoBriefTablesEx`: cGenDaoBriefTablesEx, - `cGenDaoBriefPrefix`: cGenDaoBriefPrefix, - `cGenDaoBriefRemovePrefix`: cGenDaoBriefRemovePrefix, - `cGenDaoBriefStdTime`: cGenDaoBriefStdTime, - `cGenDaoBriefWithTime`: cGenDaoBriefWithTime, - `cGenDaoBriefDaoPath`: cGenDaoBriefDaoPath, - `cGenDaoBriefDoPath`: cGenDaoBriefDoPath, - `cGenDaoBriefEntityPath`: cGenDaoBriefEntityPath, - `cGenDaoBriefGJsonSupport`: cGenDaoBriefGJsonSupport, - `cGenDaoBriefImportPrefix`: cGenDaoBriefImportPrefix, - `cGenDaoBriefOverwriteDao`: cGenDaoBriefOverwriteDao, - `cGenDaoBriefModelFile`: cGenDaoBriefModelFile, - `cGenDaoBriefModelFileForDao`: cGenDaoBriefModelFileForDao, - `cGenDaoBriefDescriptionTag`: cGenDaoBriefDescriptionTag, - `cGenDaoBriefNoJsonTag`: cGenDaoBriefNoJsonTag, - `cGenDaoBriefNoModelComment`: cGenDaoBriefNoModelComment, - `cGenDaoBriefGroup`: cGenDaoBriefGroup, - `cGenDaoBriefJsonCase`: cGenDaoBriefJsonCase, - }) -} - type ( - cGenDao struct{} - cGenDaoInput struct { - g.Meta `name:"dao" config:"{cGenDaoConfig}" usage:"{cGenDaoUsage}" brief:"{cGenDaoBrief}" eg:"{cGenDaoEg}" ad:"{cGenDaoAd}"` - Path string `name:"path" short:"p" brief:"{cGenDaoBriefPath}" d:"internal"` - Link string `name:"link" short:"l" brief:"{cGenDaoBriefLink}"` - Tables string `name:"tables" short:"t" brief:"{cGenDaoBriefTables}"` - TablesEx string `name:"tablesEx" short:"x" brief:"{cGenDaoBriefTablesEx}"` - Group string `name:"group" short:"g" brief:"{cGenDaoBriefGroup}" d:"default"` - Prefix string `name:"prefix" short:"f" brief:"{cGenDaoBriefPrefix}"` - RemovePrefix string `name:"removePrefix" short:"r" brief:"{cGenDaoBriefRemovePrefix}"` - JsonCase string `name:"jsonCase" short:"j" brief:"{cGenDaoBriefJsonCase}" d:"CamelLower"` - ImportPrefix string `name:"importPrefix" short:"i" brief:"{cGenDaoBriefImportPrefix}"` - DaoPath string `name:"daoPath" short:"d" brief:"{cGenDaoBriefDaoPath}" d:"dao"` - DoPath string `name:"doPath" short:"o" brief:"{cGenDaoBriefDoPath}" d:"model/do"` - EntityPath string `name:"entityPath" short:"e" brief:"{cGenDaoBriefEntityPath}" d:"model/entity"` - StdTime bool `name:"stdTime" short:"s" brief:"{cGenDaoBriefStdTime}" orphan:"true"` - WithTime bool `name:"withTime" short:"w" brief:"{cGenDaoBriefWithTime}" orphan:"true"` - GJsonSupport bool `name:"gJsonSupport" short:"n" brief:"{cGenDaoBriefGJsonSupport}" orphan:"true"` - OverwriteDao bool `name:"overwriteDao" short:"v" brief:"{cGenDaoBriefOverwriteDao}" orphan:"true"` - DescriptionTag bool `name:"descriptionTag" short:"c" brief:"{cGenDaoBriefDescriptionTag}" orphan:"true"` - NoJsonTag bool `name:"noJsonTag" short:"k" brief:"{cGenDaoBriefNoJsonTag" orphan:"true"` - NoModelComment bool `name:"noModelComment" short:"m" brief:"{cGenDaoBriefNoModelComment}" orphan:"true"` - } - cGenDaoOutput struct{} - - cGenDaoInternalInput struct { - cGenDaoInput - TableName string // TableName specifies the table name of the table. - NewTableName string // NewTableName specifies the prefix-stripped name of the table. - ModName string // ModName specifies the module name of current golang project, which is used for import purpose. - } + cGenDao = gendao.CGenDao ) - -func (c cGenDao) Dao(ctx context.Context, in cGenDaoInput) (out *cGenDaoOutput, err error) { - if g.Cfg().Available(ctx) { - v := g.Cfg().MustGet(ctx, cGenDaoConfig) - if v.IsSlice() { - for i := 0; i < len(v.Interfaces()); i++ { - doGenDaoForArray(ctx, i, in) - } - } else { - doGenDaoForArray(ctx, -1, in) - } - } else { - doGenDaoForArray(ctx, -1, in) - } - mlog.Print("done!") - return -} - -// doGenDaoForArray implements the "gen dao" command for configuration array. -func doGenDaoForArray(ctx context.Context, index int, in cGenDaoInput) { - var ( - err error - db gdb.DB - modName string // Go module name, eg: github.com/gogf/gf. - ) - if index >= 0 { - err = g.Cfg().MustGet( - ctx, - fmt.Sprintf(`%s.%d`, cGenDaoConfig, index), - ).Scan(&in) - if err != nil { - mlog.Fatalf(`invalid configuration of "%s": %+v`, cGenDaoConfig, err) - } - } - if dirRealPath := gfile.RealPath(in.Path); dirRealPath == "" { - mlog.Fatalf(`path "%s" does not exist`, in.Path) - } - removePrefixArray := gstr.SplitAndTrim(in.RemovePrefix, ",") - if in.ImportPrefix == "" { - if !gfile.Exists("go.mod") { - mlog.Fatal("go.mod does not exist in current working directory") - } - var ( - goModContent = gfile.GetContents("go.mod") - match, _ = gregex.MatchString(`^module\s+(.+)\s*`, goModContent) - ) - if len(match) > 1 { - modName = gstr.Trim(match[1]) - } else { - mlog.Fatal("module name does not found in go.mod") - } - } - - // It uses user passed database configuration. - if in.Link != "" { - var tempGroup = gtime.TimestampNanoStr() - gdb.AddConfigNode(tempGroup, gdb.ConfigNode{ - Link: in.Link, - }) - if db, err = gdb.Instance(tempGroup); err != nil { - mlog.Fatalf(`database initialization failed: %+v`, err) - } - } else { - db = g.DB(in.Group) - } - if db == nil { - mlog.Fatal(`database initialization failed, may be invalid database configuration`) - } - - var tableNames []string - if in.Tables != "" { - tableNames = gstr.SplitAndTrim(in.Tables, ",") - } else { - tableNames, err = db.Tables(context.TODO()) - if err != nil { - mlog.Fatalf("fetching tables failed: %+v", err) - } - } - // Table excluding. - if in.TablesEx != "" { - array := garray.NewStrArrayFrom(tableNames) - for _, v := range gstr.SplitAndTrim(in.TablesEx, ",") { - array.RemoveValue(v) - } - tableNames = array.Slice() - } - - // Generating dao & model go files one by one according to given table name. - newTableNames := make([]string, len(tableNames)) - for i, tableName := range tableNames { - newTableName := tableName - for _, v := range removePrefixArray { - newTableName = gstr.TrimLeftStr(newTableName, v, 1) - } - newTableName = in.Prefix + newTableName - newTableNames[i] = newTableName - // Dao. - generateDao(ctx, db, cGenDaoInternalInput{ - cGenDaoInput: in, - TableName: tableName, - NewTableName: newTableName, - ModName: modName, - }) - } - // Do. - generateDo(ctx, db, tableNames, newTableNames, cGenDaoInternalInput{ - cGenDaoInput: in, - ModName: modName, - }) - // Entity. - generateEntity(ctx, db, tableNames, newTableNames, cGenDaoInternalInput{ - cGenDaoInput: in, - ModName: modName, - }) -} - -// generateDaoContentFile generates the dao and model content of given table. -func generateDao(ctx context.Context, db gdb.DB, in cGenDaoInternalInput) { - // Generating table data preparing. - fieldMap, err := db.TableFields(ctx, in.TableName) - if err != nil { - mlog.Fatalf(`fetching tables fields failed for table "%s": %+v`, in.TableName, err) - } - var ( - dirRealPath = gfile.RealPath(in.Path) - dirPathDao = gfile.Join(in.Path, in.DaoPath) - tableNameCamelCase = gstr.CaseCamel(in.NewTableName) - tableNameCamelLowerCase = gstr.CaseCamelLower(in.NewTableName) - tableNameSnakeCase = gstr.CaseSnake(in.NewTableName) - importPrefix = in.ImportPrefix - ) - if importPrefix == "" { - if dirRealPath == "" { - dirRealPath = in.Path - importPrefix = dirRealPath - importPrefix = gstr.Trim(dirRealPath, "./") - } else { - importPrefix = gstr.Replace(dirRealPath, gfile.Pwd(), "") - } - importPrefix = gstr.Replace(importPrefix, gfile.Separator, "/") - importPrefix = gstr.Join(g.SliceStr{in.ModName, importPrefix, in.DaoPath}, "/") - importPrefix, _ = gregex.ReplaceString(`\/{2,}`, `/`, gstr.Trim(importPrefix, "/")) - } else { - importPrefix = gstr.Join(g.SliceStr{importPrefix, in.DaoPath}, "/") - } - - fileName := gstr.Trim(tableNameSnakeCase, "-_.") - if len(fileName) > 5 && fileName[len(fileName)-5:] == "_test" { - // Add suffix to avoid the table name which contains "_test", - // which would make the go file a testing file. - fileName += "_table" - } - - // dao - index - generateDaoIndex(in, tableNameCamelCase, tableNameCamelLowerCase, importPrefix, dirPathDao, fileName) - - // dao - internal - generateDaoInternal(in, tableNameCamelCase, tableNameCamelLowerCase, importPrefix, dirPathDao, fileName, fieldMap) -} - -func generateDo(ctx context.Context, db gdb.DB, tableNames, newTableNames []string, in cGenDaoInternalInput) { - var ( - doDirPath = gfile.Join(in.Path, in.DoPath) - ) - in.NoJsonTag = true - in.DescriptionTag = false - in.NoModelComment = false - // Model content. - for i, tableName := range tableNames { - in.TableName = tableName - fieldMap, err := db.TableFields(ctx, tableName) - if err != nil { - mlog.Fatalf("fetching tables fields failed for table '%s':\n%v", in.TableName, err) - } - var ( - newTableName = newTableNames[i] - doFilePath = gfile.Join(doDirPath, gstr.CaseSnake(newTableName)+".go") - structDefinition = generateStructDefinition(generateStructDefinitionInput{ - cGenDaoInternalInput: in, - StructName: gstr.CaseCamel(newTableName), - FieldMap: fieldMap, - IsDo: true, - }) - ) - // replace all types to interface{}. - structDefinition, _ = gregex.ReplaceStringFuncMatch( - "([A-Z]\\w*?)\\s+([\\w\\*\\.]+?)\\s+(//)", - structDefinition, - func(match []string) string { - // If the type is already a pointer/slice/map, it does nothing. - if !gstr.HasPrefix(match[2], "*") && !gstr.HasPrefix(match[2], "[]") && !gstr.HasPrefix(match[2], "map") { - return fmt.Sprintf(`%s interface{} %s`, match[1], match[3]) - } - return match[0] - }, - ) - modelContent := generateDoContent( - in, - tableName, - gstr.CaseCamel(newTableName), - structDefinition, - ) - err = gfile.PutContents(doFilePath, strings.TrimSpace(modelContent)) - if err != nil { - mlog.Fatalf(`writing content to "%s" failed: %v`, doFilePath, err) - } else { - utils.GoFmt(doFilePath) - mlog.Print("generated:", doFilePath) - } - } -} - -func generateEntity(ctx context.Context, db gdb.DB, tableNames, newTableNames []string, in cGenDaoInternalInput) { - var ( - entityDirPath = gfile.Join(in.Path, in.EntityPath) - ) - - // Model content. - for i, tableName := range tableNames { - fieldMap, err := db.TableFields(ctx, tableName) - if err != nil { - mlog.Fatalf("fetching tables fields failed for table '%s':\n%v", in.TableName, err) - } - var ( - newTableName = newTableNames[i] - entityFilePath = gfile.Join(entityDirPath, gstr.CaseSnake(newTableName)+".go") - entityContent = generateEntityContent( - in, - newTableName, - gstr.CaseCamel(newTableName), - generateStructDefinition(generateStructDefinitionInput{ - cGenDaoInternalInput: in, - StructName: gstr.CaseCamel(newTableName), - FieldMap: fieldMap, - IsDo: false, - }), - ) - ) - err = gfile.PutContents(entityFilePath, strings.TrimSpace(entityContent)) - if err != nil { - mlog.Fatalf("writing content to '%s' failed: %v", entityFilePath, err) - } else { - utils.GoFmt(entityFilePath) - mlog.Print("generated:", entityFilePath) - } - } -} - -func getImportPartContent(source string, isDo bool) string { - var ( - packageImportsArray = garray.NewStrArray() - ) - - if isDo { - packageImportsArray.Append(`"github.com/gogf/gf/v2/frame/g"`) - } - - // Time package recognition. - if strings.Contains(source, "gtime.Time") { - packageImportsArray.Append(`"github.com/gogf/gf/v2/os/gtime"`) - } else if strings.Contains(source, "time.Time") { - packageImportsArray.Append(`"time"`) - } - - // Json type. - if strings.Contains(source, "gjson.Json") { - packageImportsArray.Append(`"github.com/gogf/gf/v2/encoding/gjson"`) - } - - // Generate and write content to golang file. - packageImportsStr := "" - if packageImportsArray.Len() > 0 { - packageImportsStr = fmt.Sprintf("import(\n%s\n)", packageImportsArray.Join("\n")) - } - return packageImportsStr -} - -func generateEntityContent(in cGenDaoInternalInput, tableName, tableNameCamelCase, structDefine string) string { - entityContent := gstr.ReplaceByMap(consts.TemplateGenDaoEntityContent, g.MapStrStr{ - tplVarTableName: tableName, - tplVarPackageImports: getImportPartContent(structDefine, false), - tplVarTableNameCamelCase: tableNameCamelCase, - tplVarStructDefine: structDefine, - }) - entityContent = replaceDefaultVar(in, entityContent) - return entityContent -} - -func generateDoContent(in cGenDaoInternalInput, tableName, tableNameCamelCase, structDefine string) string { - doContent := gstr.ReplaceByMap(consts.TemplateGenDaoDoContent, g.MapStrStr{ - tplVarTableName: tableName, - tplVarPackageImports: getImportPartContent(structDefine, true), - tplVarTableNameCamelCase: tableNameCamelCase, - tplVarStructDefine: structDefine, - }) - doContent = replaceDefaultVar(in, doContent) - return doContent -} - -func generateDaoIndex(in cGenDaoInternalInput, tableNameCamelCase, tableNameCamelLowerCase, importPrefix, dirPathDao, fileName string) { - path := gfile.Join(dirPathDao, fileName+".go") - if in.OverwriteDao || !gfile.Exists(path) { - indexContent := gstr.ReplaceByMap(getTplDaoIndexContent(""), g.MapStrStr{ - tplVarImportPrefix: importPrefix, - tplVarTableName: in.TableName, - tplVarTableNameCamelCase: tableNameCamelCase, - tplVarTableNameCamelLowerCase: tableNameCamelLowerCase, - }) - indexContent = replaceDefaultVar(in, indexContent) - if err := gfile.PutContents(path, strings.TrimSpace(indexContent)); err != nil { - mlog.Fatalf("writing content to '%s' failed: %v", path, err) - } else { - utils.GoFmt(path) - mlog.Print("generated:", path) - } - } -} - -func generateDaoInternal( - in cGenDaoInternalInput, - tableNameCamelCase, tableNameCamelLowerCase, importPrefix string, - dirPathDao, fileName string, - fieldMap map[string]*gdb.TableField, -) { - path := gfile.Join(dirPathDao, "internal", fileName+".go") - modelContent := gstr.ReplaceByMap(getTplDaoInternalContent(""), g.MapStrStr{ - tplVarImportPrefix: importPrefix, - tplVarTableName: in.TableName, - tplVarGroupName: in.Group, - tplVarTableNameCamelCase: tableNameCamelCase, - tplVarTableNameCamelLowerCase: tableNameCamelLowerCase, - tplVarColumnDefine: gstr.Trim(generateColumnDefinitionForDao(fieldMap)), - tplVarColumnNames: gstr.Trim(generateColumnNamesForDao(fieldMap)), - }) - modelContent = replaceDefaultVar(in, modelContent) - if err := gfile.PutContents(path, strings.TrimSpace(modelContent)); err != nil { - mlog.Fatalf("writing content to '%s' failed: %v", path, err) - } else { - utils.GoFmt(path) - mlog.Print("generated:", path) - } -} - -func replaceDefaultVar(in cGenDaoInternalInput, origin string) string { - var tplDatetimeStr string - if in.WithTime { - tplDatetimeStr = fmt.Sprintf(`Created at %s`, createdAt.String()) - } - return gstr.ReplaceByMap(origin, g.MapStrStr{ - tplVarDatetimeStr: tplDatetimeStr, - }) -} - -type generateStructDefinitionInput struct { - cGenDaoInternalInput - StructName string // Struct name. - FieldMap map[string]*gdb.TableField // Table field map. - IsDo bool // Is generating DTO struct. -} - -func generateStructDefinition(in generateStructDefinitionInput) string { - buffer := bytes.NewBuffer(nil) - array := make([][]string, len(in.FieldMap)) - names := sortFieldKeyForDao(in.FieldMap) - for index, name := range names { - field := in.FieldMap[name] - array[index] = generateStructFieldDefinition(field, in) - } - tw := tablewriter.NewWriter(buffer) - tw.SetBorder(false) - tw.SetRowLine(false) - tw.SetAutoWrapText(false) - tw.SetColumnSeparator("") - tw.AppendBulk(array) - tw.Render() - stContent := buffer.String() - // Let's do this hack of table writer for indent! - stContent = gstr.Replace(stContent, " #", "") - stContent = gstr.Replace(stContent, "` ", "`") - stContent = gstr.Replace(stContent, "``", "") - buffer.Reset() - buffer.WriteString(fmt.Sprintf("type %s struct {\n", in.StructName)) - if in.IsDo { - buffer.WriteString(fmt.Sprintf("g.Meta `orm:\"table:%s, do:true\"`\n", in.TableName)) - } - buffer.WriteString(stContent) - buffer.WriteString("}") - return buffer.String() -} - -// generateStructFieldForModel generates and returns the attribute definition for specified field. -func generateStructFieldDefinition(field *gdb.TableField, in generateStructDefinitionInput) []string { - var ( - typeName string - jsonTag = getJsonTagFromCase(field.Name, in.JsonCase) - ) - t, _ := gregex.ReplaceString(`\(.+\)`, "", field.Type) - t = gstr.Split(gstr.Trim(t), " ")[0] - t = gstr.ToLower(t) - - switch t { - case "binary", "varbinary", "blob", "tinyblob", "mediumblob", "longblob": - typeName = "[]byte" - - case "bit", "int", "int2", "tinyint", "small_int", "smallint", "medium_int", "mediumint", "serial": - if gstr.ContainsI(field.Type, "unsigned") { - typeName = "uint" - } else { - typeName = "int" - } - - case "int4", "int8", "big_int", "bigint", "bigserial": - if gstr.ContainsI(field.Type, "unsigned") { - typeName = "uint64" - } else { - typeName = "int64" - } - case "_int2": - if gstr.ContainsI(field.Type, "unsigned") { - typeName = "[]uint" - } else { - typeName = "[]int" - } - case "_int4", "_int8": - if gstr.ContainsI(field.Type, "unsigned") { - typeName = "[]uint64" - } else { - typeName = "[]int64" - } - case "real": - typeName = "float32" - - case "float", "double", "decimal", "smallmoney", "numeric": - typeName = "float64" - - case "bool": - typeName = "bool" - - case "datetime", "timestamp", "date", "time": - if in.StdTime { - typeName = "time.Time" - } else { - typeName = "*gtime.Time" - } - case "json", "jsonb": - if in.GJsonSupport { - typeName = "*gjson.Json" - } else { - typeName = "string" - } - default: - // Automatically detect its data type. - switch { - case strings.Contains(t, "int"): - typeName = "int" - case strings.Contains(t, "text") || strings.Contains(t, "char"): - typeName = "string" - case strings.Contains(t, "float") || strings.Contains(t, "double"): - typeName = "float64" - case strings.Contains(t, "bool"): - typeName = "bool" - case strings.Contains(t, "binary") || strings.Contains(t, "blob"): - typeName = "[]byte" - case strings.Contains(t, "date") || strings.Contains(t, "time"): - if in.StdTime { - typeName = "time.Time" - } else { - typeName = "*gtime.Time" - } - default: - typeName = "string" - } - } - - var ( - tagKey = "`" - result = []string{ - " #" + gstr.CaseCamel(field.Name), - " #" + typeName, - } - descriptionTag = gstr.Replace(formatComment(field.Comment), `"`, `\"`) - ) - - result = append(result, " #"+fmt.Sprintf(tagKey+`json:"%s"`, jsonTag)) - result = append(result, " #"+fmt.Sprintf(`description:"%s"`+tagKey, descriptionTag)) - result = append(result, " #"+fmt.Sprintf(`// %s`, formatComment(field.Comment))) - - for k, v := range result { - if in.NoJsonTag { - v, _ = gregex.ReplaceString(`json:".+"`, ``, v) - } - if !in.DescriptionTag { - v, _ = gregex.ReplaceString(`description:".*"`, ``, v) - } - if in.NoModelComment { - v, _ = gregex.ReplaceString(`//.+`, ``, v) - } - result[k] = v - } - return result -} - -// formatComment formats the comment string to fit the golang code without any lines. -func formatComment(comment string) string { - comment = gstr.ReplaceByArray(comment, g.SliceStr{ - "\n", " ", - "\r", " ", - }) - comment = gstr.Replace(comment, `\n`, " ") - comment = gstr.Trim(comment) - return comment -} - -// generateColumnDefinitionForDao generates and returns the column names definition for specified table. -func generateColumnDefinitionForDao(fieldMap map[string]*gdb.TableField) string { - var ( - buffer = bytes.NewBuffer(nil) - array = make([][]string, len(fieldMap)) - names = sortFieldKeyForDao(fieldMap) - ) - for index, name := range names { - var ( - field = fieldMap[name] - comment = gstr.Trim(gstr.ReplaceByArray(field.Comment, g.SliceStr{ - "\n", " ", - "\r", " ", - })) - ) - array[index] = []string{ - " #" + gstr.CaseCamel(field.Name), - " # " + "string", - " #" + fmt.Sprintf(`// %s`, comment), - } - } - tw := tablewriter.NewWriter(buffer) - tw.SetBorder(false) - tw.SetRowLine(false) - tw.SetAutoWrapText(false) - tw.SetColumnSeparator("") - tw.AppendBulk(array) - tw.Render() - defineContent := buffer.String() - // Let's do this hack of table writer for indent! - defineContent = gstr.Replace(defineContent, " #", "") - buffer.Reset() - buffer.WriteString(defineContent) - return buffer.String() -} - -// generateColumnNamesForDao generates and returns the column names assignment content of column struct -// for specified table. -func generateColumnNamesForDao(fieldMap map[string]*gdb.TableField) string { - var ( - buffer = bytes.NewBuffer(nil) - array = make([][]string, len(fieldMap)) - names = sortFieldKeyForDao(fieldMap) - ) - for index, name := range names { - field := fieldMap[name] - array[index] = []string{ - " #" + gstr.CaseCamel(field.Name) + ":", - fmt.Sprintf(` #"%s",`, field.Name), - } - } - tw := tablewriter.NewWriter(buffer) - tw.SetBorder(false) - tw.SetRowLine(false) - tw.SetAutoWrapText(false) - tw.SetColumnSeparator("") - tw.AppendBulk(array) - tw.Render() - namesContent := buffer.String() - // Let's do this hack of table writer for indent! - namesContent = gstr.Replace(namesContent, " #", "") - buffer.Reset() - buffer.WriteString(namesContent) - return buffer.String() -} - -func getTplDaoIndexContent(tplDaoIndexPath string) string { - if tplDaoIndexPath != "" { - return gfile.GetContents(tplDaoIndexPath) - } - return consts.TemplateDaoDaoIndexContent -} - -func getTplDaoInternalContent(tplDaoInternalPath string) string { - if tplDaoInternalPath != "" { - return gfile.GetContents(tplDaoInternalPath) - } - return consts.TemplateDaoDaoInternalContent -} - -// getJsonTagFromCase call gstr.Case* function to convert the s to specified case. -func getJsonTagFromCase(str, caseStr string) string { - switch gstr.ToLower(caseStr) { - case gstr.ToLower("Camel"): - return gstr.CaseCamel(str) - - case gstr.ToLower("CamelLower"): - return gstr.CaseCamelLower(str) - - case gstr.ToLower("Kebab"): - return gstr.CaseKebab(str) - - case gstr.ToLower("KebabScreaming"): - return gstr.CaseKebabScreaming(str) - - case gstr.ToLower("Snake"): - return gstr.CaseSnake(str) - - case gstr.ToLower("SnakeFirstUpper"): - return gstr.CaseSnakeFirstUpper(str) - - case gstr.ToLower("SnakeScreaming"): - return gstr.CaseSnakeScreaming(str) - } - return str -} - -func sortFieldKeyForDao(fieldMap map[string]*gdb.TableField) []string { - names := make(map[int]string) - for _, field := range fieldMap { - names[field.Index] = field.Name - } - var ( - i = 0 - j = 0 - result = make([]string, len(names)) - ) - for { - if len(names) == 0 { - break - } - if val, ok := names[i]; ok { - result[j] = val - j++ - delete(names, i) - } - i++ - } - return result -} diff --git a/cmd/gf/internal/cmd/cmd_gen_pbentity.go b/cmd/gf/internal/cmd/cmd_gen_pbentity.go index b07a1e522..a87d3f9ba 100644 --- a/cmd/gf/internal/cmd/cmd_gen_pbentity.go +++ b/cmd/gf/internal/cmd/cmd_gen_pbentity.go @@ -153,10 +153,10 @@ func doGenPbEntityForArray(ctx context.Context, index int, in cGenPbEntityInput) if index >= 0 { err = g.Cfg().MustGet( ctx, - fmt.Sprintf(`%s.%d`, cGenDaoConfig, index), + fmt.Sprintf(`%s.%d`, cGenPbEntityConfig, index), ).Scan(&in) if err != nil { - mlog.Fatalf(`invalid configuration of "%s": %+v`, cGenDaoConfig, err) + mlog.Fatalf(`invalid configuration of "%s": %+v`, cGenPbEntityConfig, err) } } if in.Package == "" { diff --git a/cmd/gf/internal/cmd/gendao/gendao.go b/cmd/gf/internal/cmd/gendao/gendao.go new file mode 100644 index 000000000..0aca83856 --- /dev/null +++ b/cmd/gf/internal/cmd/gendao/gendao.go @@ -0,0 +1,339 @@ +package gendao + +import ( + "context" + "fmt" + "strings" + + "github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/v2/container/garray" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gtag" +) + +const ( + CGenDaoConfig = `gfcli.gen.dao` + CGenDaoUsage = `gf gen dao [OPTION]` + CGenDaoBrief = `automatically generate go files for dao/do/entity` + CGenDaoEg = ` +gf gen dao +gf gen dao -l "mysql:root:12345678@tcp(127.0.0.1:3306)/test" +gf gen dao -p ./model -g user-center -t user,user_detail,user_login +gf gen dao -r user_ +` + + CGenDaoAd = ` +CONFIGURATION SUPPORT + Options are also supported by configuration file. + It's suggested using configuration file instead of command line arguments making producing. + The configuration node name is "gfcli.gen.dao", which also supports multiple databases, for example(config.yaml): + gfcli: + gen: + dao: + - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" + tables: "order,products" + jsonCase: "CamelLower" + - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/primary" + path: "./my-app" + prefix: "primary_" + tables: "user, userDetail" +` + CGenDaoBriefPath = `directory path for generated files` + CGenDaoBriefLink = `database configuration, the same as the ORM configuration of GoFrame` + CGenDaoBriefTables = `generate models only for given tables, multiple table names separated with ','` + CGenDaoBriefTablesEx = `generate models excluding given tables, multiple table names separated with ','` + CGenDaoBriefPrefix = `add prefix for all table of specified link/database tables` + CGenDaoBriefRemovePrefix = `remove specified prefix of the table, multiple prefix separated with ','` + CGenDaoBriefStdTime = `use time.Time from stdlib instead of gtime.Time for generated time/date fields of tables` + CGenDaoBriefWithTime = `add created time for auto produced go files` + CGenDaoBriefGJsonSupport = `use gJsonSupport to use *gjson.Json instead of string for generated json fields of tables` + CGenDaoBriefImportPrefix = `custom import prefix for generated go files` + CGenDaoBriefDaoPath = `directory path for storing generated dao files under path` + CGenDaoBriefDoPath = `directory path for storing generated do files under path` + CGenDaoBriefEntityPath = `directory path for storing generated entity files under path` + CGenDaoBriefOverwriteDao = `overwrite all dao files both inside/outside internal folder` + CGenDaoBriefModelFile = `custom file name for storing generated model content` + CGenDaoBriefModelFileForDao = `custom file name generating model for DAO operations like Where/Data. It's empty in default` + CGenDaoBriefDescriptionTag = `add comment to description tag for each field` + CGenDaoBriefNoJsonTag = `no json tag will be added for each field` + CGenDaoBriefNoModelComment = `no model comment will be added for each field` + CGenDaoBriefGroup = ` +specifying the configuration group name of database for generated ORM instance, +it's not necessary and the default value is "default" +` + CGenDaoBriefJsonCase = ` +generated json tag case for model struct, cases are as follows: +| Case | Example | +|---------------- |--------------------| +| Camel | AnyKindOfString | +| CamelLower | anyKindOfString | default +| Snake | any_kind_of_string | +| SnakeScreaming | ANY_KIND_OF_STRING | +| SnakeFirstUpper | rgb_code_md5 | +| Kebab | any-kind-of-string | +| KebabScreaming | ANY-KIND-OF-STRING | +` + + tplVarTableName = `{TplTableName}` + tplVarTableNameCamelCase = `{TplTableNameCamelCase}` + tplVarTableNameCamelLowerCase = `{TplTableNameCamelLowerCase}` + tplVarPackageImports = `{TplPackageImports}` + tplVarImportPrefix = `{TplImportPrefix}` + tplVarStructDefine = `{TplStructDefine}` + tplVarColumnDefine = `{TplColumnDefine}` + tplVarColumnNames = `{TplColumnNames}` + tplVarGroupName = `{TplGroupName}` + tplVarDatetimeStr = `{TplDatetimeStr}` +) + +var ( + createdAt = gtime.Now() +) + +func init() { + gtag.Sets(g.MapStrStr{ + `CGenDaoConfig`: CGenDaoConfig, + `CGenDaoUsage`: CGenDaoUsage, + `CGenDaoBrief`: CGenDaoBrief, + `CGenDaoEg`: CGenDaoEg, + `CGenDaoAd`: CGenDaoAd, + `CGenDaoBriefPath`: CGenDaoBriefPath, + `CGenDaoBriefLink`: CGenDaoBriefLink, + `CGenDaoBriefTables`: CGenDaoBriefTables, + `CGenDaoBriefTablesEx`: CGenDaoBriefTablesEx, + `CGenDaoBriefPrefix`: CGenDaoBriefPrefix, + `CGenDaoBriefRemovePrefix`: CGenDaoBriefRemovePrefix, + `CGenDaoBriefStdTime`: CGenDaoBriefStdTime, + `CGenDaoBriefWithTime`: CGenDaoBriefWithTime, + `CGenDaoBriefDaoPath`: CGenDaoBriefDaoPath, + `CGenDaoBriefDoPath`: CGenDaoBriefDoPath, + `CGenDaoBriefEntityPath`: CGenDaoBriefEntityPath, + `CGenDaoBriefGJsonSupport`: CGenDaoBriefGJsonSupport, + `CGenDaoBriefImportPrefix`: CGenDaoBriefImportPrefix, + `CGenDaoBriefOverwriteDao`: CGenDaoBriefOverwriteDao, + `CGenDaoBriefModelFile`: CGenDaoBriefModelFile, + `CGenDaoBriefModelFileForDao`: CGenDaoBriefModelFileForDao, + `CGenDaoBriefDescriptionTag`: CGenDaoBriefDescriptionTag, + `CGenDaoBriefNoJsonTag`: CGenDaoBriefNoJsonTag, + `CGenDaoBriefNoModelComment`: CGenDaoBriefNoModelComment, + `CGenDaoBriefGroup`: CGenDaoBriefGroup, + `CGenDaoBriefJsonCase`: CGenDaoBriefJsonCase, + }) +} + +type ( + CGenDao struct{} + CGenDaoInput struct { + g.Meta `name:"dao" config:"{CGenDaoConfig}" usage:"{CGenDaoUsage}" brief:"{CGenDaoBrief}" eg:"{CGenDaoEg}" ad:"{CGenDaoAd}"` + Path string `name:"path" short:"p" brief:"{CGenDaoBriefPath}" d:"internal"` + Link string `name:"link" short:"l" brief:"{CGenDaoBriefLink}"` + Tables string `name:"tables" short:"t" brief:"{CGenDaoBriefTables}"` + TablesEx string `name:"tablesEx" short:"x" brief:"{CGenDaoBriefTablesEx}"` + Group string `name:"group" short:"g" brief:"{CGenDaoBriefGroup}" d:"default"` + Prefix string `name:"prefix" short:"f" brief:"{CGenDaoBriefPrefix}"` + RemovePrefix string `name:"removePrefix" short:"r" brief:"{CGenDaoBriefRemovePrefix}"` + JsonCase string `name:"jsonCase" short:"j" brief:"{CGenDaoBriefJsonCase}" d:"CamelLower"` + ImportPrefix string `name:"importPrefix" short:"i" brief:"{CGenDaoBriefImportPrefix}"` + DaoPath string `name:"daoPath" short:"d" brief:"{CGenDaoBriefDaoPath}" d:"dao"` + DoPath string `name:"doPath" short:"o" brief:"{CGenDaoBriefDoPath}" d:"model/do"` + EntityPath string `name:"entityPath" short:"e" brief:"{CGenDaoBriefEntityPath}" d:"model/entity"` + StdTime bool `name:"stdTime" short:"s" brief:"{CGenDaoBriefStdTime}" orphan:"true"` + WithTime bool `name:"withTime" short:"w" brief:"{CGenDaoBriefWithTime}" orphan:"true"` + GJsonSupport bool `name:"gJsonSupport" short:"n" brief:"{CGenDaoBriefGJsonSupport}" orphan:"true"` + OverwriteDao bool `name:"overwriteDao" short:"v" brief:"{CGenDaoBriefOverwriteDao}" orphan:"true"` + DescriptionTag bool `name:"descriptionTag" short:"c" brief:"{CGenDaoBriefDescriptionTag}" orphan:"true"` + NoJsonTag bool `name:"noJsonTag" short:"k" brief:"{CGenDaoBriefNoJsonTag" orphan:"true"` + NoModelComment bool `name:"noModelComment" short:"m" brief:"{CGenDaoBriefNoModelComment}" orphan:"true"` + } + CGenDaoOutput struct{} + + CGenDaoInternalInput struct { + CGenDaoInput + TableName string // TableName specifies the table name of the table. + NewTableName string // NewTableName specifies the prefix-stripped name of the table. + ModName string // ModName specifies the module name of current golang project, which is used for import purpose. + } +) + +func (c CGenDao) Dao(ctx context.Context, in CGenDaoInput) (out *CGenDaoOutput, err error) { + if g.Cfg().Available(ctx) { + v := g.Cfg().MustGet(ctx, CGenDaoConfig) + if v.IsSlice() { + for i := 0; i < len(v.Interfaces()); i++ { + doGenDaoForArray(ctx, i, in) + } + } else { + doGenDaoForArray(ctx, -1, in) + } + } else { + doGenDaoForArray(ctx, -1, in) + } + mlog.Print("done!") + return +} + +// doGenDaoForArray implements the "gen dao" command for configuration array. +func doGenDaoForArray(ctx context.Context, index int, in CGenDaoInput) { + var ( + err error + db gdb.DB + modName string // Go module name, eg: github.com/gogf/gf. + ) + if index >= 0 { + err = g.Cfg().MustGet( + ctx, + fmt.Sprintf(`%s.%d`, CGenDaoConfig, index), + ).Scan(&in) + if err != nil { + mlog.Fatalf(`invalid configuration of "%s": %+v`, CGenDaoConfig, err) + } + } + if dirRealPath := gfile.RealPath(in.Path); dirRealPath == "" { + mlog.Fatalf(`path "%s" does not exist`, in.Path) + } + removePrefixArray := gstr.SplitAndTrim(in.RemovePrefix, ",") + if in.ImportPrefix == "" { + if !gfile.Exists("go.mod") { + mlog.Fatal("go.mod does not exist in current working directory") + } + var ( + goModContent = gfile.GetContents("go.mod") + match, _ = gregex.MatchString(`^module\s+(.+)\s*`, goModContent) + ) + if len(match) > 1 { + modName = gstr.Trim(match[1]) + } else { + mlog.Fatal("module name does not found in go.mod") + } + } + + // It uses user passed database configuration. + if in.Link != "" { + var tempGroup = gtime.TimestampNanoStr() + gdb.AddConfigNode(tempGroup, gdb.ConfigNode{ + Link: in.Link, + }) + if db, err = gdb.Instance(tempGroup); err != nil { + mlog.Fatalf(`database initialization failed: %+v`, err) + } + } else { + db = g.DB(in.Group) + } + if db == nil { + mlog.Fatal(`database initialization failed, may be invalid database configuration`) + } + + var tableNames []string + if in.Tables != "" { + tableNames = gstr.SplitAndTrim(in.Tables, ",") + } else { + tableNames, err = db.Tables(context.TODO()) + if err != nil { + mlog.Fatalf("fetching tables failed: %+v", err) + } + } + // Table excluding. + if in.TablesEx != "" { + array := garray.NewStrArrayFrom(tableNames) + for _, v := range gstr.SplitAndTrim(in.TablesEx, ",") { + array.RemoveValue(v) + } + tableNames = array.Slice() + } + + // Generating dao & model go files one by one according to given table name. + newTableNames := make([]string, len(tableNames)) + for i, tableName := range tableNames { + newTableName := tableName + for _, v := range removePrefixArray { + newTableName = gstr.TrimLeftStr(newTableName, v, 1) + } + newTableName = in.Prefix + newTableName + newTableNames[i] = newTableName + // Dao. + generateDao(ctx, db, CGenDaoInternalInput{ + CGenDaoInput: in, + TableName: tableName, + NewTableName: newTableName, + ModName: modName, + }) + } + // Do. + generateDo(ctx, db, tableNames, newTableNames, CGenDaoInternalInput{ + CGenDaoInput: in, + ModName: modName, + }) + // Entity. + generateEntity(ctx, db, tableNames, newTableNames, CGenDaoInternalInput{ + CGenDaoInput: in, + ModName: modName, + }) +} + +func getImportPartContent(source string, isDo bool) string { + var ( + packageImportsArray = garray.NewStrArray() + ) + + if isDo { + packageImportsArray.Append(`"github.com/gogf/gf/v2/frame/g"`) + } + + // Time package recognition. + if strings.Contains(source, "gtime.Time") { + packageImportsArray.Append(`"github.com/gogf/gf/v2/os/gtime"`) + } else if strings.Contains(source, "time.Time") { + packageImportsArray.Append(`"time"`) + } + + // Json type. + if strings.Contains(source, "gjson.Json") { + packageImportsArray.Append(`"github.com/gogf/gf/v2/encoding/gjson"`) + } + + // Generate and write content to golang file. + packageImportsStr := "" + if packageImportsArray.Len() > 0 { + packageImportsStr = fmt.Sprintf("import(\n%s\n)", packageImportsArray.Join("\n")) + } + return packageImportsStr +} + +func replaceDefaultVar(in CGenDaoInternalInput, origin string) string { + var tplDatetimeStr string + if in.WithTime { + tplDatetimeStr = fmt.Sprintf(`Created at %s`, createdAt.String()) + } + return gstr.ReplaceByMap(origin, g.MapStrStr{ + tplVarDatetimeStr: tplDatetimeStr, + }) +} + +func sortFieldKeyForDao(fieldMap map[string]*gdb.TableField) []string { + names := make(map[int]string) + for _, field := range fieldMap { + names[field.Index] = field.Name + } + var ( + i = 0 + j = 0 + result = make([]string, len(names)) + ) + for { + if len(names) == 0 { + break + } + if val, ok := names[i]; ok { + result[j] = val + j++ + delete(names, i) + } + i++ + } + return result +} diff --git a/cmd/gf/internal/cmd/gendao/gendao_dao.go b/cmd/gf/internal/cmd/gendao/gendao_dao.go new file mode 100644 index 000000000..c90ad252b --- /dev/null +++ b/cmd/gf/internal/cmd/gendao/gendao_dao.go @@ -0,0 +1,186 @@ +package gendao + +import ( + "bytes" + "context" + "fmt" + "strings" + + "github.com/gogf/gf/cmd/gf/v2/internal/consts" + "github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/cmd/gf/v2/internal/utility/utils" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" + "github.com/olekukonko/tablewriter" +) + +// generateDaoContentFile generates the dao and model content of given table. +func generateDao(ctx context.Context, db gdb.DB, in CGenDaoInternalInput) { + // Generating table data preparing. + fieldMap, err := db.TableFields(ctx, in.TableName) + if err != nil { + mlog.Fatalf(`fetching tables fields failed for table "%s": %+v`, in.TableName, err) + } + var ( + dirRealPath = gfile.RealPath(in.Path) + dirPathDao = gfile.Join(in.Path, in.DaoPath) + tableNameCamelCase = gstr.CaseCamel(in.NewTableName) + tableNameCamelLowerCase = gstr.CaseCamelLower(in.NewTableName) + tableNameSnakeCase = gstr.CaseSnake(in.NewTableName) + importPrefix = in.ImportPrefix + ) + if importPrefix == "" { + if dirRealPath == "" { + dirRealPath = in.Path + importPrefix = dirRealPath + importPrefix = gstr.Trim(dirRealPath, "./") + } else { + importPrefix = gstr.Replace(dirRealPath, gfile.Pwd(), "") + } + importPrefix = gstr.Replace(importPrefix, gfile.Separator, "/") + importPrefix = gstr.Join(g.SliceStr{in.ModName, importPrefix, in.DaoPath}, "/") + importPrefix, _ = gregex.ReplaceString(`\/{2,}`, `/`, gstr.Trim(importPrefix, "/")) + } else { + importPrefix = gstr.Join(g.SliceStr{importPrefix, in.DaoPath}, "/") + } + + fileName := gstr.Trim(tableNameSnakeCase, "-_.") + if len(fileName) > 5 && fileName[len(fileName)-5:] == "_test" { + // Add suffix to avoid the table name which contains "_test", + // which would make the go file a testing file. + fileName += "_table" + } + + // dao - index + generateDaoIndex(in, tableNameCamelCase, tableNameCamelLowerCase, importPrefix, dirPathDao, fileName) + + // dao - internal + generateDaoInternal(in, tableNameCamelCase, tableNameCamelLowerCase, importPrefix, dirPathDao, fileName, fieldMap) +} + +func generateDaoIndex(in CGenDaoInternalInput, tableNameCamelCase, tableNameCamelLowerCase, importPrefix, dirPathDao, fileName string) { + path := gfile.Join(dirPathDao, fileName+".go") + if in.OverwriteDao || !gfile.Exists(path) { + indexContent := gstr.ReplaceByMap(getTplDaoIndexContent(""), g.MapStrStr{ + tplVarImportPrefix: importPrefix, + tplVarTableName: in.TableName, + tplVarTableNameCamelCase: tableNameCamelCase, + tplVarTableNameCamelLowerCase: tableNameCamelLowerCase, + }) + indexContent = replaceDefaultVar(in, indexContent) + if err := gfile.PutContents(path, strings.TrimSpace(indexContent)); err != nil { + mlog.Fatalf("writing content to '%s' failed: %v", path, err) + } else { + utils.GoFmt(path) + mlog.Print("generated:", path) + } + } +} + +func generateDaoInternal( + in CGenDaoInternalInput, + tableNameCamelCase, tableNameCamelLowerCase, importPrefix string, + dirPathDao, fileName string, + fieldMap map[string]*gdb.TableField, +) { + path := gfile.Join(dirPathDao, "internal", fileName+".go") + modelContent := gstr.ReplaceByMap(getTplDaoInternalContent(""), g.MapStrStr{ + tplVarImportPrefix: importPrefix, + tplVarTableName: in.TableName, + tplVarGroupName: in.Group, + tplVarTableNameCamelCase: tableNameCamelCase, + tplVarTableNameCamelLowerCase: tableNameCamelLowerCase, + tplVarColumnDefine: gstr.Trim(generateColumnDefinitionForDao(fieldMap)), + tplVarColumnNames: gstr.Trim(generateColumnNamesForDao(fieldMap)), + }) + modelContent = replaceDefaultVar(in, modelContent) + if err := gfile.PutContents(path, strings.TrimSpace(modelContent)); err != nil { + mlog.Fatalf("writing content to '%s' failed: %v", path, err) + } else { + utils.GoFmt(path) + mlog.Print("generated:", path) + } +} + +func getTplDaoIndexContent(tplDaoIndexPath string) string { + if tplDaoIndexPath != "" { + return gfile.GetContents(tplDaoIndexPath) + } + return consts.TemplateDaoDaoIndexContent +} + +func getTplDaoInternalContent(tplDaoInternalPath string) string { + if tplDaoInternalPath != "" { + return gfile.GetContents(tplDaoInternalPath) + } + return consts.TemplateDaoDaoInternalContent +} + +// generateColumnNamesForDao generates and returns the column names assignment content of column struct +// for specified table. +func generateColumnNamesForDao(fieldMap map[string]*gdb.TableField) string { + var ( + buffer = bytes.NewBuffer(nil) + array = make([][]string, len(fieldMap)) + names = sortFieldKeyForDao(fieldMap) + ) + for index, name := range names { + field := fieldMap[name] + array[index] = []string{ + " #" + gstr.CaseCamel(field.Name) + ":", + fmt.Sprintf(` #"%s",`, field.Name), + } + } + tw := tablewriter.NewWriter(buffer) + tw.SetBorder(false) + tw.SetRowLine(false) + tw.SetAutoWrapText(false) + tw.SetColumnSeparator("") + tw.AppendBulk(array) + tw.Render() + namesContent := buffer.String() + // Let's do this hack of table writer for indent! + namesContent = gstr.Replace(namesContent, " #", "") + buffer.Reset() + buffer.WriteString(namesContent) + return buffer.String() +} + +// generateColumnDefinitionForDao generates and returns the column names definition for specified table. +func generateColumnDefinitionForDao(fieldMap map[string]*gdb.TableField) string { + var ( + buffer = bytes.NewBuffer(nil) + array = make([][]string, len(fieldMap)) + names = sortFieldKeyForDao(fieldMap) + ) + for index, name := range names { + var ( + field = fieldMap[name] + comment = gstr.Trim(gstr.ReplaceByArray(field.Comment, g.SliceStr{ + "\n", " ", + "\r", " ", + })) + ) + array[index] = []string{ + " #" + gstr.CaseCamel(field.Name), + " # " + "string", + " #" + fmt.Sprintf(`// %s`, comment), + } + } + tw := tablewriter.NewWriter(buffer) + tw.SetBorder(false) + tw.SetRowLine(false) + tw.SetAutoWrapText(false) + tw.SetColumnSeparator("") + tw.AppendBulk(array) + tw.Render() + defineContent := buffer.String() + // Let's do this hack of table writer for indent! + defineContent = gstr.Replace(defineContent, " #", "") + buffer.Reset() + buffer.WriteString(defineContent) + return buffer.String() +} diff --git a/cmd/gf/internal/cmd/gendao/gendao_do.go b/cmd/gf/internal/cmd/gendao/gendao_do.go new file mode 100644 index 000000000..c7e5152c4 --- /dev/null +++ b/cmd/gf/internal/cmd/gendao/gendao_do.go @@ -0,0 +1,79 @@ +package gendao + +import ( + "context" + "fmt" + "strings" + + "github.com/gogf/gf/cmd/gf/v2/internal/consts" + "github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/cmd/gf/v2/internal/utility/utils" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" +) + +func generateDo(ctx context.Context, db gdb.DB, tableNames, newTableNames []string, in CGenDaoInternalInput) { + var ( + doDirPath = gfile.Join(in.Path, in.DoPath) + ) + in.NoJsonTag = true + in.DescriptionTag = false + in.NoModelComment = false + // Model content. + for i, tableName := range tableNames { + in.TableName = tableName + fieldMap, err := db.TableFields(ctx, tableName) + if err != nil { + mlog.Fatalf("fetching tables fields failed for table '%s':\n%v", in.TableName, err) + } + var ( + newTableName = newTableNames[i] + doFilePath = gfile.Join(doDirPath, gstr.CaseSnake(newTableName)+".go") + structDefinition = generateStructDefinition(generateStructDefinitionInput{ + CGenDaoInternalInput: in, + StructName: gstr.CaseCamel(newTableName), + FieldMap: fieldMap, + IsDo: true, + }) + ) + // replace all types to interface{}. + structDefinition, _ = gregex.ReplaceStringFuncMatch( + "([A-Z]\\w*?)\\s+([\\w\\*\\.]+?)\\s+(//)", + structDefinition, + func(match []string) string { + // If the type is already a pointer/slice/map, it does nothing. + if !gstr.HasPrefix(match[2], "*") && !gstr.HasPrefix(match[2], "[]") && !gstr.HasPrefix(match[2], "map") { + return fmt.Sprintf(`%s interface{} %s`, match[1], match[3]) + } + return match[0] + }, + ) + modelContent := generateDoContent( + in, + tableName, + gstr.CaseCamel(newTableName), + structDefinition, + ) + err = gfile.PutContents(doFilePath, strings.TrimSpace(modelContent)) + if err != nil { + mlog.Fatalf(`writing content to "%s" failed: %v`, doFilePath, err) + } else { + utils.GoFmt(doFilePath) + mlog.Print("generated:", doFilePath) + } + } +} + +func generateDoContent(in CGenDaoInternalInput, tableName, tableNameCamelCase, structDefine string) string { + doContent := gstr.ReplaceByMap(consts.TemplateGenDaoDoContent, g.MapStrStr{ + tplVarTableName: tableName, + tplVarPackageImports: getImportPartContent(structDefine, true), + tplVarTableNameCamelCase: tableNameCamelCase, + tplVarStructDefine: structDefine, + }) + doContent = replaceDefaultVar(in, doContent) + return doContent +} diff --git a/cmd/gf/internal/cmd/gendao/gendao_entity.go b/cmd/gf/internal/cmd/gendao/gendao_entity.go new file mode 100644 index 000000000..77ce2e901 --- /dev/null +++ b/cmd/gf/internal/cmd/gendao/gendao_entity.go @@ -0,0 +1,58 @@ +package gendao + +import ( + "context" + "strings" + + "github.com/gogf/gf/cmd/gf/v2/internal/consts" + "github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/cmd/gf/v2/internal/utility/utils" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/text/gstr" +) + +func generateEntity(ctx context.Context, db gdb.DB, tableNames, newTableNames []string, in CGenDaoInternalInput) { + var entityDirPath = gfile.Join(in.Path, in.EntityPath) + // Model content. + for i, tableName := range tableNames { + fieldMap, err := db.TableFields(ctx, tableName) + if err != nil { + mlog.Fatalf("fetching tables fields failed for table '%s':\n%v", in.TableName, err) + } + var ( + newTableName = newTableNames[i] + entityFilePath = gfile.Join(entityDirPath, gstr.CaseSnake(newTableName)+".go") + entityContent = generateEntityContent( + in, + newTableName, + gstr.CaseCamel(newTableName), + generateStructDefinition(generateStructDefinitionInput{ + CGenDaoInternalInput: in, + StructName: gstr.CaseCamel(newTableName), + FieldMap: fieldMap, + IsDo: false, + }), + ) + ) + err = gfile.PutContents(entityFilePath, strings.TrimSpace(entityContent)) + if err != nil { + mlog.Fatalf("writing content to '%s' failed: %v", entityFilePath, err) + } else { + utils.GoFmt(entityFilePath) + mlog.Print("generated:", entityFilePath) + } + } +} + +func generateEntityContent(in CGenDaoInternalInput, tableName, tableNameCamelCase, structDefine string) string { + entityContent := gstr.ReplaceByMap(consts.TemplateGenDaoEntityContent, g.MapStrStr{ + tplVarTableName: tableName, + tplVarPackageImports: getImportPartContent(structDefine, false), + tplVarTableNameCamelCase: tableNameCamelCase, + tplVarStructDefine: structDefine, + }) + entityContent = replaceDefaultVar(in, entityContent) + return entityContent +} diff --git a/cmd/gf/internal/cmd/gendao/gendao_structure.go b/cmd/gf/internal/cmd/gendao/gendao_structure.go new file mode 100644 index 000000000..99e73cf5a --- /dev/null +++ b/cmd/gf/internal/cmd/gendao/gendao_structure.go @@ -0,0 +1,205 @@ +package gendao + +import ( + "bytes" + "fmt" + "strings" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" + "github.com/olekukonko/tablewriter" +) + +type generateStructDefinitionInput struct { + CGenDaoInternalInput + StructName string // Struct name. + FieldMap map[string]*gdb.TableField // Table field map. + IsDo bool // Is generating DTO struct. +} + +func generateStructDefinition(in generateStructDefinitionInput) string { + buffer := bytes.NewBuffer(nil) + array := make([][]string, len(in.FieldMap)) + names := sortFieldKeyForDao(in.FieldMap) + for index, name := range names { + field := in.FieldMap[name] + array[index] = generateStructFieldDefinition(field, in) + } + tw := tablewriter.NewWriter(buffer) + tw.SetBorder(false) + tw.SetRowLine(false) + tw.SetAutoWrapText(false) + tw.SetColumnSeparator("") + tw.AppendBulk(array) + tw.Render() + stContent := buffer.String() + // Let's do this hack of table writer for indent! + stContent = gstr.Replace(stContent, " #", "") + stContent = gstr.Replace(stContent, "` ", "`") + stContent = gstr.Replace(stContent, "``", "") + buffer.Reset() + buffer.WriteString(fmt.Sprintf("type %s struct {\n", in.StructName)) + if in.IsDo { + buffer.WriteString(fmt.Sprintf("g.Meta `orm:\"table:%s, do:true\"`\n", in.TableName)) + } + buffer.WriteString(stContent) + buffer.WriteString("}") + return buffer.String() +} + +// generateStructFieldForModel generates and returns the attribute definition for specified field. +func generateStructFieldDefinition(field *gdb.TableField, in generateStructDefinitionInput) []string { + var ( + typeName string + jsonTag = getJsonTagFromCase(field.Name, in.JsonCase) + ) + t, _ := gregex.ReplaceString(`\(.+\)`, "", field.Type) + t = gstr.Split(gstr.Trim(t), " ")[0] + t = gstr.ToLower(t) + + switch t { + case "binary", "varbinary", "blob", "tinyblob", "mediumblob", "longblob": + typeName = "[]byte" + + case "bit", "int", "int2", "tinyint", "small_int", "smallint", "medium_int", "mediumint", "serial": + if gstr.ContainsI(field.Type, "unsigned") { + typeName = "uint" + } else { + typeName = "int" + } + + case "int4", "int8", "big_int", "bigint", "bigserial": + if gstr.ContainsI(field.Type, "unsigned") { + typeName = "uint64" + } else { + typeName = "int64" + } + + // pgsql int32 slice. + case "_int2": + if gstr.ContainsI(field.Type, "unsigned") { + typeName = "[]uint" + } else { + typeName = "[]int" + } + + // pgsql int64 slice. + case "_int4", "_int8": + if gstr.ContainsI(field.Type, "unsigned") { + typeName = "[]uint64" + } else { + typeName = "[]int64" + } + + case "real": + typeName = "float32" + + case "float", "double", "decimal", "smallmoney", "numeric": + typeName = "float64" + + case "bool": + typeName = "bool" + + case "datetime", "timestamp", "date", "time": + if in.StdTime { + typeName = "time.Time" + } else { + typeName = "*gtime.Time" + } + case "json", "jsonb": + if in.GJsonSupport { + typeName = "*gjson.Json" + } else { + typeName = "string" + } + default: + // Automatically detect its data type. + switch { + case strings.Contains(t, "int"): + typeName = "int" + case strings.Contains(t, "text") || strings.Contains(t, "char"): + typeName = "string" + case strings.Contains(t, "float") || strings.Contains(t, "double"): + typeName = "float64" + case strings.Contains(t, "bool"): + typeName = "bool" + case strings.Contains(t, "binary") || strings.Contains(t, "blob"): + typeName = "[]byte" + case strings.Contains(t, "date") || strings.Contains(t, "time"): + if in.StdTime { + typeName = "time.Time" + } else { + typeName = "*gtime.Time" + } + default: + typeName = "string" + } + } + + var ( + tagKey = "`" + result = []string{ + " #" + gstr.CaseCamel(field.Name), + " #" + typeName, + } + descriptionTag = gstr.Replace(formatComment(field.Comment), `"`, `\"`) + ) + + result = append(result, " #"+fmt.Sprintf(tagKey+`json:"%s"`, jsonTag)) + result = append(result, " #"+fmt.Sprintf(`description:"%s"`+tagKey, descriptionTag)) + result = append(result, " #"+fmt.Sprintf(`// %s`, formatComment(field.Comment))) + + for k, v := range result { + if in.NoJsonTag { + v, _ = gregex.ReplaceString(`json:".+"`, ``, v) + } + if !in.DescriptionTag { + v, _ = gregex.ReplaceString(`description:".*"`, ``, v) + } + if in.NoModelComment { + v, _ = gregex.ReplaceString(`//.+`, ``, v) + } + result[k] = v + } + return result +} + +// formatComment formats the comment string to fit the golang code without any lines. +func formatComment(comment string) string { + comment = gstr.ReplaceByArray(comment, g.SliceStr{ + "\n", " ", + "\r", " ", + }) + comment = gstr.Replace(comment, `\n`, " ") + comment = gstr.Trim(comment) + return comment +} + +// getJsonTagFromCase call gstr.Case* function to convert the s to specified case. +func getJsonTagFromCase(str, caseStr string) string { + switch gstr.ToLower(caseStr) { + case gstr.ToLower("Camel"): + return gstr.CaseCamel(str) + + case gstr.ToLower("CamelLower"): + return gstr.CaseCamelLower(str) + + case gstr.ToLower("Kebab"): + return gstr.CaseKebab(str) + + case gstr.ToLower("KebabScreaming"): + return gstr.CaseKebabScreaming(str) + + case gstr.ToLower("Snake"): + return gstr.CaseSnake(str) + + case gstr.ToLower("SnakeFirstUpper"): + return gstr.CaseSnakeFirstUpper(str) + + case gstr.ToLower("SnakeScreaming"): + return gstr.CaseSnakeScreaming(str) + } + return str +} diff --git a/contrib/drivers/mysql/mysql_feature_ctx_test.go b/contrib/drivers/mysql/mysql_feature_ctx_test.go index ce1a19d42..f03ca4ba5 100644 --- a/contrib/drivers/mysql/mysql_feature_ctx_test.go +++ b/contrib/drivers/mysql/mysql_feature_ctx_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/test/gtest" ) @@ -30,7 +31,7 @@ func Test_Ctx(t *testing.T) { } func Test_Ctx_Query(t *testing.T) { - db.GetLogger().SetCtxKeys("SpanId", "TraceId") + db.GetLogger().(*glog.Logger).SetCtxKeys("SpanId", "TraceId") gtest.C(t, func(t *gtest.T) { db.SetDebug(true) defer db.SetDebug(false) @@ -48,7 +49,7 @@ func Test_Ctx_Query(t *testing.T) { func Test_Ctx_Model(t *testing.T) { table := createInitTable() defer dropTable(table) - db.GetLogger().SetCtxKeys("SpanId", "TraceId") + db.GetLogger().(*glog.Logger).SetCtxKeys("SpanId", "TraceId") gtest.C(t, func(t *gtest.T) { db.SetDebug(true) defer db.SetDebug(false) diff --git a/contrib/drivers/mysql/mysql_model_test.go b/contrib/drivers/mysql/mysql_model_test.go index 82e7d51d8..15468dfaf 100644 --- a/contrib/drivers/mysql/mysql_model_test.go +++ b/contrib/drivers/mysql/mysql_model_test.go @@ -22,6 +22,7 @@ import ( "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/test/gtest" "github.com/gogf/gf/v2/text/gstr" @@ -978,8 +979,8 @@ func Test_Model_StructsWithOrmTag(t *testing.T) { users []User buffer = bytes.NewBuffer(nil) ) - dbInvalid.GetLogger().SetWriter(buffer) - defer dbInvalid.GetLogger().SetWriter(os.Stdout) + dbInvalid.GetLogger().(*glog.Logger).SetWriter(buffer) + defer dbInvalid.GetLogger().(*glog.Logger).SetWriter(os.Stdout) dbInvalid.Model(table).Order("id asc").Scan(&users) //fmt.Println(buffer.String()) t.Assert( diff --git a/contrib/drivers/pgsql/pgsql.go b/contrib/drivers/pgsql/pgsql.go index c1c3a3f5c..df3ab4383 100644 --- a/contrib/drivers/pgsql/pgsql.go +++ b/contrib/drivers/pgsql/pgsql.go @@ -16,7 +16,9 @@ import ( "context" "database/sql" "fmt" + "strings" + "github.com/gogf/gf/v2/util/gconv" _ "github.com/lib/pq" "github.com/gogf/gf/v2/container/gmap" @@ -108,6 +110,55 @@ func (d *Driver) GetChars() (charLeft string, charRight string) { return `"`, `"` } +// ConvertValueForLocal converts value to local Golang type of value according field type name from database. +// The parameter `fieldType` is in lower case, like: +// `float(5,2)`, `unsigned double(5,2)`, `decimal(10,2)`, `char(45)`, `varchar(100)`, etc. +func (d *Driver) ConvertValueForLocal(ctx context.Context, fieldType string, fieldValue interface{}) (interface{}, error) { + typeName, _ := gregex.ReplaceString(`\(.+\)`, "", fieldType) + typeName = strings.ToLower(typeName) + switch typeName { + // For pgsql, int8 = bigint. + case "int8": + if gstr.ContainsI(fieldType, "unsigned") { + return gconv.Uint64(gconv.String(fieldValue)), nil + } + return gconv.Int64(gconv.String(fieldValue)), nil + + // Int32 slice. + case + "_int2": + if gstr.ContainsI(fieldType, "unsigned") { + gconv.Uints(gconv.String(fieldValue)) + } + return gconv.Ints( + gstr.ReplaceByMap(gconv.String(fieldValue), + map[string]string{ + "{": "[", + "}": "]", + }, + ), + ), nil + + // Int64 slice. + case + "_int4", "_int8": + if gstr.ContainsI(fieldType, "unsigned") { + gconv.Uint64(gconv.String(fieldValue)) + } + return gconv.Int64s( + gstr.ReplaceByMap(gconv.String(fieldValue), + map[string]string{ + "{": "[", + "}": "]", + }, + ), + ), nil + + default: + return d.Core.ConvertValueForLocal(ctx, fieldType, fieldValue) + } +} + // DoFilter deals with the sql string before commits it to underlying sql driver. func (d *Driver) DoFilter(ctx context.Context, link gdb.Link, sql string, args []interface{}) (newSql string, newArgs []interface{}, err error) { defer func() { diff --git a/contrib/drivers/sqlite/sqlite_model_test.go b/contrib/drivers/sqlite/sqlite_model_test.go index 974ce649a..c4ee4cd07 100644 --- a/contrib/drivers/sqlite/sqlite_model_test.go +++ b/contrib/drivers/sqlite/sqlite_model_test.go @@ -21,6 +21,7 @@ import ( "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/test/gtest" "github.com/gogf/gf/v2/text/gstr" @@ -959,8 +960,8 @@ func Test_Model_StructsWithOrmTag(t *testing.T) { users []User buffer = bytes.NewBuffer(nil) ) - db.GetLogger().SetWriter(buffer) - defer db.GetLogger().SetWriter(os.Stdout) + db.GetLogger().(*glog.Logger).SetWriter(buffer) + defer db.GetLogger().(*glog.Logger).SetWriter(os.Stdout) db.Model(table).Order("id asc").Scan(&users) // fmt.Println(buffer.String()) t.Assert( diff --git a/database/gdb/gdb.go b/database/gdb/gdb.go index 7d523cc0e..1a29bf0d2 100644 --- a/database/gdb/gdb.go +++ b/database/gdb/gdb.go @@ -154,8 +154,8 @@ type DB interface { GetGroup() string // See Core.GetGroup. SetDryRun(enabled bool) // See Core.SetDryRun. GetDryRun() bool // See Core.GetDryRun. - SetLogger(logger *glog.Logger) // See Core.SetLogger. - GetLogger() *glog.Logger // See Core.GetLogger. + SetLogger(logger glog.ILogger) // See Core.SetLogger. + GetLogger() glog.ILogger // See Core.GetLogger. GetConfig() *ConfigNode // See Core.GetConfig. SetMaxIdleConnCount(n int) // See Core.SetMaxIdleConnCount. SetMaxOpenConnCount(n int) // See Core.SetMaxOpenConnCount. @@ -165,13 +165,14 @@ type DB interface { // Utility methods. // =========================================================================== - GetCtx() context.Context // See Core.GetCtx. - GetCore() *Core // See Core.GetCore - GetChars() (charLeft string, charRight string) // See Core.GetChars. - Tables(ctx context.Context, schema ...string) (tables []string, err error) // See Core.Tables. - TableFields(ctx context.Context, table string, schema ...string) (map[string]*TableField, error) // See Core.TableFields. - ConvertDataForRecord(ctx context.Context, data interface{}) (map[string]interface{}, error) // See Core.ConvertDataForRecord - FilteredLink() string // FilteredLink is used for filtering sensitive information in `Link` configuration before output it to tracing server. + GetCtx() context.Context // See Core.GetCtx. + GetCore() *Core // See Core.GetCore + GetChars() (charLeft string, charRight string) // See Core.GetChars. + Tables(ctx context.Context, schema ...string) (tables []string, err error) // See Core.Tables. + TableFields(ctx context.Context, table string, schema ...string) (map[string]*TableField, error) // See Core.TableFields. + ConvertDataForRecord(ctx context.Context, data interface{}) (map[string]interface{}, error) // See Core.ConvertDataForRecord + ConvertValueForLocal(ctx context.Context, fieldType string, fieldValue interface{}) (interface{}, error) // See Core.ConvertValueForLocal + FilteredLink() string // FilteredLink is used for filtering sensitive information in `Link` configuration before output it to tracing server. } // Core is the base struct for database management. @@ -183,7 +184,7 @@ type Core struct { debug *gtype.Bool // Enable debug mode for the database, which can be changed in runtime. cache *gcache.Cache // Cache manager, SQL result cache only. links *gmap.StrAnyMap // links caches all created links by node. - logger *glog.Logger // Logger for logging functionality. + logger glog.ILogger // Logger for logging functionality. config *ConfigNode // Current config node. } diff --git a/database/gdb/gdb_core_config.go b/database/gdb/gdb_core_config.go index bbc1021d1..233a41c81 100644 --- a/database/gdb/gdb_core_config.go +++ b/database/gdb/gdb_core_config.go @@ -156,12 +156,12 @@ func IsConfigured() bool { } // SetLogger sets the logger for orm. -func (c *Core) SetLogger(logger *glog.Logger) { +func (c *Core) SetLogger(logger glog.ILogger) { c.logger = logger } // GetLogger returns the (logger) of the orm. -func (c *Core) GetLogger() *glog.Logger { +func (c *Core) GetLogger() glog.ILogger { return c.logger } diff --git a/database/gdb/gdb_core_structure.go b/database/gdb/gdb_core_structure.go index 762b9f65e..ee82c0538 100644 --- a/database/gdb/gdb_core_structure.go +++ b/database/gdb/gdb_core_structure.go @@ -121,13 +121,14 @@ func (c *Core) ConvertDataForRecordValue(ctx context.Context, value interface{}) return convertedValue, nil } -// convertFieldValueToLocalValue automatically checks and converts field value from database type -// to golang variable type as underlying value of Value. -func (c *Core) convertFieldValueToLocalValue(fieldValue interface{}, fieldType string) interface{} { +// ConvertValueForLocal converts value to local Golang type of value according field type name from database. +// The parameter `fieldType` is in lower case, like: +// `float(5,2)`, `unsigned double(5,2)`, `decimal(10,2)`, `char(45)`, `varchar(100)`, etc. +func (c *Core) ConvertValueForLocal(ctx context.Context, fieldType string, fieldValue interface{}) (interface{}, error) { // If there's no type retrieved, it returns the `fieldValue` directly // to use its original data type, as `fieldValue` is type of interface{}. if fieldType == "" { - return fieldValue + return fieldValue, nil } typeName, _ := gregex.ReplaceString(`\(.+\)`, "", fieldType) typeName = strings.ToLower(typeName) @@ -139,7 +140,7 @@ func (c *Core) convertFieldValueToLocalValue(fieldValue interface{}, fieldType s "tinyblob", "mediumblob", "longblob": - return gconv.Bytes(fieldValue) + return gconv.Bytes(fieldValue), nil case "int", @@ -150,39 +151,21 @@ func (c *Core) convertFieldValueToLocalValue(fieldValue interface{}, fieldType s "mediumint", "serial": if gstr.ContainsI(fieldType, "unsigned") { - gconv.Uint(gconv.String(fieldValue)) + return gconv.Uint(gconv.String(fieldValue)), nil } - return gconv.Int(gconv.String(fieldValue)) + return gconv.Int(gconv.String(fieldValue)), nil + case - "_int2": - if gstr.ContainsI(fieldType, "unsigned") { - gconv.Uints(gconv.String(fieldValue)) - } - return gconv.Ints(gstr.ReplaceByMap(gconv.String(fieldValue), map[string]string{ - "{": "[", - "}": "]", - })) - case - "_int4", "_int8": - if gstr.ContainsI(fieldType, "unsigned") { - gconv.Uint64(gconv.String(fieldValue)) - } - return gconv.Int64s(gstr.ReplaceByMap(gconv.String(fieldValue), map[string]string{ - "{": "[", - "}": "]", - })) - case - "int8", // For pgsql, int8 = bigint. "big_int", "bigint", "bigserial": if gstr.ContainsI(fieldType, "unsigned") { - gconv.Uint64(gconv.String(fieldValue)) + return gconv.Uint64(gconv.String(fieldValue)), nil } - return gconv.Int64(gconv.String(fieldValue)) + return gconv.Int64(gconv.String(fieldValue)), nil case "real": - return gconv.Float32(gconv.String(fieldValue)) + return gconv.Float32(gconv.String(fieldValue)), nil case "float", @@ -191,76 +174,76 @@ func (c *Core) convertFieldValueToLocalValue(fieldValue interface{}, fieldType s "money", "numeric", "smallmoney": - return gconv.Float64(gconv.String(fieldValue)) + return gconv.Float64(gconv.String(fieldValue)), nil case "bit": s := gconv.String(fieldValue) // mssql is true|false string. if strings.EqualFold(s, "true") { - return 1 + return 1, nil } if strings.EqualFold(s, "false") { - return 0 + return 0, nil } - return gbinary.BeDecodeToInt64(gconv.Bytes(fieldValue)) + return gbinary.BeDecodeToInt64(gconv.Bytes(fieldValue)), nil case "bool": - return gconv.Bool(fieldValue) + return gconv.Bool(fieldValue), nil case "date": // Date without time. if t, ok := fieldValue.(time.Time); ok { - return gtime.NewFromTime(t).Format("Y-m-d") + return gtime.NewFromTime(t).Format("Y-m-d"), nil } t, _ := gtime.StrToTime(gconv.String(fieldValue)) - return t.Format("Y-m-d") + return t.Format("Y-m-d"), nil case "datetime", "timestamp", "timestamptz": if t, ok := fieldValue.(time.Time); ok { - return gtime.NewFromTime(t) + return gtime.NewFromTime(t), nil } t, _ := gtime.StrToTime(gconv.String(fieldValue)) - return t + return t, nil default: // Auto-detect field type, using key match. switch { case strings.Contains(typeName, "text") || strings.Contains(typeName, "char") || strings.Contains(typeName, "character"): - return gconv.String(fieldValue) + return gconv.String(fieldValue), nil case strings.Contains(typeName, "float") || strings.Contains(typeName, "double") || strings.Contains(typeName, "numeric"): - return gconv.Float64(gconv.String(fieldValue)) + return gconv.Float64(gconv.String(fieldValue)), nil case strings.Contains(typeName, "bool"): - return gconv.Bool(gconv.String(fieldValue)) + return gconv.Bool(gconv.String(fieldValue)), nil case strings.Contains(typeName, "binary") || strings.Contains(typeName, "blob"): - return fieldValue + return fieldValue, nil case strings.Contains(typeName, "int"): - return gconv.Int(gconv.String(fieldValue)) + return gconv.Int(gconv.String(fieldValue)), nil case strings.Contains(typeName, "time"): s := gconv.String(fieldValue) t, err := gtime.StrToTime(s) if err != nil { - return s + return s, nil } - return t + return t, nil case strings.Contains(typeName, "date"): s := gconv.String(fieldValue) t, err := gtime.StrToTime(s) if err != nil { - return s + return s, nil } - return t + return t, nil default: - return gconv.String(fieldValue) + return gconv.String(fieldValue), nil } } } diff --git a/database/gdb/gdb_core_underlying.go b/database/gdb/gdb_core_underlying.go index 5f0e669e4..37737fa1e 100644 --- a/database/gdb/gdb_core_underlying.go +++ b/database/gdb/gdb_core_underlying.go @@ -376,7 +376,11 @@ func (c *Core) RowsToResult(ctx context.Context, rows *sql.Rows) (Result, error) if value == nil { record[columnNames[i]] = gvar.New(nil) } else { - record[columnNames[i]] = gvar.New(c.convertFieldValueToLocalValue(value, columnTypes[i])) + var convertedValue interface{} + if convertedValue, err = c.db.ConvertValueForLocal(ctx, columnTypes[i], value); err != nil { + return nil, err + } + record[columnNames[i]] = gvar.New(convertedValue) } } result = append(result, record) diff --git a/frame/gins/gins_database.go b/frame/gins/gins_database.go index 66d1bb742..2e91fd9b6 100644 --- a/frame/gins/gins_database.go +++ b/frame/gins/gins_database.go @@ -16,6 +16,7 @@ import ( "github.com/gogf/gf/v2/internal/consts" "github.com/gogf/gf/v2/internal/intlog" "github.com/gogf/gf/v2/os/gcfg" + "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/util/gutil" ) @@ -138,8 +139,10 @@ func Database(name ...string) gdb.DB { } } if len(loggerConfigMap) > 0 { - if err = db.GetLogger().SetConfigWithMap(loggerConfigMap); err != nil { - panic(err) + if logger, ok := db.GetLogger().(*glog.Logger); ok { + if err = logger.SetConfigWithMap(loggerConfigMap); err != nil { + panic(err) + } } } return db diff --git a/os/gcron/gcron_z_unit_entry_test.go b/os/gcron/gcron_z_unit_entry_test.go index 159197534..62fa361ac 100644 --- a/os/gcron/gcron_z_unit_entry_test.go +++ b/os/gcron/gcron_z_unit_entry_test.go @@ -12,7 +12,6 @@ import ( "time" "github.com/gogf/gf/v2/container/garray" - "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcron" "github.com/gogf/gf/v2/test/gtest" ) @@ -24,7 +23,6 @@ func TestCron_Entry_Operations(t *testing.T) { array = garray.New(true) ) cron.DelayAddTimes(ctx, 500*time.Millisecond, "* * * * * *", 2, func(ctx context.Context) { - g.Log().Print(ctx, "add times") array.Append(1) }) t.Assert(cron.Size(), 0) @@ -42,7 +40,6 @@ func TestCron_Entry_Operations(t *testing.T) { array = garray.New(true) ) entry, err1 := cron.Add(ctx, "* * * * * *", func(ctx context.Context) { - g.Log().Print(ctx, "add") array.Append(1) }) t.Assert(err1, nil) @@ -56,7 +53,6 @@ func TestCron_Entry_Operations(t *testing.T) { t.Assert(array.Len(), 1) t.Assert(cron.Size(), 1) entry.Start() - g.Log().Print(ctx, "start") time.Sleep(1000 * time.Millisecond) t.Assert(array.Len(), 2) t.Assert(cron.Size(), 1) diff --git a/os/gcron/gcron_z_unit_test.go b/os/gcron/gcron_z_unit_test.go index 54e90f44d..d18a19ac0 100644 --- a/os/gcron/gcron_z_unit_test.go +++ b/os/gcron/gcron_z_unit_test.go @@ -112,7 +112,8 @@ func TestCron_Add_FixedPattern(t *testing.T) { array.Append(1) }) t.AssertNil(err) - time.Sleep(2800 * time.Millisecond) + time.Sleep(3000 * time.Millisecond) + g.Log().Debug(ctx, `current time`) t.Assert(array.Len(), 1) }) }