├── gen_options_test.go ├── internal ├── examples │ ├── tests │ │ ├── example13 │ │ │ ├── internal │ │ │ │ └── models │ │ │ │ │ ├── examples.go │ │ │ │ │ ├── example3.go │ │ │ │ │ ├── example2.go │ │ │ │ │ ├── example1.go │ │ │ │ │ ├── gormcnm_test.go │ │ │ │ │ └── gormcnm.go │ │ │ └── example13_test.go │ │ ├── example11 │ │ │ ├── internal │ │ │ │ └── models │ │ │ │ │ ├── example1.go │ │ │ │ │ ├── gormcnm_test.go │ │ │ │ │ └── gormcnm.go │ │ │ └── example11_test.go │ │ └── example12 │ │ │ ├── internal │ │ │ └── models │ │ │ │ ├── example2.go │ │ │ │ ├── gormcnm_test.go │ │ │ │ └── gormcnm.go │ │ │ └── example12_test.go │ ├── example1 │ │ ├── internal │ │ │ └── models │ │ │ │ ├── user.go │ │ │ │ ├── gormcnm_test.go │ │ │ │ └── gormcnm.go │ │ └── example1_test.go │ ├── example3 │ │ ├── internal │ │ │ └── models │ │ │ │ ├── user.go │ │ │ │ ├── gormcnm_test.go │ │ │ │ └── gormcnm.go │ │ └── example3_test.go │ └── example2 │ │ ├── internal │ │ └── models │ │ │ ├── user.go │ │ │ ├── gormcnm_test.go │ │ │ └── gormcnm.go │ │ └── example2_test.go ├── simpleindexname │ ├── simple_index_name_test.go │ └── simple_index_name.go ├── unicodehex │ ├── unicodehex_test.go │ └── unicodehex.go ├── simplename │ ├── simple_column_name_test.go │ └── simple_column_name.go ├── tests │ ├── tests_test.go │ └── tests.go ├── utils │ ├── utils_test.go │ └── utils.go └── docs │ ├── README_OLD_DOC.zh.md │ └── README_OLD_DOC.en.md ├── gormmomzhcn └── README.md ├── gormidxname ├── pattern_test.go ├── pattern_lowercase63.go ├── pattern_uppercase63.go └── pattern.go ├── Makefile ├── .gitignore ├── gormmomname ├── pattern_lowercase30.go ├── pattern_lowercase63.go ├── pattern_uppercase30.go ├── pattern_uppercase63.go ├── pattern_test.go └── pattern.go ├── LICENSE ├── gorm_struct_test.go ├── go.mod ├── validate_gorm_tags_test.go ├── gen_test.go ├── .github └── workflows │ └── release.yml ├── gen_index_test.go ├── gen_batch.go ├── gorm_struct.go ├── validate_gorm_tags.go ├── gen_options.go ├── go.sum ├── gen_index.go ├── README.zh.md ├── README.md └── gen.go /gen_options_test.go: -------------------------------------------------------------------------------- 1 | package gormmom 2 | -------------------------------------------------------------------------------- /internal/examples/tests/example13/internal/models/examples.go: -------------------------------------------------------------------------------- 1 | package models 2 | -------------------------------------------------------------------------------- /gormmomzhcn/README.md: -------------------------------------------------------------------------------- 1 | Code -> New-Github-Project [gormzhcn](https://github.com/go-zwbc/gormzhcn) 2 | -------------------------------------------------------------------------------- /internal/simpleindexname/simple_index_name_test.go: -------------------------------------------------------------------------------- 1 | package simpleindexname 2 | 3 | import "testing" 4 | 5 | func TestName_mergeIndexName(t *testing.T) { 6 | t.Log(mergeIndexName("idx", "student", "age")) 7 | } 8 | -------------------------------------------------------------------------------- /internal/unicodehex/unicodehex_test.go: -------------------------------------------------------------------------------- 1 | package unicodehex 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestS2LeHex4sUppers(t *testing.T) { 8 | t.Log(StringToHex4Uppercase("我叫杨亦乐")) 9 | t.Log(StringToHex4Uppercase("刘亦菲的亦")) 10 | t.Log(StringToHex4Uppercase("古天乐的乐")) 11 | } 12 | -------------------------------------------------------------------------------- /gormidxname/pattern_test.go: -------------------------------------------------------------------------------- 1 | package gormidxname 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/yyle88/gormmom/internal/simpleindexname" 7 | "github.com/yyle88/must" 8 | ) 9 | 10 | func TestPattern(t *testing.T) { 11 | must.Same(simpleindexname.IdxPatternTagName, IdxPatternTagName) 12 | must.Same(simpleindexname.UdxPatternTagName, UdxPatternTagName) 13 | } 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | COVERAGE_DIR ?= .coverage.out 2 | 3 | # cp from: https://github.com/yyle88/gormrepo/blob/c31435669714611c9ebde6975060f48cd5634451/Makefile#L4 4 | test: 5 | @if [ -d $(COVERAGE_DIR) ]; then rm -r $(COVERAGE_DIR); fi 6 | @mkdir $(COVERAGE_DIR) 7 | make test-with-flags TEST_FLAGS='-v -race -covermode atomic -coverprofile $$(COVERAGE_DIR)/combined.txt -bench=. -benchmem -timeout 20m' 8 | 9 | test-with-flags: 10 | @go test $(TEST_FLAGS) ./... 11 | -------------------------------------------------------------------------------- /internal/simplename/simple_column_name_test.go: -------------------------------------------------------------------------------- 1 | package simplename 2 | 3 | import "testing" 4 | 5 | func TestBuildColumnName(t *testing.T) { 6 | t.Log(BuildColumnName("abc")) 7 | t.Log(BuildColumnName("ABC")) 8 | t.Log(BuildColumnName("AaBbCc")) 9 | t.Log(BuildColumnName("FieldNameExample世界")) 10 | t.Log(BuildColumnName("世界ABC")) 11 | t.Log(BuildColumnName("姓名")) 12 | t.Log(BuildColumnName("V性别")) 13 | t.Log(BuildColumnName("(特殊)(情况)")) 14 | t.Log(BuildColumnName("abc*xyz")) 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | -------------------------------------------------------------------------------- /internal/tests/tests_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yyle88/gormmom/internal/tests" 8 | "github.com/yyle88/rese" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | func TestNewDBRun(t *testing.T) { 13 | tests.NewDBRun(t, func(db *gorm.DB) { 14 | var result int 15 | require.NoError(t, db.Raw("SELECT 1").Scan(&result).Error) 16 | require.Equal(t, 1, result) 17 | }) 18 | } 19 | 20 | func TestNewMemDB(t *testing.T) { 21 | db := tests.NewMemDB(t) 22 | defer rese.F0(rese.P1(db.DB()).Close) 23 | 24 | var result int 25 | require.NoError(t, db.Raw("SELECT 1").Scan(&result).Error) 26 | require.Equal(t, 1, result) 27 | } 28 | -------------------------------------------------------------------------------- /internal/examples/tests/example13/internal/models/example3.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "gorm.io/gorm" 4 | 5 | type Example3 struct { 6 | gorm.Model 7 | U账号 string `gorm:"column:username;uniqueIndex:udx_example3s_USERNAME; NOT NULL" mom:"udx:CNM;"` 8 | N昵称 string `gorm:"column:nickname; NOT NULL"` 9 | V分数 uint64 `gorm:"column:score;index:idx_example3s_score;" mom:"idx:cnm"` 10 | Age int `gorm:"index:idx_example3s_age;" mom:"idx:cnm"` 11 | Uuid int `gorm:"uniqueIndex:udx_example3s_uuid;index:idx_example3s_uuid;" mom:"idx:cnm;udx:cnm"` 12 | Rank int `gorm:"index" mom:"udx:cnm"` // udx not affect index 13 | } 14 | 15 | func (*Example3) TableName() string { 16 | return "example3s" 17 | } 18 | -------------------------------------------------------------------------------- /internal/unicodehex/unicodehex.go: -------------------------------------------------------------------------------- 1 | package unicodehex 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/hex" 6 | "strings" 7 | ) 8 | 9 | func StringToHex4Uppercase(s string) (results []string) { 10 | for _, c := range s { 11 | results = append(results, Uint32ToHex4Uppercase(c)) 12 | } 13 | return results 14 | } 15 | 16 | func Uint32ToHex4Lowercase(c int32) string { 17 | return strings.ToLower(Uint32ToHex4s(uint32(c))) 18 | } 19 | 20 | func Uint32ToHex4Uppercase(c int32) string { 21 | return strings.ToUpper(Uint32ToHex4s(uint32(c))) 22 | } 23 | 24 | func Uint32ToHex4s(x uint32) string { 25 | return hex.EncodeToString(Uint32ToLittleEndianBytes(x))[:4] 26 | } 27 | 28 | func Uint32ToLittleEndianBytes(x uint32) []byte { 29 | var src = make([]byte, 4) 30 | binary.LittleEndian.PutUint32(src, uint32(x)) 31 | return src 32 | } 33 | -------------------------------------------------------------------------------- /gormmomname/pattern_lowercase30.go: -------------------------------------------------------------------------------- 1 | package gormmomname 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/yyle88/gormmom/internal/simplename" 7 | "github.com/yyle88/gormmom/internal/utils" 8 | ) 9 | 10 | type Lowercase30pattern struct{} 11 | 12 | func NewLowercase30pattern() *Lowercase30pattern { 13 | return &Lowercase30pattern{} 14 | } 15 | 16 | func (G *Lowercase30pattern) GetPatternEnum() PatternEnum { 17 | return "s30" //表示30个小写字符(含数字和下划线)组成的列名 18 | } 19 | 20 | func (G *Lowercase30pattern) CheckColumnName(columnName string) bool { 21 | return utils.NewCommonRegexp(30).MatchString(columnName) 22 | } 23 | 24 | func (G *Lowercase30pattern) BuildColumnName(fieldName string) string { 25 | columnName := strings.ToLower(simplename.BuildColumnName(fieldName)) 26 | utils.MustMatchRegexp(utils.NewCommonRegexp(30), columnName) 27 | return columnName 28 | } 29 | -------------------------------------------------------------------------------- /gormmomname/pattern_lowercase63.go: -------------------------------------------------------------------------------- 1 | package gormmomname 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/yyle88/gormmom/internal/simplename" 7 | "github.com/yyle88/gormmom/internal/utils" 8 | ) 9 | 10 | type Lowercase63pattern struct{} 11 | 12 | func NewLowercase63pattern() *Lowercase63pattern { 13 | return &Lowercase63pattern{} 14 | } 15 | 16 | func (G *Lowercase63pattern) GetPatternEnum() PatternEnum { 17 | return "s63" //表示63个小写字符(含数字和下划线)组成的列名 18 | } 19 | 20 | func (G *Lowercase63pattern) CheckColumnName(columnName string) bool { 21 | return utils.NewCommonRegexp(63).MatchString(columnName) 22 | } 23 | 24 | func (G *Lowercase63pattern) BuildColumnName(fieldName string) string { 25 | columnName := strings.ToLower(simplename.BuildColumnName(fieldName)) 26 | utils.MustMatchRegexp(utils.NewCommonRegexp(63), columnName) 27 | return columnName 28 | } 29 | -------------------------------------------------------------------------------- /gormmomname/pattern_uppercase30.go: -------------------------------------------------------------------------------- 1 | package gormmomname 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/yyle88/gormmom/internal/simplename" 7 | "github.com/yyle88/gormmom/internal/utils" 8 | ) 9 | 10 | type Uppercase30pattern struct{} 11 | 12 | func NewUppercase30pattern() *Uppercase30pattern { 13 | return &Uppercase30pattern{} 14 | } 15 | 16 | func (G *Uppercase30pattern) GetPatternEnum() PatternEnum { 17 | return "S30" //表示30个大写字符(含数字和下划线)组成的列名 18 | } 19 | 20 | func (G *Uppercase30pattern) CheckColumnName(columnName string) bool { 21 | return utils.NewCommonRegexp(30).MatchString(columnName) 22 | } 23 | 24 | func (G *Uppercase30pattern) BuildColumnName(fieldName string) string { 25 | columnName := strings.ToUpper(simplename.BuildColumnName(fieldName)) 26 | utils.MustMatchRegexp(utils.NewCommonRegexp(30), columnName) 27 | return columnName 28 | } 29 | -------------------------------------------------------------------------------- /gormmomname/pattern_uppercase63.go: -------------------------------------------------------------------------------- 1 | package gormmomname 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/yyle88/gormmom/internal/simplename" 7 | "github.com/yyle88/gormmom/internal/utils" 8 | ) 9 | 10 | type Uppercase63pattern struct{} 11 | 12 | func NewUppercase63pattern() *Uppercase63pattern { 13 | return &Uppercase63pattern{} 14 | } 15 | 16 | func (G *Uppercase63pattern) GetPatternEnum() PatternEnum { 17 | return "S63" //表示63个大写字符(含数字和下划线)组成的列名 18 | } 19 | 20 | func (G *Uppercase63pattern) CheckColumnName(columnName string) bool { 21 | return utils.NewCommonRegexp(63).MatchString(columnName) 22 | } 23 | 24 | func (G *Uppercase63pattern) BuildColumnName(fieldName string) string { 25 | columnName := strings.ToUpper(simplename.BuildColumnName(fieldName)) 26 | utils.MustMatchRegexp(utils.NewCommonRegexp(63), columnName) 27 | return columnName 28 | } 29 | -------------------------------------------------------------------------------- /internal/examples/tests/example11/internal/models/example1.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type Example struct { 6 | ID int32 `gorm:"column:id; primaryKey;" json:"id"` 7 | V名称 string `gorm:"column:v_0d54_f079;type:text" mom:"mcp:s63;"` 8 | V字段 string `gorm:"column:v_575b_b56b;" mom:"mcp:s63;"` 9 | V性别 string `gorm:"column:v_2760_2b52;" mom:"mcp:s63;"` 10 | V特殊 string `gorm:"column:v_7972_8a6b;type:int32" mom:"mcp:s63;"` 11 | V年龄 int `gorm:"column:v_745e_849f;" json:"age" mom:"mcp:s63;"` //理论上不要直接给model添加json标签,因为那是view层的逻辑,但实际上假如非这样做也能处理 12 | Rank int32 13 | V身高 int32 `gorm:"column:v_ab8e_d89a;" mom:"mcp:s63;"` 14 | V体重 int32 `gorm:"column:v_534f_cd91;" mom:"mcp:s63;"` 15 | CreatedAt time.Time `gorm:"autoCreateTime"` 16 | UpdatedAt time.Time `gorm:"autoUpdateTime"` 17 | } 18 | -------------------------------------------------------------------------------- /internal/examples/example1/internal/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | /* 4 | type T用户 struct { 5 | ID uint `gorm:"primaryKey"` 6 | U用户名 string `gorm:"uniqueIndex"` 7 | E邮箱 string `gorm:"index"` 8 | A年龄 int `gorm:""` 9 | D电话 string `gorm:""` 10 | J住所 string `gorm:""` 11 | S状态 string `gorm:"index"` 12 | } 13 | */ 14 | type T用户 struct { 15 | ID uint `gorm:"primaryKey"` 16 | U用户名 string `gorm:"column:u_2875_3762_0d54;uniqueIndex:udx_users_u_2875_3762_0d54" mom:"mcp:s63;udx:cnm;"` 17 | E邮箱 string `gorm:"column:e_ae90_b17b;index:idx_users_e_ae90_b17b" mom:"mcp:s63;idx:cnm;"` 18 | A年龄 int `gorm:"column:a_745e_849f;" mom:"mcp:s63;"` 19 | D电话 string `gorm:"column:d_3575_dd8b;" mom:"mcp:s63;"` 20 | J住所 string `gorm:"column:j_4f4f_4062;" mom:"mcp:s63;"` 21 | S状态 string `gorm:"column:s_b672_0160;index:idx_users_s_b672_0160" mom:"mcp:s63;idx:cnm;"` 22 | } 23 | 24 | func (*T用户) TableName() string { 25 | return "users" 26 | } 27 | -------------------------------------------------------------------------------- /gormmomname/pattern_test.go: -------------------------------------------------------------------------------- 1 | package gormmomname 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestLowercase63pattern_CheckColumnName(t *testing.T) { 10 | pattern := NewLowercase63pattern() 11 | 12 | require.True(t, pattern.CheckColumnName("abc")) 13 | } 14 | 15 | func TestUppercase63pattern_CheckColumnName(t *testing.T) { 16 | pattern := NewUppercase63pattern() 17 | 18 | require.True(t, pattern.CheckColumnName("ABC")) 19 | } 20 | 21 | func TestLowercase30pattern_BuildColumnName(t *testing.T) { 22 | pattern := NewLowercase30pattern() 23 | 24 | t.Log(pattern.BuildColumnName("v杨亦乐")) 25 | t.Log(pattern.BuildColumnName("v刘亦菲")) 26 | t.Log(pattern.BuildColumnName("v古天乐")) 27 | } 28 | 29 | func TestUppercase63pattern_BuildColumnName(t *testing.T) { 30 | pattern := NewUppercase63pattern() 31 | 32 | t.Log(pattern.BuildColumnName("v杨亦乐")) 33 | t.Log(pattern.BuildColumnName("v刘亦菲的亦")) 34 | t.Log(pattern.BuildColumnName("v古天乐的乐")) 35 | } 36 | -------------------------------------------------------------------------------- /internal/examples/tests/example13/internal/models/example2.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type Example2 struct { 6 | ID int32 `gorm:"column:id; primaryKey;" json:"id"` 7 | V名称 string `gorm:"column:V_0D54_F079;type:text" mom:"mcp:S63;"` //使用大写字母的规则 8 | V字段 string `gorm:"column:v_575b_b56b;" mom:"mcp:s63;"` //使用小写字母的规则 9 | V性别 string `gorm:"column:v_2760_2b52;" mom:"mcp:s63;"` 10 | V特殊 string `gorm:"column:V_7972_8A6B;type:int32" mom:"mcp:S63;"` 11 | V年龄 int `gorm:"column:v_745e_849f;index:idx_example2s_V_745E_849F" json:"age" mom:"mcp:s63;idx:CNM;"` //理论上不要直接给model添加json标签,因为那是view层的逻辑,但实际上假如非这样做也能处理 12 | Rank int32 13 | V身高 int32 `gorm:"column:V_AB8E_D89A;" mom:"mcp:S63;"` 14 | V体重 int32 `gorm:"column:v_534f_cd91;" mom:"mcp:s63;"` 15 | CreatedAt time.Time `gorm:"autoCreateTime"` 16 | UpdatedAt time.Time `gorm:"autoUpdateTime"` 17 | } 18 | 19 | func (*Example2) TableName() string { 20 | return "example2s" 21 | } 22 | -------------------------------------------------------------------------------- /internal/examples/tests/example13/internal/models/example1.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type Example struct { 6 | ID int32 `gorm:"column:id; primaryKey;" json:"id"` 7 | V名称 string `gorm:"column:v_0d54_f079;type:text;uniqueIndex:udx_example1s_v_0d54_f079" mom:"mcp:s63;udx:cnm;"` 8 | V字段 string `gorm:"column:v_575b_b56b;" mom:"mcp:s63;"` 9 | V性别 string `gorm:"column:v_2760_2b52;" mom:"mcp:s63;"` 10 | V特殊 string `gorm:"column:v_7972_8a6b;type:int32" mom:"mcp:s63;"` 11 | V年龄 int `gorm:"column:v_745e_849f;" json:"age" mom:"mcp:s63;"` //理论上不要直接给model添加json标签,因为那是view层的逻辑,但实际上假如非这样做也能处理 12 | Rank int32 13 | V身高 int32 `gorm:"column:v_ab8e_d89a;" mom:"mcp:s63;"` 14 | V体重 int32 `gorm:"column:v_534f_cd91;index:idx_example1s_V_534F_CD91" mom:"mcp:s63;idx:CNM;"` 15 | CreatedAt time.Time `gorm:"autoCreateTime"` 16 | UpdatedAt time.Time `gorm:"autoUpdateTime"` 17 | } 18 | 19 | func (*Example) TableName() string { 20 | return "example1s" 21 | } 22 | -------------------------------------------------------------------------------- /internal/examples/tests/example12/internal/models/example2.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type Example struct { 6 | ID int32 `gorm:"column:id; primaryKey;" json:"id"` 7 | V名称 string `gorm:"column:V_0D54_F079;type:text;uniqueIndex:udx_examples_v_0d54_f079" mom:"mcp:S63;udx:cnm;"` //使用大写字母的规则 8 | V字段 string `gorm:"column:v_575b_b56b;" mom:"mcp:s63;"` //使用小写字母的规则 9 | V性别 string `gorm:"column:v_2760_2b52;" mom:"mcp:s63;"` 10 | V特殊 string `gorm:"column:V_7972_8A6B;type:int32;index:idx_examples_v_7972_8a6b" mom:"mcp:S63;idx:cnm;"` 11 | V年龄 int `gorm:"column:v_745e_849f;" json:"age" mom:"mcp:s63;"` //理论上不要直接给model添加json标签,因为那是view层的逻辑,但实际上假如非这样做也能处理 12 | Rank int32 13 | V身高 int32 `gorm:"column:V_AB8E_D89A;" mom:"mcp:S63;"` 14 | V体重 int32 `gorm:"column:v_534f_cd91;" mom:"mcp:s63;"` 15 | CreatedAt time.Time `gorm:"autoCreateTime;index;"` 16 | UpdatedAt time.Time `gorm:"autoUpdateTime;uniqueIndex;"` 17 | } 18 | -------------------------------------------------------------------------------- /internal/examples/example3/internal/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // 文档中的韩语示例模型(原始版本) 4 | /* 5 | type T사용자 struct { 6 | ID uint `gorm:"primaryKey"` 7 | U사용자명 string `gorm:"uniqueIndex"` 8 | E이메일 string `gorm:"index"` 9 | N나이 int `gorm:""` 10 | J전화 string `gorm:""` 11 | J주소 string `gorm:""` 12 | S상태 string `gorm:"index"` 13 | } 14 | */ 15 | 16 | // 经过 gormmom 生成器处理后的韩语模型 17 | type T사용자 struct { 18 | ID uint `gorm:"primaryKey"` 19 | U사용자명 string `gorm:"column:u_acc0_a9c6_90c7_85ba;uniqueIndex:udx_users_u_acc0_a9c6_90c7_85ba" mom:"mcp:s63;udx:cnm;"` 20 | E이메일 string `gorm:"column:e_74c7_54ba_7cc7;index:idx_users_e_74c7_54ba_7cc7" mom:"mcp:s63;idx:cnm;"` 21 | N나이 int `gorm:"column:n_98b0_74c7;" mom:"mcp:s63;"` 22 | J전화 string `gorm:"column:j_04c8_54d6;" mom:"mcp:s63;"` 23 | J주소 string `gorm:"column:j_fcc8_8cc1;" mom:"mcp:s63;"` 24 | S상태 string `gorm:"column:s_c1c0_dcd0;index:idx_users_s_c1c0_dcd0" mom:"mcp:s63;idx:cnm;"` 25 | } 26 | 27 | func (*T사용자) TableName() string { 28 | return "users" 29 | } 30 | -------------------------------------------------------------------------------- /internal/examples/example2/internal/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // 文档中的日语示例模型(原始版本) 4 | /* 5 | type Tユーザー struct { 6 | ID uint `gorm:"primaryKey"` 7 | Uユーザー名 string `gorm:"uniqueIndex"` 8 | Eメール string `gorm:"index"` 9 | A年齢 int `gorm:""` 10 | D電話 string `gorm:""` 11 | J住所 string `gorm:""` 12 | Sステータス string `gorm:"index"` 13 | } 14 | */ 15 | 16 | // 经过 gormmom 生成器处理后的日语模型 17 | type Tユーザー struct { 18 | ID uint `gorm:"primaryKey"` 19 | Uユーザー名 string `gorm:"column:u_e630_fc30_b630_fc30_0d54;uniqueIndex:udx_users_u_e630_fc30_b630_fc30_0d54" mom:"mcp:s63;udx:cnm;"` 20 | Eメール string `gorm:"column:e_e130_fc30_eb30;index:idx_users_e_e130_fc30_eb30" mom:"mcp:s63;idx:cnm;"` 21 | A年齢 int `gorm:"column:a_745e_629f;" mom:"mcp:s63;"` 22 | D電話 string `gorm:"column:d_fb96_718a;" mom:"mcp:s63;"` 23 | J住所 string `gorm:"column:j_4f4f_4062;" mom:"mcp:s63;"` 24 | Sステータス string `gorm:"column:s_b930_c630_fc30_bf30_b930;index:idx_users_s_b930_c630_fc30_bf30_b930" mom:"mcp:s63;idx:cnm;"` 25 | } 26 | 27 | func (*Tユーザー) TableName() string { 28 | return "users" 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 yangyile-yyle88 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /gorm_struct_test.go: -------------------------------------------------------------------------------- 1 | package gormmom 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yyle88/runpath" 8 | ) 9 | 10 | type Example1 struct { 11 | Name string `gorm:"column:name"` 12 | Age int `gorm:"column:age"` 13 | Sex bool `gorm:"column:sex"` 14 | } 15 | 16 | func TestParseStruct(t *testing.T) { 17 | param := ParseStruct[Example1](runpath.CurrentPath()) 18 | t.Log(param.sourcePath) 19 | t.Log(param.structName) 20 | require.Equal(t, "Example1", param.structName) 21 | } 22 | 23 | func TestParseObject(t *testing.T) { 24 | param := ParseObject(runpath.CurrentPath(), &Example1{}) 25 | t.Log(param.sourcePath) 26 | t.Log(param.structName) 27 | require.Equal(t, "Example1", param.structName) 28 | } 29 | 30 | type Example2 struct { 31 | V姓名 string `gorm:"column:name"` 32 | V年龄 int `gorm:"column:age"` 33 | V性别 bool `gorm:"column:sex"` 34 | } 35 | 36 | func TestParseObjects(t *testing.T) { 37 | params := ParseObjects(runpath.PARENT.Path(), []any{&Example1{}, &Example2{}}) 38 | require.Len(t, params, 2) 39 | require.Equal(t, "Example1", params[0].structName) 40 | require.Equal(t, "Example2", params[1].structName) 41 | } 42 | -------------------------------------------------------------------------------- /gormidxname/pattern_lowercase63.go: -------------------------------------------------------------------------------- 1 | package gormidxname 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/yyle88/gormmom/internal/simpleindexname" 7 | "github.com/yyle88/gormmom/internal/utils" 8 | "gorm.io/gorm/schema" 9 | ) 10 | 11 | type Lowercase63pattern struct{} 12 | 13 | func NewLowercase63pattern() *Lowercase63pattern { 14 | return &Lowercase63pattern{} 15 | } 16 | 17 | func (G *Lowercase63pattern) GetPatternEnum() PatternEnum { 18 | return "cnm" //表示使用 column name 作为拼接索引名的规则,但后缀是小写字母的 19 | } 20 | 21 | func (G *Lowercase63pattern) CheckIndexName(indexName string) bool { 22 | return utils.NewCommonRegexp(63).MatchString(indexName) 23 | } 24 | 25 | func (G *Lowercase63pattern) BuildIndexName(schemaIndex *schema.Index, param *BuildIndexParam) *IndexNameResult { 26 | result := simpleindexname.BuildIndexName(schemaIndex, &simpleindexname.BuildIndexParam{ 27 | TableName: param.TableName, 28 | FieldName: param.FieldName, 29 | ColumnName: strings.ToLower(param.ColumnName), 30 | }) 31 | utils.MustMatchRegexp(utils.NewCommonRegexp(63), result.NewIndexName) 32 | return &IndexNameResult{ 33 | TagFieldName: result.TagFieldName, 34 | NewIndexName: result.NewIndexName, 35 | IdxUdxPrefix: IndexPatternTagEnum(result.IdxUdxPrefix), 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /gormidxname/pattern_uppercase63.go: -------------------------------------------------------------------------------- 1 | package gormidxname 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/yyle88/gormmom/internal/simpleindexname" 7 | "github.com/yyle88/gormmom/internal/utils" 8 | "gorm.io/gorm/schema" 9 | ) 10 | 11 | type Uppercase63pattern struct{} 12 | 13 | func NewUppercase63pattern() *Uppercase63pattern { 14 | return &Uppercase63pattern{} 15 | } 16 | 17 | func (G *Uppercase63pattern) GetPatternEnum() PatternEnum { 18 | return "CNM" //表示使用 column name 作为拼接索引名的规则,但后缀是大写字母的 19 | } 20 | 21 | func (G *Uppercase63pattern) CheckIndexName(indexName string) bool { 22 | return utils.NewCommonRegexp(63).MatchString(indexName) 23 | } 24 | 25 | func (G *Uppercase63pattern) BuildIndexName(schemaIndex *schema.Index, param *BuildIndexParam) *IndexNameResult { 26 | result := simpleindexname.BuildIndexName(schemaIndex, &simpleindexname.BuildIndexParam{ 27 | TableName: param.TableName, 28 | FieldName: param.FieldName, 29 | ColumnName: strings.ToUpper(param.ColumnName), 30 | }) 31 | utils.MustMatchRegexp(utils.NewCommonRegexp(63), result.NewIndexName) 32 | return &IndexNameResult{ 33 | TagFieldName: result.TagFieldName, 34 | NewIndexName: result.NewIndexName, 35 | IdxUdxPrefix: IndexPatternTagEnum(result.IdxUdxPrefix), 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/examples/example2/internal/models/gormcnm_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yyle88/gormcngen" 8 | "github.com/yyle88/gormmom" 9 | "github.com/yyle88/runpath" 10 | "github.com/yyle88/runpath/runtestpath" 11 | ) 12 | 13 | func TestGenGormMomAndCnm(t *testing.T) { 14 | objects := []interface{}{ 15 | &Tユーザー{}, 16 | } 17 | 18 | require.True(t, t.Run("GenGormMom", func(t *testing.T) { 19 | params := gormmom.ParseObjects(runpath.PARENT.Path(), objects) 20 | cfg := gormmom.NewConfigs(params, gormmom.NewOptions()) 21 | t.Log("第一步:使用 gormmom 生成日语字段的 mom 标签") 22 | 23 | result := cfg.Generate() 24 | require.False(t, result.HasChange()) // 因为已经替换过,而且写到了新代码里,因此这里就只能是没有变化 25 | require.NoError(t, cfg.ValidateGormTags()) 26 | })) 27 | 28 | require.True(t, t.Run("GenGormCnm", func(t *testing.T) { 29 | absPath := runtestpath.SrcPath(t) 30 | t.Log("第二步:使用 gormcngen 生成类型安全的列方法") 31 | 32 | options := gormcngen.NewOptions(). 33 | WithColumnClassExportable(true). 34 | WithColumnsMethodRecvName("T"). 35 | WithColumnsCheckFieldType(true) 36 | 37 | cfg := gormcngen.NewConfigs(objects, options, absPath) 38 | cfg.Gen() 39 | t.Log("gormcngen 生成完成") 40 | })) 41 | } 42 | -------------------------------------------------------------------------------- /internal/examples/example3/internal/models/gormcnm_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yyle88/gormcngen" 8 | "github.com/yyle88/gormmom" 9 | "github.com/yyle88/runpath" 10 | "github.com/yyle88/runpath/runtestpath" 11 | ) 12 | 13 | func TestGenGormMomAndCnm(t *testing.T) { 14 | objects := []interface{}{ 15 | &T사용자{}, 16 | } 17 | 18 | require.True(t, t.Run("GenGormMom", func(t *testing.T) { 19 | params := gormmom.ParseObjects(runpath.PARENT.Path(), objects) 20 | cfg := gormmom.NewConfigs(params, gormmom.NewOptions()) 21 | t.Log("第一步:使用 gormmom 生成韩语字段的 mom 标签") 22 | 23 | result := cfg.Generate() 24 | require.False(t, result.HasChange()) // 因为已经替换过,而且写到了新代码里,因此这里就只能是没有变化 25 | require.NoError(t, cfg.ValidateGormTags()) 26 | })) 27 | 28 | require.True(t, t.Run("GenGormCnm", func(t *testing.T) { 29 | absPath := runtestpath.SrcPath(t) 30 | t.Log("第二步:使用 gormcngen 生成类型安全的列方法") 31 | 32 | options := gormcngen.NewOptions(). 33 | WithColumnClassExportable(true). 34 | WithColumnsMethodRecvName("T"). 35 | WithColumnsCheckFieldType(true) 36 | 37 | cfg := gormcngen.NewConfigs(objects, options, absPath) 38 | cfg.Gen() 39 | t.Log("gormcngen 生成完成") 40 | })) 41 | } 42 | -------------------------------------------------------------------------------- /internal/examples/example1/internal/models/gormcnm_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yyle88/gormcngen" 8 | "github.com/yyle88/gormmom" 9 | "github.com/yyle88/osexistpath/osmustexist" 10 | "github.com/yyle88/runpath" 11 | "github.com/yyle88/runpath/runtestpath" 12 | ) 13 | 14 | func TestGen(t *testing.T) { 15 | objects := []interface{}{ 16 | &T用户{}, 17 | } 18 | 19 | require.True(t, t.Run("GenGormMom", func(t *testing.T) { 20 | params := gormmom.ParseObjects(runpath.PARENT.Path(), objects) 21 | 22 | cfg := gormmom.NewConfigs(params, gormmom.NewOptions()) 23 | t.Log(cfg) 24 | 25 | result := cfg.Generate() 26 | require.False(t, result.HasChange()) // 因为已经替换过,而且写到了新代码里,因此这里就只能是没有变化 27 | require.NoError(t, cfg.ValidateGormTags()) 28 | })) 29 | 30 | // 使用 require.True(t, t.Run(---)) 限制只有前一步成功才执行后一步的 31 | 32 | require.True(t, t.Run("GenGormCnm", func(t *testing.T) { 33 | absPath := osmustexist.FILE(runtestpath.SrcPath(t)) 34 | t.Log(absPath) 35 | 36 | options := gormcngen.NewOptions(). 37 | WithColumnClassExportable(true). //中间类型名称的样式为导出的 T学生Columns 38 | WithColumnsMethodRecvName("T"). 39 | WithColumnsCheckFieldType(true) 40 | 41 | cfg := gormcngen.NewConfigs(objects, options, absPath) 42 | cfg.Gen() 43 | })) 44 | } 45 | -------------------------------------------------------------------------------- /internal/examples/tests/example11/internal/models/gormcnm_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yyle88/gormcngen" 8 | "github.com/yyle88/gormmom" 9 | "github.com/yyle88/osexistpath/osmustexist" 10 | "github.com/yyle88/runpath" 11 | "github.com/yyle88/runpath/runtestpath" 12 | ) 13 | 14 | func TestGen(t *testing.T) { 15 | objects := []interface{}{ 16 | &Example{}, 17 | } 18 | 19 | require.True(t, t.Run("GenGormMom", func(t *testing.T) { 20 | params := gormmom.ParseObjects(runpath.PARENT.Path(), objects) 21 | 22 | cfg := gormmom.NewConfigs(params, gormmom.NewOptions()) 23 | t.Log(cfg) 24 | 25 | result := cfg.Generate() 26 | require.False(t, result.HasChange()) // 因为已经替换过,而且写到了新代码里,因此这里就只能是没有变化 27 | require.NoError(t, cfg.ValidateGormTags()) 28 | })) 29 | 30 | // 使用 require.True(t, t.Run(---)) 限制只有前一步成功才执行后一步的 31 | 32 | require.True(t, t.Run("GenGormCnm", func(t *testing.T) { 33 | absPath := osmustexist.FILE(runtestpath.SrcPath(t)) 34 | t.Log(absPath) 35 | 36 | options := gormcngen.NewOptions(). 37 | WithColumnClassExportable(true). //中间类型名称的样式为导出的 ExampleColumns 38 | WithColumnsMethodRecvName("T"). 39 | WithColumnsCheckFieldType(true) 40 | 41 | cfg := gormcngen.NewConfigs(objects, options, absPath) 42 | cfg.Gen() 43 | })) 44 | } 45 | -------------------------------------------------------------------------------- /internal/examples/tests/example13/internal/models/gormcnm_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yyle88/gormcngen" 8 | "github.com/yyle88/gormmom" 9 | "github.com/yyle88/osexistpath/osmustexist" 10 | "github.com/yyle88/runpath" 11 | "github.com/yyle88/runpath/runtestpath" 12 | ) 13 | 14 | func TestGen(t *testing.T) { 15 | objects := []interface{}{ 16 | &Example{}, 17 | &Example2{}, 18 | &Example3{}, 19 | } 20 | 21 | require.True(t, t.Run("GenGormMom", func(t *testing.T) { 22 | params := gormmom.ParseObjects(runpath.PARENT.Path(), objects) 23 | 24 | cfg := gormmom.NewConfigs(params, gormmom.NewOptions()) 25 | t.Log(cfg) 26 | 27 | result := cfg.Generate() 28 | require.False(t, result.HasChange()) // 因为已经替换过,而且写到了新代码里,因此这里就只能是没有变化 29 | require.NoError(t, cfg.ValidateGormTags()) 30 | })) 31 | 32 | // 使用 require.True(t, t.Run(---)) 限制只有前一步成功才执行后一步的 33 | 34 | require.True(t, t.Run("GenGormCnm", func(t *testing.T) { 35 | absPath := osmustexist.FILE(runtestpath.SrcPath(t)) 36 | t.Log(absPath) 37 | 38 | options := gormcngen.NewOptions(). 39 | WithColumnClassExportable(true). //中间类型名称的样式为导出的 ExampleColumns 40 | WithColumnsMethodRecvName("T"). 41 | WithColumnsCheckFieldType(true) 42 | 43 | cfg := gormcngen.NewConfigs(objects, options, absPath) 44 | cfg.Gen() 45 | })) 46 | } 47 | -------------------------------------------------------------------------------- /internal/examples/example1/internal/models/gormcnm.go: -------------------------------------------------------------------------------- 1 | // Code generated using gormcngen. DO NOT EDIT. 2 | // This file was auto generated via github.com/yyle88/gormcngen 3 | // Generated from: gormcnm_test.go:41 -> models.TestGen.func2 4 | // ========== GORMCNGEN:DO-NOT-EDIT-MARKER:END ========== 5 | 6 | package models 7 | 8 | import "github.com/yyle88/gormcnm" 9 | 10 | func (T *T用户) Columns() *T用户Columns { 11 | return &T用户Columns{ 12 | // Auto-generated: column names and types mapping. DO NOT EDIT. // 自动生成:列名和类型映射。请勿编辑。 13 | ID: gormcnm.Cnm(T.ID, "id"), 14 | U用户名: gormcnm.Cnm(T.U用户名, "u_2875_3762_0d54"), 15 | E邮箱: gormcnm.Cnm(T.E邮箱, "e_ae90_b17b"), 16 | A年龄: gormcnm.Cnm(T.A年龄, "a_745e_849f"), 17 | D电话: gormcnm.Cnm(T.D电话, "d_3575_dd8b"), 18 | J住所: gormcnm.Cnm(T.J住所, "j_4f4f_4062"), 19 | S状态: gormcnm.Cnm(T.S状态, "s_b672_0160"), 20 | } 21 | } 22 | 23 | type T用户Columns struct { 24 | // Auto-generated: embedding operation functions to make it simple to use. DO NOT EDIT. // 自动生成:嵌入操作函数便于使用。请勿编辑。 25 | gormcnm.ColumnOperationClass 26 | // Auto-generated: column names and types in database table. DO NOT EDIT. // 自动生成:数据库表的列名和类型。请勿编辑。 27 | ID gormcnm.ColumnName[uint] 28 | U用户名 gormcnm.ColumnName[string] 29 | E邮箱 gormcnm.ColumnName[string] 30 | A年龄 gormcnm.ColumnName[int] 31 | D电话 gormcnm.ColumnName[string] 32 | J住所 gormcnm.ColumnName[string] 33 | S状态 gormcnm.ColumnName[string] 34 | } 35 | -------------------------------------------------------------------------------- /internal/examples/tests/example12/internal/models/gormcnm_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yyle88/gormcngen" 8 | "github.com/yyle88/gormmom" 9 | "github.com/yyle88/gormmom/gormmomname" 10 | "github.com/yyle88/osexistpath/osmustexist" 11 | "github.com/yyle88/runpath" 12 | "github.com/yyle88/runpath/runtestpath" 13 | ) 14 | 15 | func TestGen(t *testing.T) { 16 | objects := []interface{}{ 17 | &Example{}, 18 | } 19 | 20 | require.True(t, t.Run("GenGormMom", func(t *testing.T) { 21 | params := gormmom.ParseObjects(runpath.PARENT.Path(), objects) 22 | 23 | cfg := gormmom.NewConfigs(params, gormmom.NewOptions().WithDefaultColumnPattern(gormmomname.NewUppercase63pattern())) 24 | t.Log(cfg) 25 | 26 | result := cfg.Generate() 27 | require.False(t, result.HasChange()) // 因为已经替换过,而且写到了新代码里,因此这里就只能是没有变化 28 | require.NoError(t, cfg.ValidateGormTags()) 29 | })) 30 | 31 | // 使用 require.True(t, t.Run(---)) 限制只有前一步成功才执行后一步的 32 | 33 | require.True(t, t.Run("GenGormCnm", func(t *testing.T) { 34 | absPath := osmustexist.FILE(runtestpath.SrcPath(t)) 35 | t.Log(absPath) 36 | 37 | options := gormcngen.NewOptions(). 38 | WithColumnClassExportable(true). //中间类型名称的样式为导出的 ExampleColumns 39 | WithColumnsMethodRecvName("T"). 40 | WithColumnsCheckFieldType(true) 41 | 42 | cfg := gormcngen.NewConfigs(objects, options, absPath) 43 | cfg.Gen() 44 | })) 45 | } 46 | -------------------------------------------------------------------------------- /internal/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "github.com/yyle88/neatjson/neatjsons" 9 | "github.com/yyle88/rese" 10 | "github.com/yyle88/rese/resb" 11 | "github.com/yyle88/runpath" 12 | ) 13 | 14 | func TestListGoFiles(t *testing.T) { 15 | paths := ListGoFiles(runpath.PARENT.Path()) 16 | t.Log(neatjsons.S(paths)) 17 | require.NotEmpty(t, paths) 18 | } 19 | 20 | type Example struct { 21 | Name string `json:"name"` 22 | Account string `gorm:"column:account"` 23 | Score string `yaml:"score"` 24 | } 25 | 26 | func TestParseTags(t *testing.T) { 27 | data := rese.A1(os.ReadFile(runpath.Path())) 28 | tags := ParseTags(data, &Example{}) 29 | t.Log(neatjsons.S(tags)) 30 | require.Equal(t, 3, tags.Size()) 31 | keys := tags.Keys() 32 | require.Equal(t, "Name", keys[0]) 33 | require.Equal(t, `json:"name"`, TrimBackticks(resb.C1(tags.Get(keys[0])))) 34 | require.Equal(t, "Account", keys[1]) 35 | require.Equal(t, `gorm:"column:account"`, TrimBackticks(resb.C1(tags.Get(keys[1])))) 36 | require.Equal(t, "Score", keys[2]) 37 | require.Equal(t, `yaml:"score"`, TrimBackticks(resb.C1(tags.Get(keys[2])))) 38 | } 39 | 40 | func TestParseTagsTrimBackticks(t *testing.T) { 41 | data := rese.A1(os.ReadFile(runpath.Path())) 42 | tags := ParseTagsTrimBackticks(data, &Example{}) 43 | t.Log(neatjsons.S(tags)) 44 | require.Equal(t, `gorm:"column:account"`, resb.C1(tags.Get("Account"))) 45 | } 46 | -------------------------------------------------------------------------------- /internal/examples/example3/internal/models/gormcnm.go: -------------------------------------------------------------------------------- 1 | // Code generated using gormcngen. DO NOT EDIT. 2 | // This file was auto generated via github.com/yyle88/gormcngen 3 | // Generated from: gormcnm_test.go:37 -> models.TestGenGormMomAndCnm.func2 4 | // ========== GORMCNGEN:DO-NOT-EDIT-MARKER:END ========== 5 | 6 | package models 7 | 8 | import "github.com/yyle88/gormcnm" 9 | 10 | // 这个文件将被 gormcngen 自动生成和更新 11 | 12 | func (T *T사용자) Columns() *T사용자Columns { 13 | return &T사용자Columns{ 14 | // Auto-generated: column names and types mapping. DO NOT EDIT. // 自动生成:列名和类型映射。请勿编辑。 15 | ID: gormcnm.Cnm(T.ID, "id"), 16 | U사용자명: gormcnm.Cnm(T.U사용자명, "u_acc0_a9c6_90c7_85ba"), 17 | E이메일: gormcnm.Cnm(T.E이메일, "e_74c7_54ba_7cc7"), 18 | N나이: gormcnm.Cnm(T.N나이, "n_98b0_74c7"), 19 | J전화: gormcnm.Cnm(T.J전화, "j_04c8_54d6"), 20 | J주소: gormcnm.Cnm(T.J주소, "j_fcc8_8cc1"), 21 | S상태: gormcnm.Cnm(T.S상태, "s_c1c0_dcd0"), 22 | } 23 | } 24 | 25 | type T사용자Columns struct { 26 | // Auto-generated: embedding operation functions to make it simple to use. DO NOT EDIT. // 自动生成:嵌入操作函数便于使用。请勿编辑。 27 | gormcnm.ColumnOperationClass 28 | // Auto-generated: column names and types in database table. DO NOT EDIT. // 自动生成:数据库表的列名和类型。请勿编辑。 29 | ID gormcnm.ColumnName[uint] 30 | U사용자명 gormcnm.ColumnName[string] 31 | E이메일 gormcnm.ColumnName[string] 32 | N나이 gormcnm.ColumnName[int] 33 | J전화 gormcnm.ColumnName[string] 34 | J주소 gormcnm.ColumnName[string] 35 | S상태 gormcnm.ColumnName[string] 36 | } 37 | -------------------------------------------------------------------------------- /internal/examples/example2/internal/models/gormcnm.go: -------------------------------------------------------------------------------- 1 | // Code generated using gormcngen. DO NOT EDIT. 2 | // This file was auto generated via github.com/yyle88/gormcngen 3 | // Generated from: gormcnm_test.go:37 -> models.TestGenGormMomAndCnm.func2 4 | // ========== GORMCNGEN:DO-NOT-EDIT-MARKER:END ========== 5 | 6 | package models 7 | 8 | import "github.com/yyle88/gormcnm" 9 | 10 | // 这个文件将被 gormcngen 自动生成和更新 11 | 12 | func (T *Tユーザー) Columns() *TユーザーColumns { 13 | return &TユーザーColumns{ 14 | // Auto-generated: column names and types mapping. DO NOT EDIT. // 自动生成:列名和类型映射。请勿编辑。 15 | ID: gormcnm.Cnm(T.ID, "id"), 16 | Uユーザー名: gormcnm.Cnm(T.Uユーザー名, "u_e630_fc30_b630_fc30_0d54"), 17 | Eメール: gormcnm.Cnm(T.Eメール, "e_e130_fc30_eb30"), 18 | A年齢: gormcnm.Cnm(T.A年齢, "a_745e_629f"), 19 | D電話: gormcnm.Cnm(T.D電話, "d_fb96_718a"), 20 | J住所: gormcnm.Cnm(T.J住所, "j_4f4f_4062"), 21 | Sステータス: gormcnm.Cnm(T.Sステータス, "s_b930_c630_fc30_bf30_b930"), 22 | } 23 | } 24 | 25 | type TユーザーColumns struct { 26 | // Auto-generated: embedding operation functions to make it simple to use. DO NOT EDIT. // 自动生成:嵌入操作函数便于使用。请勿编辑。 27 | gormcnm.ColumnOperationClass 28 | // Auto-generated: column names and types in database table. DO NOT EDIT. // 自动生成:数据库表的列名和类型。请勿编辑。 29 | ID gormcnm.ColumnName[uint] 30 | Uユーザー名 gormcnm.ColumnName[string] 31 | Eメール gormcnm.ColumnName[string] 32 | A年齢 gormcnm.ColumnName[int] 33 | D電話 gormcnm.ColumnName[string] 34 | J住所 gormcnm.ColumnName[string] 35 | Sステータス gormcnm.ColumnName[string] 36 | } 37 | -------------------------------------------------------------------------------- /internal/tests/tests.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/google/uuid" 8 | "github.com/yyle88/must" 9 | "github.com/yyle88/rese" 10 | "gorm.io/driver/sqlite" 11 | "gorm.io/gorm" 12 | "gorm.io/gorm/logger" 13 | ) 14 | 15 | // NewDBRun runs a function with an in-mem SQLite database 16 | // Auto creates a temporary database connection and handles cleanup 17 | // NewDBRun 在内存数据库中运行函数,用于测试目的 18 | // 自动创建临时数据库连接并在函数执行后处理清理工作 19 | func NewDBRun(t *testing.T, run func(db *gorm.DB)) { 20 | dsn := fmt.Sprintf("file:db-%s?mode=memory&cache=shared", uuid.New().String()) 21 | db := rese.P1(gorm.Open(sqlite.Open(dsn), &gorm.Config{ 22 | Logger: logger.Default.LogMode(logger.Info), 23 | })) 24 | defer rese.F0(rese.P1(db.DB()).Close) 25 | 26 | t.Log("--- DB BEGIN ---") 27 | run(db) 28 | t.Log("--- DB CLOSE ---") 29 | } 30 | 31 | // NewMemDB creates an in-mem SQLite database connection with auto cleanup 32 | // Returns the database connection, cleanup is handled via t.Cleanup 33 | // NewMemDB 创建内存 SQLite 数据库连接,自动清理 34 | // 返回数据库连接,通过 t.Cleanup 处理清理工作 35 | func NewMemDB(t *testing.T) *gorm.DB { 36 | dsn := fmt.Sprintf("file:db-%s?mode=memory&cache=shared", uuid.New().String()) 37 | db := rese.P1(gorm.Open(sqlite.Open(dsn), &gorm.Config{ 38 | Logger: logger.Default.LogMode(logger.Info), 39 | })) 40 | t.Cleanup(func() { 41 | must.Done(rese.P1(db.DB()).Close()) 42 | }) 43 | 44 | t.Log("--- DB BEGIN ---") 45 | t.Cleanup(func() { 46 | t.Log("--- DB CLOSE ---") 47 | }) 48 | return db 49 | } 50 | -------------------------------------------------------------------------------- /internal/docs/README_OLD_DOC.zh.md: -------------------------------------------------------------------------------- 1 | # gormmom 2 | **赋能母语编程** 3 | 4 | --- 5 | 6 | 华为最近发布了仓颉编程语言,但遗憾的是,它仍然不支持直接用中文编程,尽管对这一功能的需求日益增长。 7 | 8 | --- 9 | 10 | 在 Go 编程中,实际上是可以使用母语编写代码的。 11 | 12 | 例如: 13 | ```go 14 | type A学生信息 struct { 15 | V姓名 string 16 | V性别 bool 17 | V年龄 int 18 | } 19 | 20 | func (a *A客户信息) Get姓名() string { return a.V姓名 } 21 | ``` 22 | 23 | 这种方式是完全可行的。 24 | 25 | --- 26 | 27 | 然而,在使用 GORM 时就会遇到一些挑战,因为列名需要显式地使用 GORM 标签定义。 28 | 29 | 这个工具提供了一个简单的解决方案,帮助你轻松生成 GORM 标签,让你在使用 Go 编程时依然能够使用母语。 30 | 31 | --- 32 | 33 | 由于我的英语水平有限,且在找到简单明了的描述词时常常感到困难,我给这个包起名为 **"gormmom"**。 34 | 35 | 虽然掌握英语对于程序员来说至关重要,但我认为即便是那些英语能力已经过了四六级考试或有十年以上编程经验的人,在编写代码时也可能会遇到表达不流畅的情况。此时,或许该考虑一种替代的方法。 36 | 37 | --- 38 | 39 | 例如,在这个项目中,我遇到了在标签中提取内容时的困难。推荐的词汇是 `extract`,但我最初只能想到 `get`、`parse` 或 `obtain` 等替代词。 40 | 41 | 这个经历让我更加坚定了母语编程的必要性。否则,代码往往会变成一些不准确的词语拼凑,这会导致混乱和低效。 42 | 43 | --- 44 | 45 | 另一个我经常面临的问题是,在英语中,相对意义的词通常用不同长度的词来表示。例如: 46 | `up/down`、`left/right`、`open/close`、`start/stop`、`public/private`。 47 | 48 | 程序员常常被迫从一组有限的词汇中选择,这些词汇只能大致适应上下文,比如: 49 | - `get/set` 50 | - `start/close` 51 | - `create/update/select/delete`。 52 | 53 | 这对于非英语母语的人来说,这种不一致性会造成困惑,进而影响代码的清晰度。 54 | 55 | --- 56 | 57 | 目前,这个工具没有包含翻译功能,不能将母语代码自动转换为英文。 58 | 59 | 它的核心功能是简化生成 GORM 标签的过程。 60 | 61 | 未来,我们可能会增加多语言翻译功能,甚至引入基于拼音的编码。不过,目前没有立即实施这些功能的计划,因为现有功能已经能够满足大多数用例。 62 | 63 | --- 64 | 65 | 华为错失了一个重要的机会,没有充分利用“仓颉”这一名字的潜力。 66 | 67 | 汉语是一种语义简洁的语言,字符之间不需要分隔符,这就避免了类似驼峰命名法或下划线的问题。因此,它非常适合用来作为编程语言。 68 | 69 | 或许,未来会有人开发出一门完全功能化的中文编程语言(也许现在已经有这样的努力在进行中)。 70 | 71 | --- 72 | 73 | **gormmom** 旨在弥补这一空白,帮助开发者在使用 Go 语言的同时,能够用母语编写有意义且高效的代码。 74 | 75 | --- 76 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yyle88/gormmom 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/brianvoe/gofakeit/v7 v7.12.1 7 | github.com/emirpasic/gods/v2 v2.0.0-alpha 8 | github.com/google/uuid v1.6.0 9 | github.com/stretchr/testify v1.11.1 10 | github.com/yyle88/done v1.0.28 11 | github.com/yyle88/erero v1.0.24 12 | github.com/yyle88/formatgo v1.0.28 13 | github.com/yyle88/gormcngen v1.0.49 14 | github.com/yyle88/gormcnm v1.0.60 15 | github.com/yyle88/gormrepo v1.0.59 16 | github.com/yyle88/must v0.0.29 17 | github.com/yyle88/neatjson v0.0.13 18 | github.com/yyle88/osexistpath v0.0.18 19 | github.com/yyle88/printgo v1.0.6 20 | github.com/yyle88/rese v0.0.12 21 | github.com/yyle88/runpath v1.0.25 22 | github.com/yyle88/syntaxgo v0.0.54 23 | github.com/yyle88/tern v0.0.10 24 | github.com/yyle88/zaplog v0.0.27 25 | go.uber.org/zap v1.27.1 26 | gorm.io/driver/sqlite v1.6.0 27 | gorm.io/gorm v1.31.1 28 | ) 29 | 30 | require ( 31 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 32 | github.com/jinzhu/inflection v1.0.0 // indirect 33 | github.com/jinzhu/now v1.1.5 // indirect 34 | github.com/mattn/go-sqlite3 v1.14.32 // indirect 35 | github.com/pkg/errors v0.9.1 // indirect 36 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 37 | github.com/yyle88/mutexmap v1.0.15 // indirect 38 | github.com/yyle88/sortx v1.0.11 // indirect 39 | github.com/yyle88/sure v0.0.42 // indirect 40 | go.uber.org/multierr v1.11.0 // indirect 41 | golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect 42 | golang.org/x/mod v0.31.0 // indirect 43 | golang.org/x/sync v0.19.0 // indirect 44 | golang.org/x/text v0.32.0 // indirect 45 | golang.org/x/tools v0.40.0 // indirect 46 | gopkg.in/yaml.v3 v3.0.1 // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /internal/simplename/simple_column_name.go: -------------------------------------------------------------------------------- 1 | package simplename 2 | 3 | import ( 4 | "unicode" 5 | 6 | "github.com/yyle88/gormmom/internal/unicodehex" 7 | "github.com/yyle88/printgo" 8 | ) 9 | 10 | // 这是个简单的替换逻辑,能把特殊符号转换为相应的字母(这允许在自定义标签里包含特殊符号) 11 | var punctuationMap = map[string]string{ 12 | "(": "x", 13 | ")": "x", 14 | "(": "x", 15 | ")": "x", 16 | "%": "p", 17 | ".": "t", 18 | "|": "n", 19 | "、": "c", 20 | "/": "k", 21 | ":": "c", 22 | ":": "c", 23 | } 24 | 25 | func BuildColumnName(fieldName string) string { 26 | var pts = printgo.NewPTS() 27 | var preSimple = true 28 | for i, c := range fieldName { 29 | if replacement, ok := punctuationMap[string(c)]; ok { 30 | if i > 0 { //非首个时需要添加下划线以转换成蛇形的 31 | pts.WriteRune('_') 32 | } 33 | pts.WriteString(replacement) 34 | preSimple = false 35 | } else if c <= unicode.MaxASCII { 36 | nowSimple := true 37 | if unicode.IsUpper(c) { 38 | if i > 0 { //非首个时需要添加下划线以转换成蛇形的 39 | pts.WriteRune('_') 40 | } 41 | pts.WriteRune(unicode.ToLower(c)) 42 | } else if unicode.IsLower(c) { 43 | if !preSimple { 44 | pts.WriteRune('_') 45 | } 46 | pts.WriteRune(c) 47 | } else if unicode.IsDigit(c) { 48 | if !preSimple { 49 | pts.WriteRune('_') 50 | } 51 | pts.WriteRune(c) 52 | } else if c == '_' { 53 | pts.WriteRune(c) 54 | } else { 55 | if i > 0 { //非首个时需要添加下划线以转换成蛇形的 56 | pts.WriteRune('_') 57 | } 58 | pts.WriteString("x") 59 | nowSimple = false //这种情况不能当作普通字符,而是要当成特殊字符 60 | } 61 | preSimple = nowSimple //这表示前一次是个普通的简单字符 62 | } else { 63 | if i > 0 { //非首个时需要添加下划线以转换成蛇形的 64 | pts.WriteRune('_') 65 | } else { 66 | pts.WriteString("column") 67 | pts.WriteRune('_') 68 | } 69 | pts.WriteString(unicodehex.Uint32ToHex4Lowercase(c)) 70 | preSimple = false 71 | } 72 | } 73 | return pts.String() 74 | } 75 | -------------------------------------------------------------------------------- /internal/examples/tests/example11/internal/models/gormcnm.go: -------------------------------------------------------------------------------- 1 | // Code generated using gormcngen. DO NOT EDIT. 2 | // This file was auto generated via github.com/yyle88/gormcngen 3 | // Generated from: gormcnm_test.go:41 -> models.TestGen.func2 4 | // ========== GORMCNGEN:DO-NOT-EDIT-MARKER:END ========== 5 | 6 | package models 7 | 8 | import ( 9 | "time" 10 | 11 | "github.com/yyle88/gormcnm" 12 | ) 13 | 14 | func (T *Example) Columns() *ExampleColumns { 15 | return &ExampleColumns{ 16 | // Auto-generated: column names and types mapping. DO NOT EDIT. // 自动生成:列名和类型映射。请勿编辑。 17 | ID: gormcnm.Cnm(T.ID, "id"), 18 | V名称: gormcnm.Cnm(T.V名称, "v_0d54_f079"), 19 | V字段: gormcnm.Cnm(T.V字段, "v_575b_b56b"), 20 | V性别: gormcnm.Cnm(T.V性别, "v_2760_2b52"), 21 | V特殊: gormcnm.Cnm(T.V特殊, "v_7972_8a6b"), 22 | V年龄: gormcnm.Cnm(T.V年龄, "v_745e_849f"), 23 | Rank: gormcnm.Cnm(T.Rank, "rank"), 24 | V身高: gormcnm.Cnm(T.V身高, "v_ab8e_d89a"), 25 | V体重: gormcnm.Cnm(T.V体重, "v_534f_cd91"), 26 | CreatedAt: gormcnm.Cnm(T.CreatedAt, "created_at"), 27 | UpdatedAt: gormcnm.Cnm(T.UpdatedAt, "updated_at"), 28 | } 29 | } 30 | 31 | type ExampleColumns struct { 32 | // Auto-generated: embedding operation functions to make it simple to use. DO NOT EDIT. // 自动生成:嵌入操作函数便于使用。请勿编辑。 33 | gormcnm.ColumnOperationClass 34 | // Auto-generated: column names and types in database table. DO NOT EDIT. // 自动生成:数据库表的列名和类型。请勿编辑。 35 | ID gormcnm.ColumnName[int32] 36 | V名称 gormcnm.ColumnName[string] 37 | V字段 gormcnm.ColumnName[string] 38 | V性别 gormcnm.ColumnName[string] 39 | V特殊 gormcnm.ColumnName[string] 40 | V年龄 gormcnm.ColumnName[int] 41 | Rank gormcnm.ColumnName[int32] 42 | V身高 gormcnm.ColumnName[int32] 43 | V体重 gormcnm.ColumnName[int32] 44 | CreatedAt gormcnm.ColumnName[time.Time] 45 | UpdatedAt gormcnm.ColumnName[time.Time] 46 | } 47 | -------------------------------------------------------------------------------- /internal/examples/tests/example12/internal/models/gormcnm.go: -------------------------------------------------------------------------------- 1 | // Code generated using gormcngen. DO NOT EDIT. 2 | // This file was auto generated via github.com/yyle88/gormcngen 3 | // Generated from: gormcnm_test.go:42 -> models.TestGen.func2 4 | // ========== GORMCNGEN:DO-NOT-EDIT-MARKER:END ========== 5 | 6 | package models 7 | 8 | import ( 9 | "time" 10 | 11 | "github.com/yyle88/gormcnm" 12 | ) 13 | 14 | func (T *Example) Columns() *ExampleColumns { 15 | return &ExampleColumns{ 16 | // Auto-generated: column names and types mapping. DO NOT EDIT. // 自动生成:列名和类型映射。请勿编辑。 17 | ID: gormcnm.Cnm(T.ID, "id"), 18 | V名称: gormcnm.Cnm(T.V名称, "V_0D54_F079"), 19 | V字段: gormcnm.Cnm(T.V字段, "v_575b_b56b"), 20 | V性别: gormcnm.Cnm(T.V性别, "v_2760_2b52"), 21 | V特殊: gormcnm.Cnm(T.V特殊, "V_7972_8A6B"), 22 | V年龄: gormcnm.Cnm(T.V年龄, "v_745e_849f"), 23 | Rank: gormcnm.Cnm(T.Rank, "rank"), 24 | V身高: gormcnm.Cnm(T.V身高, "V_AB8E_D89A"), 25 | V体重: gormcnm.Cnm(T.V体重, "v_534f_cd91"), 26 | CreatedAt: gormcnm.Cnm(T.CreatedAt, "created_at"), 27 | UpdatedAt: gormcnm.Cnm(T.UpdatedAt, "updated_at"), 28 | } 29 | } 30 | 31 | type ExampleColumns struct { 32 | // Auto-generated: embedding operation functions to make it simple to use. DO NOT EDIT. // 自动生成:嵌入操作函数便于使用。请勿编辑。 33 | gormcnm.ColumnOperationClass 34 | // Auto-generated: column names and types in database table. DO NOT EDIT. // 自动生成:数据库表的列名和类型。请勿编辑。 35 | ID gormcnm.ColumnName[int32] 36 | V名称 gormcnm.ColumnName[string] 37 | V字段 gormcnm.ColumnName[string] 38 | V性别 gormcnm.ColumnName[string] 39 | V特殊 gormcnm.ColumnName[string] 40 | V年龄 gormcnm.ColumnName[int] 41 | Rank gormcnm.ColumnName[int32] 42 | V身高 gormcnm.ColumnName[int32] 43 | V体重 gormcnm.ColumnName[int32] 44 | CreatedAt gormcnm.ColumnName[time.Time] 45 | UpdatedAt gormcnm.ColumnName[time.Time] 46 | } 47 | -------------------------------------------------------------------------------- /validate_gorm_tags_test.go: -------------------------------------------------------------------------------- 1 | package gormmom_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yyle88/gormmom" 8 | "github.com/yyle88/runpath" 9 | ) 10 | 11 | type Product1 struct { 12 | ID uint `gorm:"primaryKey"` 13 | P名称 string `gorm:"column:name;type:varchar(100);not null;uniqueIndex"` 14 | P价格 float64 `gorm:"column:price;type:decimal(10,2);not null;index"` 15 | S库存 int `gorm:"type:int;default:0"` 16 | D描述 string `gorm:"column:description;type:text"` 17 | S状态 string `gorm:"column:status;type:varchar(20);default:'active';index"` 18 | } 19 | 20 | // TableName specifies the table name for Product1 model 21 | // TableName 指定 Product1 模型的表名 22 | func (*Product1) TableName() string { 23 | return "product1s" 24 | } 25 | 26 | func TestConfigs_ValidateGormTags(t *testing.T) { 27 | objects := []interface{}{ 28 | &Product1{}, 29 | } 30 | 31 | params := gormmom.ParseObjects(runpath.PARENT.Path(), objects) 32 | 33 | cfg := gormmom.NewConfigs(params, gormmom.NewOptions()) 34 | t.Log(cfg) 35 | 36 | require.Error(t, cfg.ValidateGormTags()) 37 | } 38 | 39 | type Product2 struct { 40 | ID uint `gorm:"primaryKey"` 41 | P名称 string `gorm:"column:name;type:varchar(100);not null;uniqueIndex"` 42 | P价格 float64 `gorm:"column:price;type:decimal(10,2);not null;index"` 43 | S库存 int `gorm:"column:stock;type:int;default:0;index"` 44 | D描述 string `gorm:"column:description;type:text"` 45 | S状态 string `gorm:"column:status;type:varchar(20);default:'active';index"` 46 | } 47 | 48 | // TableName specifies the table name for Product2 model 49 | // TableName 指定 Product2 模型的表名 50 | func (*Product2) TableName() string { 51 | return "product2s" 52 | } 53 | 54 | func TestConfigs_ValidateGormTags_2(t *testing.T) { 55 | objects := []interface{}{ 56 | &Product2{}, 57 | } 58 | 59 | params := gormmom.ParseObjects(runpath.PARENT.Path(), objects) 60 | 61 | cfg := gormmom.NewConfigs(params, gormmom.NewOptions()) 62 | t.Log(cfg) 63 | 64 | require.Error(t, cfg.ValidateGormTags()) 65 | } 66 | -------------------------------------------------------------------------------- /internal/simpleindexname/simple_index_name.go: -------------------------------------------------------------------------------- 1 | package simpleindexname 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/yyle88/gormmom/internal/utils" 7 | "github.com/yyle88/must" 8 | "github.com/yyle88/zaplog" 9 | "go.uber.org/zap" 10 | "gorm.io/gorm/schema" 11 | ) 12 | 13 | const ( 14 | IdxPatternTagName = "idx" 15 | UdxPatternTagName = "udx" 16 | ) 17 | 18 | type BuildIndexParam struct { 19 | TableName string 20 | FieldName string 21 | ColumnName string 22 | } 23 | 24 | type IndexNameResult struct { 25 | TagFieldName string 26 | NewIndexName string 27 | IdxUdxPrefix string 28 | } 29 | 30 | func BuildIndexName(schemaIndex *schema.Index, param *BuildIndexParam) *IndexNameResult { 31 | zaplog.LOG.Debug( 32 | "new_index_name", 33 | zap.String("table_name", param.TableName), 34 | zap.String("field_name", param.FieldName), 35 | zap.String("column_name", param.ColumnName), 36 | ) 37 | 38 | var enumCodeName string 39 | var tagFieldName string 40 | var newIndexName string 41 | switch schemaIndex.Class { 42 | case "": 43 | enumCodeName = IdxPatternTagName 44 | tagFieldName = "index" 45 | newIndexName = mergeIndexName("idx", param.TableName, param.ColumnName) 46 | case "UNIQUE": 47 | enumCodeName = UdxPatternTagName 48 | tagFieldName = "uniqueIndex" 49 | newIndexName = mergeIndexName("udx", param.TableName, param.ColumnName) 50 | default: 51 | newIndexName = mergeIndexName("idx", param.TableName, param.ColumnName) 52 | 53 | if newIndexName != schemaIndex.Name { //这种情况暂时没有遇到,依然是暂不处理 54 | zaplog.LOG.Warn("new_index_name", zap.String("new_index_name", newIndexName)) 55 | } 56 | 57 | if !utils.NewCommonRegexp(63).MatchString(schemaIndex.Name) { 58 | zaplog.LOG.Warn("idx_not_match", zap.String("old_index_name", schemaIndex.Name)) 59 | } 60 | 61 | return &IndexNameResult{ 62 | TagFieldName: "", //这种情况就不处理啦,打出告警日志让开发者手动解决 63 | NewIndexName: newIndexName, 64 | IdxUdxPrefix: "", 65 | } 66 | } 67 | must.Nice(newIndexName) 68 | 69 | return &IndexNameResult{ 70 | TagFieldName: tagFieldName, 71 | NewIndexName: newIndexName, 72 | IdxUdxPrefix: enumCodeName, 73 | } 74 | } 75 | 76 | func mergeIndexName(prefix string, tableName string, suffix string) string { 77 | return strings.ReplaceAll(strings.Join([]string{prefix, tableName, suffix}, "_"), ".", "_") 78 | } 79 | -------------------------------------------------------------------------------- /internal/examples/tests/example11/example11_test.go: -------------------------------------------------------------------------------- 1 | package example11 2 | 3 | import ( 4 | "fmt" 5 | "math/rand/v2" 6 | "testing" 7 | 8 | "github.com/brianvoe/gofakeit/v7" 9 | "github.com/google/uuid" 10 | "github.com/stretchr/testify/require" 11 | "github.com/yyle88/done" 12 | "github.com/yyle88/gormmom/internal/examples/tests/example11/internal/models" 13 | "github.com/yyle88/gormrepo" 14 | "github.com/yyle88/gormrepo/gormclass" 15 | "github.com/yyle88/neatjson/neatjsons" 16 | "github.com/yyle88/rese" 17 | "gorm.io/driver/sqlite" 18 | "gorm.io/gorm" 19 | "gorm.io/gorm/logger" 20 | ) 21 | 22 | var caseDB *gorm.DB 23 | 24 | func TestMain(m *testing.M) { 25 | dsn := fmt.Sprintf("file:db-%s?mode=memory&cache=shared", uuid.New().String()) 26 | db := rese.P1(gorm.Open(sqlite.Open(dsn), &gorm.Config{ 27 | Logger: logger.Default.LogMode(logger.Info), 28 | })) 29 | defer rese.F0(rese.P1(db.DB()).Close) 30 | 31 | done.Done(db.AutoMigrate(&models.Example{})) 32 | 33 | caseDB = db 34 | m.Run() 35 | } 36 | 37 | func TestUsage(t *testing.T) { 38 | examples := make([]*models.Example, 0, 10) 39 | for i := 0; i < 10; i++ { 40 | one := newFakeExample(t) 41 | t.Log(neatjsons.S(one)) 42 | require.NoError(t, caseDB.Create(one).Error) 43 | examples = append(examples, one) 44 | } 45 | 46 | repo := gormrepo.NewRepo(gormclass.Use(&models.Example{})) 47 | 48 | t.Run("select-first", func(t *testing.T) { 49 | name := examples[rand.IntN(len(examples))].V名称 50 | 51 | example, err := repo.Repo(caseDB).First(func(db *gorm.DB, cls *models.ExampleColumns) *gorm.DB { 52 | return db.Where(cls.V名称.Eq(name)) 53 | }) 54 | require.NoError(t, err) 55 | t.Log(neatjsons.S(example)) 56 | }) 57 | t.Run("select-where-in", func(t *testing.T) { 58 | var names = make([]string, 0, 5) 59 | for idx, one := range examples { 60 | if idx%2 == 0 { 61 | continue 62 | } 63 | names = append(names, one.V名称) 64 | } 65 | 66 | results, err := repo.Repo(caseDB).Find(func(db *gorm.DB, cls *models.ExampleColumns) *gorm.DB { 67 | return db.Where(cls.V名称.In(names)) 68 | }) 69 | require.NoError(t, err) 70 | t.Log(neatjsons.S(results)) 71 | }) 72 | } 73 | 74 | func newFakeExample(t *testing.T) *models.Example { 75 | a := &models.Example{} 76 | require.NoError(t, gofakeit.Struct(a)) 77 | a.ID = 0 // 设置为0以便于使用 gorm 创建数据 78 | return a 79 | } 80 | -------------------------------------------------------------------------------- /internal/examples/tests/example12/example12_test.go: -------------------------------------------------------------------------------- 1 | package example12 2 | 3 | import ( 4 | "fmt" 5 | "math/rand/v2" 6 | "testing" 7 | 8 | "github.com/brianvoe/gofakeit/v7" 9 | "github.com/google/uuid" 10 | "github.com/stretchr/testify/require" 11 | "github.com/yyle88/done" 12 | "github.com/yyle88/gormmom/internal/examples/tests/example12/internal/models" 13 | "github.com/yyle88/gormrepo" 14 | "github.com/yyle88/gormrepo/gormclass" 15 | "github.com/yyle88/neatjson/neatjsons" 16 | "github.com/yyle88/rese" 17 | "gorm.io/driver/sqlite" 18 | "gorm.io/gorm" 19 | "gorm.io/gorm/logger" 20 | ) 21 | 22 | var caseDB *gorm.DB 23 | 24 | func TestMain(m *testing.M) { 25 | dsn := fmt.Sprintf("file:db-%s?mode=memory&cache=shared", uuid.New().String()) 26 | db := rese.P1(gorm.Open(sqlite.Open(dsn), &gorm.Config{ 27 | Logger: logger.Default.LogMode(logger.Info), 28 | })) 29 | defer rese.F0(rese.P1(db.DB()).Close) 30 | 31 | done.Done(db.AutoMigrate(&models.Example{})) 32 | 33 | caseDB = db 34 | m.Run() 35 | } 36 | 37 | func TestUsage(t *testing.T) { 38 | examples := make([]*models.Example, 0, 10) 39 | for i := 0; i < 10; i++ { 40 | one := newFakeExample(t) 41 | t.Log(neatjsons.S(one)) 42 | require.NoError(t, caseDB.Create(one).Error) 43 | examples = append(examples, one) 44 | } 45 | 46 | repo := gormrepo.NewRepo(gormclass.Use(&models.Example{})) 47 | 48 | t.Run("select-first", func(t *testing.T) { 49 | name := examples[rand.IntN(len(examples))].V名称 50 | 51 | example, err := repo.Repo(caseDB).First(func(db *gorm.DB, cls *models.ExampleColumns) *gorm.DB { 52 | return db.Where(cls.V名称.Eq(name)) 53 | }) 54 | require.NoError(t, err) 55 | t.Log(neatjsons.S(example)) 56 | }) 57 | t.Run("select-where-in", func(t *testing.T) { 58 | var names = make([]string, 0, 5) 59 | for idx, one := range examples { 60 | if idx%2 == 0 { 61 | continue 62 | } 63 | names = append(names, one.V名称) 64 | } 65 | 66 | results, err := repo.Repo(caseDB).Find(func(db *gorm.DB, cls *models.ExampleColumns) *gorm.DB { 67 | return db.Where(cls.V名称.In(names)) 68 | }) 69 | require.NoError(t, err) 70 | t.Log(neatjsons.S(results)) 71 | }) 72 | } 73 | 74 | func newFakeExample(t *testing.T) *models.Example { 75 | a := &models.Example{} 76 | require.NoError(t, gofakeit.Struct(a)) 77 | a.ID = 0 // 设置为0以便于使用 gorm 创建数据 78 | return a 79 | } 80 | -------------------------------------------------------------------------------- /internal/examples/tests/example13/example13_test.go: -------------------------------------------------------------------------------- 1 | package example13 2 | 3 | import ( 4 | "fmt" 5 | "math/rand/v2" 6 | "testing" 7 | 8 | "github.com/brianvoe/gofakeit/v7" 9 | "github.com/google/uuid" 10 | "github.com/stretchr/testify/require" 11 | "github.com/yyle88/done" 12 | "github.com/yyle88/gormmom/internal/examples/tests/example13/internal/models" 13 | "github.com/yyle88/gormrepo" 14 | "github.com/yyle88/gormrepo/gormclass" 15 | "github.com/yyle88/neatjson/neatjsons" 16 | "github.com/yyle88/rese" 17 | "gorm.io/driver/sqlite" 18 | "gorm.io/gorm" 19 | "gorm.io/gorm/logger" 20 | ) 21 | 22 | var caseDB *gorm.DB 23 | 24 | func TestMain(m *testing.M) { 25 | dsn := fmt.Sprintf("file:db-%s?mode=memory&cache=shared", uuid.New().String()) 26 | db := rese.P1(gorm.Open(sqlite.Open(dsn), &gorm.Config{ 27 | Logger: logger.Default.LogMode(logger.Info), 28 | })) 29 | defer rese.F0(rese.P1(db.DB()).Close) 30 | 31 | done.Done(db.AutoMigrate(&models.Example{})) 32 | 33 | caseDB = db 34 | m.Run() 35 | } 36 | 37 | func TestExample(t *testing.T) { 38 | examples := make([]*models.Example, 0, 10) 39 | for i := 0; i < 10; i++ { 40 | one := newFakeExample(t) 41 | t.Log(neatjsons.S(one)) 42 | require.NoError(t, caseDB.Create(one).Error) 43 | examples = append(examples, one) 44 | } 45 | 46 | repo := gormrepo.NewRepo(gormclass.Use(&models.Example{})) 47 | 48 | t.Run("select-first", func(t *testing.T) { 49 | name := examples[rand.IntN(len(examples))].V名称 50 | 51 | example, err := repo.Repo(caseDB).First(func(db *gorm.DB, cls *models.ExampleColumns) *gorm.DB { 52 | return db.Where(cls.V名称.Eq(name)) 53 | }) 54 | require.NoError(t, err) 55 | t.Log(neatjsons.S(example)) 56 | }) 57 | t.Run("select-where-in", func(t *testing.T) { 58 | var names = make([]string, 0, 5) 59 | for idx, one := range examples { 60 | if idx%2 == 0 { 61 | continue 62 | } 63 | names = append(names, one.V名称) 64 | } 65 | 66 | results, err := repo.Repo(caseDB).Find(func(db *gorm.DB, cls *models.ExampleColumns) *gorm.DB { 67 | return db.Where(cls.V名称.In(names)) 68 | }) 69 | require.NoError(t, err) 70 | t.Log(neatjsons.S(results)) 71 | }) 72 | } 73 | 74 | func newFakeExample(t *testing.T) *models.Example { 75 | a := &models.Example{} 76 | require.NoError(t, gofakeit.Struct(a)) 77 | a.ID = 0 // 设置为0以便于使用 gorm 创建数据 78 | return a 79 | } 80 | -------------------------------------------------------------------------------- /gen_test.go: -------------------------------------------------------------------------------- 1 | package gormmom 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | "github.com/yyle88/gormmom/gormmomname" 9 | "github.com/yyle88/gormmom/internal/utils" 10 | "github.com/yyle88/neatjson/neatjsons" 11 | "github.com/yyle88/rese/resb" 12 | "github.com/yyle88/runpath" 13 | ) 14 | 15 | func TestMain(m *testing.M) { 16 | m.Run() 17 | } 18 | 19 | type Example struct { 20 | ID int32 `gorm:"column:id; primaryKey;" json:"id"` 21 | V名称 string `gorm:"type:text" mom:"mcp:s63"` 22 | V字段 string `gorm:"column: some_field" mom:"mcp:S63;"` 23 | V性别 string 24 | V特殊 string `gorm:"column:特殊;type:int32" mom:"mcp:S63;"` 25 | V年龄 int `json:"age"` //理论上不要直接给model添加json标签,因为那是view层的逻辑,但实际上假如非这样做也能处理 26 | Rank int32 `` //看看这种情况是啥效果 27 | V身高 int32 `` //看看这种情况是啥效果 28 | V体重 int32 `` //看看这种情况是啥效果 29 | // v啥呀 string //这个是小写字母开头的所以不是导出字段 30 | CreatedAt time.Time `gorm:"autoCreateTime"` 31 | UpdatedAt time.Time `gorm:"autoUpdateTime"` 32 | } 33 | 34 | func TestPreview(t *testing.T) { 35 | cfg := NewConfig(ParseStruct[Example](runpath.CurrentPath()), NewOptions()) 36 | t.Log(cfg) 37 | 38 | newCode := cfg.Preview() 39 | t.Log(newCode.SourcePath) 40 | t.Log(newCode.ChangedLineCount) 41 | 42 | require.Equal(t, 7, newCode.ChangedLineCount) 43 | 44 | results := utils.ParseTagsTrimBackticks(newCode.OutputCode, &Example{}) 45 | t.Log(neatjsons.S(results)) 46 | require.Equal(t, `gorm:"column:v_0d54_f079;type:text" mom:"mcp:s63;"`, resb.C1(results.Get("V名称"))) 47 | require.Equal(t, `gorm:"column:V_575B_B56B;" mom:"mcp:S63;"`, resb.C1(results.Get("V字段"))) 48 | require.Equal(t, `gorm:"column:v_2760_2b52;" mom:"mcp:s63;"`, resb.C1(results.Get("V性别"))) 49 | require.Equal(t, `gorm:"column:V_7972_8A6B;type:int32" mom:"mcp:S63;"`, resb.C1(results.Get("V特殊"))) 50 | } 51 | 52 | func TestPreview_S63(t *testing.T) { 53 | cfg := NewConfig(ParseStruct[Example](runpath.CurrentPath()), NewOptions().WithDefaultColumnPattern(gormmomname.NewUppercase63pattern())) 54 | t.Log(cfg) 55 | 56 | newCode := cfg.Preview() 57 | t.Log(newCode.SourcePath) 58 | t.Log(newCode.ChangedLineCount) 59 | 60 | require.Equal(t, 7, newCode.ChangedLineCount) 61 | 62 | results := utils.ParseTagsTrimBackticks(newCode.OutputCode, &Example{}) 63 | t.Log(neatjsons.S(results)) 64 | require.Equal(t, `gorm:"column:V_575B_B56B;" mom:"mcp:S63;"`, resb.C1(results.Get("V字段"))) 65 | } 66 | 67 | type Example5 struct { 68 | V嘿哈 string `gorm:"column:;type:text"` 69 | } 70 | 71 | func TestPreview_Example5(t *testing.T) { 72 | cfg := NewConfig(ParseStruct[Example5](runpath.CurrentPath()), NewOptions().WithDefaultColumnPattern(gormmomname.NewLowercase30pattern())) 73 | t.Log(cfg) 74 | 75 | newCode := cfg.Preview() 76 | t.Log(newCode.SourcePath) 77 | t.Log(newCode.ChangedLineCount) 78 | 79 | require.Equal(t, 1, newCode.ChangedLineCount) 80 | 81 | results := utils.ParseTagsTrimBackticks(newCode.OutputCode, &Example5{}) 82 | t.Log(neatjsons.S(results)) 83 | require.Equal(t, `gorm:"column:v_3f56_c854;type:text" mom:"mcp:s30;"`, resb.C1(results.Get("V嘿哈"))) 84 | } 85 | -------------------------------------------------------------------------------- /gormmomname/pattern.go: -------------------------------------------------------------------------------- 1 | // Package gormmomname: Native language column name generation strategies for database compatibility 2 | // Provides pattern-based column name generation from Unicode field names to database-safe identifiers 3 | // Supports multiple naming strategies including lowercase and uppercase patterns with length constraints 4 | // Ensures cross-database compatibility by implementing intersection of various database naming rules 5 | // 6 | // gormmomname: 数据库兼容的原生语言列名生成策略 7 | // 提供基于模式的列名生成,将 Unicode 字段名转换为数据库安全标识符 8 | // 支持多种命名策略,包括带长度约束的小写和大写模式 9 | // 通过实现各种数据库命名规则的交集来确保跨数据库兼容性 10 | package gormmomname 11 | 12 | import ( 13 | "github.com/yyle88/must" 14 | ) 15 | 16 | //type CaseMode string 17 | // 18 | //const ( 19 | // Lowercase CaseMode = "LOWERCASE" 20 | // Uppercase CaseMode = "UPPERCASE" 21 | //) 22 | 23 | // PatternEnum represents the column name validation pattern type 24 | // Custom enum type defining validation strategies for field name processing 25 | // Different databases have different column naming rules, so patterns implement intersections 26 | // Ensures compatibility across multiple database systems with unified naming standards 27 | // 28 | // PatternEnum 代表列名验证模式类型 29 | // 自定义枚举类型,定义字段名处理的验证策略 30 | // 由于不同数据库有不同的列命名规则,模式实现了规则交集 31 | // 通过统一命名标准确保多个数据库系统的兼容性 32 | type PatternEnum string 33 | 34 | // Pattern defines the interface for column name generation and validation 35 | // Provides pattern identification, name validation, and field-to-column conversion 36 | // Ensures generated column names meet specific database requirements and constraints 37 | // 38 | // Pattern 定义列名生成和验证的接口 39 | // 提供模式识别、名称验证和字段到列的转换 40 | // 确保生成的列名满足特定的数据库要求和约束 41 | type Pattern interface { 42 | GetPatternEnum() PatternEnum // Get pattern type identifier // 获取模式类型标识符 43 | CheckColumnName(columnName string) bool // Validate column name format // 验证列名格式 44 | BuildColumnName(fieldName string) string // Generate column name from field name // 从字段名生成列名 45 | } 46 | 47 | // Strategies manages multiple column naming patterns with smart defaults 48 | // Contains default pattern selection and strategy mapping for flexible naming 49 | // Supports pattern registration and selection based on requirements 50 | // 51 | // Strategies 管理多种列命名模式,带有智能默认值 52 | // 包含默认模式选择和策略映射,支持灵活命名 53 | // 支持基于需求的模式注册和选择 54 | type Strategies struct { 55 | defaultPattern Pattern // Default naming pattern // 默认命名模式 56 | nameStrategies map[PatternEnum]Pattern // Pattern strategy mapping // 模式策略映射 57 | } 58 | 59 | func NewStrategies() *Strategies { 60 | nameStrategies := make(map[PatternEnum]Pattern) 61 | for _, pattern := range []Pattern{ 62 | NewLowercase30pattern(), 63 | NewUppercase30pattern(), 64 | NewLowercase63pattern(), 65 | NewUppercase63pattern(), 66 | } { 67 | nameStrategies[pattern.GetPatternEnum()] = pattern 68 | } 69 | return &Strategies{ 70 | defaultPattern: NewLowercase63pattern(), 71 | nameStrategies: nameStrategies, 72 | } 73 | } 74 | 75 | func (s *Strategies) GetDefault() Pattern { 76 | return s.defaultPattern 77 | } 78 | 79 | func (s *Strategies) SetDefault(pattern Pattern) { 80 | s.defaultPattern = pattern 81 | s.SetPattern(pattern) // 同时也要把它设置到 map 里面 82 | } 83 | 84 | func (s *Strategies) GetPattern(patternEnum PatternEnum) Pattern { 85 | namePattern, ok := s.nameStrategies[patternEnum] 86 | must.True(ok) 87 | return namePattern 88 | } 89 | 90 | func (s *Strategies) SetPattern(pattern Pattern) { 91 | s.nameStrategies[pattern.GetPatternEnum()] = pattern 92 | } 93 | -------------------------------------------------------------------------------- /internal/docs/README_OLD_DOC.en.md: -------------------------------------------------------------------------------- 1 | # gormmom 2 | **Empowering Native Language Programming** 3 | 4 | --- 5 | 6 | Huawei recently introduced the Cangjie programming language. However, it still lacks support for programming directly in Chinese, despite the growing demand for such a feature. 7 | 8 | --- 9 | 10 | In Go programming, it's actually possible to write code using your native language. 11 | 12 | For example: 13 | ```go 14 | type A学生信息 struct { 15 | V姓名 string 16 | V性别 bool 17 | V年龄 int 18 | } 19 | 20 | func (a *A客户信息) Get姓名() string { return a.V姓名 } 21 | ``` 22 | 23 | This approach works seamlessly. 24 | 25 | --- 26 | 27 | However, when working with GORM, challenges arise because column names need to be explicitly defined using GORM tags. 28 | 29 | This tool offers a simple solution to help you easily generate GORM tags, making it possible to continue programming in your native language while working with Go. 30 | 31 | --- 32 | 33 | Due to my limited proficiency in English and the difficulty in finding straightforward, descriptive words, I named this package **"gormmom."** 34 | 35 | While mastering English is essential for programmers, I believe that even individuals with advanced English skills—whether they’ve passed CET-4/6 (China's English proficiency tests) or have over ten years of programming experience—can still struggle to express themselves fluently in code. In such cases, it may be time to consider an alternative approach. 36 | 37 | --- 38 | 39 | For example, during this project, I encountered difficulty when trying to extract content from tags. The recommended term was `extract`, but I first just thought of alternatives such as `get`, `parse`, or `obtain`. 40 | 41 | This experience reinforced my belief in the necessity of programming in your native language. Without it, code ends up as a patchwork of imprecise terms, which can lead to confusion and inefficiency. 42 | 43 | --- 44 | 45 | Another issue I frequently face is that in English, opposite meanings are often expressed with words of varying lengths. For instance: 46 | `up/down`, `left/right`, `open/close`, `start/stop`, `public/private`. 47 | 48 | Programmers are often forced to choose from a limited set of words that just loosely fit the context, such as: 49 | - `get/set` 50 | - `start/close` 51 | - `create/update/select/delete`. 52 | 53 | For non-native English speakers, this inconsistency can create confusion and hinder clarity. 54 | 55 | --- 56 | 57 | Now, this tool does not include a translation feature and cannot auto convert native language code into English. 58 | 59 | It focuses on simplifying the process of generating GORM tags. 60 | 61 | In the future, we may add translation capabilities for multiple languages or even introduce pinyin-based encoding. However, there are no immediate plans for these features, as the current functionality is sufficient for most use cases. 62 | 63 | --- 64 | 65 | Huawei missed a significant opportunity by not fully leveraging the potential of the "Cangjie" name. 66 | 67 | Chinese is a language with concise semantics, where characters don’t require separators, thus avoiding issues like camelCase or underscores. This makes it particularly well-suited for a programming language. 68 | 69 | Perhaps, in the future, someone will develop a fully functional Chinese programming language (and perhaps such efforts are already underway). 70 | 71 | --- 72 | 73 | **gormmom** helps bridge the gap, enabling developers to write meaningful, efficient code in their native language while utilizing GORM in Go. 74 | 75 | --- 76 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: create-release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main # 监听 main 分支的 push 操作(编译和测试/代码检查) 7 | tags: 8 | - 'v*' # 监听以 'v' 开头的标签的 push 操作(发布 Release) 9 | 10 | jobs: 11 | lint: 12 | name: lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v6 16 | - uses: actions/setup-go@v6 17 | with: 18 | go-version: stable 19 | cache: true 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@v9 22 | with: 23 | version: latest 24 | args: --timeout=5m 25 | 26 | test: 27 | runs-on: ubuntu-latest 28 | strategy: 29 | matrix: 30 | go: [ "1.24.x", "1.25.x", "stable" ] 31 | steps: 32 | - uses: actions/checkout@v6 33 | 34 | - uses: actions/setup-go@v6 35 | with: 36 | go-version: ${{ matrix.go }} 37 | cache: true 38 | 39 | - name: Run govulncheck 40 | uses: golang/govulncheck-action@v1 41 | with: 42 | go-version-input: ${{ matrix.go }} 43 | go-package: ./... 44 | continue-on-error: true # 报错时允许工作流继续执行,因为项目依赖的底层包也会有错,很难做到百分百没问题,只打印检测结果就行 45 | 46 | - name: Run test 47 | run: make test COVERAGE_DIR=/tmp/coverage 48 | 49 | - name: Upload test results 50 | uses: actions/upload-artifact@v4 51 | if: always() 52 | with: 53 | name: test-results-${{ matrix.go }} 54 | path: /tmp/coverage/ 55 | retention-days: 30 56 | 57 | - name: Send goveralls coverage 58 | uses: shogo82148/actions-goveralls@v1 59 | with: 60 | path-to-profile: /tmp/coverage/combined.txt 61 | flag-name: Go-${{ matrix.go }} 62 | parallel: true 63 | if: ${{ github.event.repository.fork == false }} # 仅在非 fork 时上传覆盖率 64 | 65 | check-coverage: 66 | name: Check coverage 67 | needs: [ test ] 68 | runs-on: ubuntu-latest 69 | steps: 70 | - uses: shogo82148/actions-goveralls@v1 71 | with: 72 | parallel-finished: true 73 | if: ${{ github.event.repository.fork == false }} # 仅在非 fork 时检查覆盖率 74 | 75 | # 代码质量分析 76 | code-analysis: 77 | name: CodeQL Analysis 78 | runs-on: ubuntu-latest 79 | permissions: 80 | actions: read 81 | contents: read 82 | security-events: write 83 | steps: 84 | - name: Checkout repository 85 | uses: actions/checkout@v6 86 | 87 | - name: Initialize CodeQL 88 | uses: github/codeql-action/init@v4 89 | with: 90 | languages: go 91 | 92 | - name: Auto Build 93 | uses: github/codeql-action/autobuild@v4 94 | 95 | - name: Perform CodeQL Analysis 96 | uses: github/codeql-action/analyze@v4 97 | 98 | # 发布 Release 99 | release: 100 | name: Release a new version 101 | needs: [ lint, test, check-coverage, code-analysis ] 102 | runs-on: ubuntu-latest 103 | # 仅在推送标签时执行 - && - 仅在非 fork 时执行发布 104 | if: ${{ github.event.repository.fork == false && success() && startsWith(github.ref, 'refs/tags/v') }} 105 | steps: 106 | # 1. 检出代码 107 | - name: Checkout code 108 | uses: actions/checkout@v6 109 | with: 110 | fetch-depth: 0 # 获取完整历史用于生成更好的 release notes 111 | 112 | # 2. 创建 Release 和上传源码包 113 | - name: Create Release 114 | uses: softprops/action-gh-release@v2 115 | with: 116 | generate_release_notes: true 117 | env: 118 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 119 | -------------------------------------------------------------------------------- /gen_index_test.go: -------------------------------------------------------------------------------- 1 | package gormmom 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yyle88/gormmom/internal/tests" 8 | "github.com/yyle88/gormmom/internal/utils" 9 | "github.com/yyle88/neatjson/neatjsons" 10 | "github.com/yyle88/rese/resb" 11 | "github.com/yyle88/runpath" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | /* 16 | 在 Gorm 里 unique 和 uniqueIndex 在 GORM 中确实有区别: 17 | 18 | unique: 19 | 这个标签表示为字段添加一个唯一性约束,但它不一定会为这个约束生成一个命名的索引。数据库会确保字段值唯一,但索引的命名和管理通常由数据库引擎自动处理,因此不一定会出现一个独立命名的唯一索引。 20 | 21 | uniqueIndex: 22 | 这个标签不仅表示字段的唯一性,还明确要求 GORM 生成一个带有名称的唯一索引。通常会按照 idx__ 的格式生成索引名称(具体格式可能因数据库而异)。 23 | */ 24 | type Example3 struct { 25 | V身份证号 string `gorm:"column:person_num;primaryKey"` 26 | V学校编号 string `gorm:"column:school_num;uniqueIndex:udx_student_unique"` 27 | V班级编号 string `gorm:"column:class_num;uniqueIndex:udx_student_unique"` 28 | V班内排名 string `gorm:"column:student_num;uniqueIndex:udx_student_unique"` 29 | V姓名 string `gorm:"column:name;index" mom:"mcp:S63"` //普通索引,默认名称不正确(现在的默认名称带中文) 30 | V年龄 int `gorm:"column:age;unique" mom:"mcp:S63"` //唯一约束,而非唯一索引,默认名称正确,使用的还是 uni_param_example3_age 这个约束名 31 | V性别 bool `gorm:"column:sex;uniqueIndex" mom:"mcp:S63"` //唯一索引,带名称,默认名称不正确(现在的默认名称带中文) 32 | } 33 | 34 | func TestDryRunMigrate(t *testing.T) { 35 | tests.NewDBRun(t, func(db *gorm.DB) { 36 | require.NoError(t, db.Session(&gorm.Session{ 37 | DryRun: true, 38 | }).AutoMigrate(&Example3{})) 39 | }) 40 | } 41 | 42 | func TestConfig_GenCode_GenIndexes(t *testing.T) { 43 | cfg := NewConfig(ParseStruct[Example3](runpath.CurrentPath()), NewOptions()) 44 | t.Log(cfg) 45 | 46 | newCode := cfg.Preview() 47 | t.Log(newCode.SourcePath) 48 | t.Log(newCode.ChangedLineCount) 49 | 50 | require.Equal(t, 3, newCode.ChangedLineCount) 51 | 52 | results := utils.ParseTagsTrimBackticks(newCode.OutputCode, &Example3{}) 53 | t.Log(neatjsons.S(results)) 54 | require.Equal(t, `gorm:"column:V_D359_0D54;index:idx_example3_v_d359_0d54" mom:"mcp:S63;idx:cnm;"`, resb.C1(results.Get("V姓名"))) 55 | require.Equal(t, `gorm:"column:V_745E_849F;unique" mom:"mcp:S63;"`, resb.C1(results.Get("V年龄"))) 56 | require.Equal(t, `gorm:"column:V_2760_2B52;uniqueIndex:udx_example3_v_2760_2b52" mom:"mcp:S63;udx:cnm;"`, resb.C1(results.Get("V性别"))) 57 | } 58 | 59 | type Example4 struct { 60 | V证号 string `gorm:"primaryKey"` 61 | V姓名 string `gorm:"index"` 62 | V年龄 int `gorm:"unique"` 63 | V性别 bool `gorm:"column:sex;uniqueIndex" mom:"mcp:S63"` 64 | } 65 | 66 | func (*Example4) TableName() string { 67 | return "example4" 68 | } 69 | 70 | func TestConfig_GenCode_GenIndexes_Example4(t *testing.T) { 71 | cfg := NewConfig(ParseStruct[Example4](runpath.CurrentPath()), NewOptions()) 72 | t.Log(cfg) 73 | 74 | newCode := cfg.Preview() 75 | t.Log(newCode.SourcePath) 76 | t.Log(newCode.ChangedLineCount) 77 | 78 | require.Equal(t, 4, newCode.ChangedLineCount) 79 | 80 | results := utils.ParseTagsTrimBackticks(newCode.OutputCode, &Example4{}) 81 | t.Log(neatjsons.S(results)) 82 | require.Equal(t, `gorm:"column:v_c18b_f753;primaryKey" mom:"mcp:s63;"`, resb.C1(results.Get("V证号"))) 83 | require.Equal(t, `gorm:"column:v_d359_0d54;index:idx_example4_v_d359_0d54" mom:"mcp:s63;idx:cnm;"`, resb.C1(results.Get("V姓名"))) 84 | require.Equal(t, `gorm:"column:v_745e_849f;unique" mom:"mcp:s63;"`, resb.C1(results.Get("V年龄"))) 85 | require.Equal(t, `gorm:"column:V_2760_2B52;uniqueIndex:udx_example4_v_2760_2b52" mom:"mcp:S63;udx:cnm;"`, resb.C1(results.Get("V性别"))) 86 | } 87 | 88 | type Example6 struct { 89 | V账号 string `gorm:"primaryKey"` 90 | V身份证号 string `gorm:"column:person_num;uniqueIndex" mom:"udx:cnm;"` 91 | V学校编号 string `gorm:"column:school_num;index" mom:"idx:cnm;"` 92 | } 93 | 94 | func TestConfig_GenCode_GenIndexes_Example6(t *testing.T) { 95 | cfg := NewConfig(ParseStruct[Example6](runpath.CurrentPath()), NewOptions()) 96 | t.Log(cfg) 97 | 98 | newCode := cfg.Preview() 99 | t.Log(newCode.SourcePath) 100 | t.Log(newCode.ChangedLineCount) 101 | 102 | require.Equal(t, 3, newCode.ChangedLineCount) 103 | 104 | results := utils.ParseTagsTrimBackticks(newCode.OutputCode, &Example6{}) 105 | t.Log(neatjsons.S(results)) 106 | require.Equal(t, `gorm:"column:v_268d_f753;primaryKey" mom:"mcp:s63;"`, resb.C1(results.Get("V账号"))) 107 | require.Equal(t, `gorm:"column:person_num;uniqueIndex:udx_example6_person_num" mom:"udx:cnm;"`, resb.C1(results.Get("V身份证号"))) 108 | require.Equal(t, `gorm:"column:school_num;index:idx_example6_school_num" mom:"idx:cnm;"`, resb.C1(results.Get("V学校编号"))) 109 | } 110 | -------------------------------------------------------------------------------- /internal/examples/tests/example13/internal/models/gormcnm.go: -------------------------------------------------------------------------------- 1 | // Code generated using gormcngen. DO NOT EDIT. 2 | // This file was auto generated via github.com/yyle88/gormcngen 3 | // Generated from: gormcnm_test.go:43 -> models.TestGen.func2 4 | // ========== GORMCNGEN:DO-NOT-EDIT-MARKER:END ========== 5 | 6 | package models 7 | 8 | import ( 9 | "time" 10 | 11 | "github.com/yyle88/gormcnm" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | func (T *Example) Columns() *ExampleColumns { 16 | return &ExampleColumns{ 17 | // Auto-generated: column names and types mapping. DO NOT EDIT. // 自动生成:列名和类型映射。请勿编辑。 18 | ID: gormcnm.Cnm(T.ID, "id"), 19 | V名称: gormcnm.Cnm(T.V名称, "v_0d54_f079"), 20 | V字段: gormcnm.Cnm(T.V字段, "v_575b_b56b"), 21 | V性别: gormcnm.Cnm(T.V性别, "v_2760_2b52"), 22 | V特殊: gormcnm.Cnm(T.V特殊, "v_7972_8a6b"), 23 | V年龄: gormcnm.Cnm(T.V年龄, "v_745e_849f"), 24 | Rank: gormcnm.Cnm(T.Rank, "rank"), 25 | V身高: gormcnm.Cnm(T.V身高, "v_ab8e_d89a"), 26 | V体重: gormcnm.Cnm(T.V体重, "v_534f_cd91"), 27 | CreatedAt: gormcnm.Cnm(T.CreatedAt, "created_at"), 28 | UpdatedAt: gormcnm.Cnm(T.UpdatedAt, "updated_at"), 29 | } 30 | } 31 | 32 | type ExampleColumns struct { 33 | // Auto-generated: embedding operation functions to make it simple to use. DO NOT EDIT. // 自动生成:嵌入操作函数便于使用。请勿编辑。 34 | gormcnm.ColumnOperationClass 35 | // Auto-generated: column names and types in database table. DO NOT EDIT. // 自动生成:数据库表的列名和类型。请勿编辑。 36 | ID gormcnm.ColumnName[int32] 37 | V名称 gormcnm.ColumnName[string] 38 | V字段 gormcnm.ColumnName[string] 39 | V性别 gormcnm.ColumnName[string] 40 | V特殊 gormcnm.ColumnName[string] 41 | V年龄 gormcnm.ColumnName[int] 42 | Rank gormcnm.ColumnName[int32] 43 | V身高 gormcnm.ColumnName[int32] 44 | V体重 gormcnm.ColumnName[int32] 45 | CreatedAt gormcnm.ColumnName[time.Time] 46 | UpdatedAt gormcnm.ColumnName[time.Time] 47 | } 48 | 49 | func (T *Example2) Columns() *Example2Columns { 50 | return &Example2Columns{ 51 | // Auto-generated: column names and types mapping. DO NOT EDIT. // 自动生成:列名和类型映射。请勿编辑。 52 | ID: gormcnm.Cnm(T.ID, "id"), 53 | V名称: gormcnm.Cnm(T.V名称, "V_0D54_F079"), 54 | V字段: gormcnm.Cnm(T.V字段, "v_575b_b56b"), 55 | V性别: gormcnm.Cnm(T.V性别, "v_2760_2b52"), 56 | V特殊: gormcnm.Cnm(T.V特殊, "V_7972_8A6B"), 57 | V年龄: gormcnm.Cnm(T.V年龄, "v_745e_849f"), 58 | Rank: gormcnm.Cnm(T.Rank, "rank"), 59 | V身高: gormcnm.Cnm(T.V身高, "V_AB8E_D89A"), 60 | V体重: gormcnm.Cnm(T.V体重, "v_534f_cd91"), 61 | CreatedAt: gormcnm.Cnm(T.CreatedAt, "created_at"), 62 | UpdatedAt: gormcnm.Cnm(T.UpdatedAt, "updated_at"), 63 | } 64 | } 65 | 66 | type Example2Columns struct { 67 | // Auto-generated: embedding operation functions to make it simple to use. DO NOT EDIT. // 自动生成:嵌入操作函数便于使用。请勿编辑。 68 | gormcnm.ColumnOperationClass 69 | // Auto-generated: column names and types in database table. DO NOT EDIT. // 自动生成:数据库表的列名和类型。请勿编辑。 70 | ID gormcnm.ColumnName[int32] 71 | V名称 gormcnm.ColumnName[string] 72 | V字段 gormcnm.ColumnName[string] 73 | V性别 gormcnm.ColumnName[string] 74 | V特殊 gormcnm.ColumnName[string] 75 | V年龄 gormcnm.ColumnName[int] 76 | Rank gormcnm.ColumnName[int32] 77 | V身高 gormcnm.ColumnName[int32] 78 | V体重 gormcnm.ColumnName[int32] 79 | CreatedAt gormcnm.ColumnName[time.Time] 80 | UpdatedAt gormcnm.ColumnName[time.Time] 81 | } 82 | 83 | func (T *Example3) Columns() *Example3Columns { 84 | return &Example3Columns{ 85 | // Auto-generated: column names and types mapping. DO NOT EDIT. // 自动生成:列名和类型映射。请勿编辑。 86 | ID: gormcnm.Cnm(T.ID, "id"), 87 | CreatedAt: gormcnm.Cnm(T.CreatedAt, "created_at"), 88 | UpdatedAt: gormcnm.Cnm(T.UpdatedAt, "updated_at"), 89 | DeletedAt: gormcnm.Cnm(T.DeletedAt, "deleted_at"), 90 | U账号: gormcnm.Cnm(T.U账号, "username"), 91 | N昵称: gormcnm.Cnm(T.N昵称, "nickname"), 92 | V分数: gormcnm.Cnm(T.V分数, "score"), 93 | Age: gormcnm.Cnm(T.Age, "age"), 94 | Uuid: gormcnm.Cnm(T.Uuid, "uuid"), 95 | Rank: gormcnm.Cnm(T.Rank, "rank"), 96 | } 97 | } 98 | 99 | type Example3Columns struct { 100 | // Auto-generated: embedding operation functions to make it simple to use. DO NOT EDIT. // 自动生成:嵌入操作函数便于使用。请勿编辑。 101 | gormcnm.ColumnOperationClass 102 | // Auto-generated: column names and types in database table. DO NOT EDIT. // 自动生成:数据库表的列名和类型。请勿编辑。 103 | ID gormcnm.ColumnName[uint] 104 | CreatedAt gormcnm.ColumnName[time.Time] 105 | UpdatedAt gormcnm.ColumnName[time.Time] 106 | DeletedAt gormcnm.ColumnName[gorm.DeletedAt] 107 | U账号 gormcnm.ColumnName[string] 108 | N昵称 gormcnm.ColumnName[string] 109 | V分数 gormcnm.ColumnName[uint64] 110 | Age gormcnm.ColumnName[int] 111 | Uuid gormcnm.ColumnName[int] 112 | Rank gormcnm.ColumnName[int] 113 | } 114 | -------------------------------------------------------------------------------- /gen_batch.go: -------------------------------------------------------------------------------- 1 | package gormmom 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/emirpasic/gods/v2/maps/linkedhashmap" 7 | "github.com/yyle88/formatgo" 8 | "github.com/yyle88/must" 9 | "github.com/yyle88/rese" 10 | "github.com/yyle88/tern" 11 | ) 12 | 13 | // Configs represents a collection of configuration instances for batch processing 14 | // Enables processing multiple GORM structures in a single operation 15 | // Provides batch generation and file modification capabilities 16 | // 17 | // Configs 代表一集配置实例,用于批量处理 18 | // 允许在单个操作中处理多个 GORM 结构 19 | // 提供批量生成和文件修改功能 20 | type Configs []*Config 21 | 22 | // NewConfigs creates a batch configuration from GORM structures and options 23 | // Processes multiple GORM structures with consistent options settings 24 | // Returns configured batch prepared for tag generation and file modification 25 | // 26 | // NewConfigs 从 GORM 结构和选项创建批量配置 27 | // 使用一致的选项设置处理多个 GORM 结构 28 | // 返回配置好的批量操作,准备进行标签生成和文件修改 29 | func NewConfigs(structs []*GormStruct, options *Options) Configs { 30 | var configs = make([]*Config, 0, len(structs)) 31 | for _, structI := range structs { 32 | configs = append(configs, NewConfig(structI, options)) 33 | } 34 | return configs 35 | } 36 | 37 | // CodeResults contains the result of batch code generation and replacement 38 | // Provides statistics about changed lines and files during processing 39 | // Includes detailed results for each processed file with change tracking 40 | // 41 | // CodeResults 包含批量代码生成和替换的结果 42 | // 提供处理过程中更改的行数和文件统计 43 | // 包含每个处理文件的详细结果和更改跟踪 44 | type CodeResults struct { 45 | Items []*CodeResult // Detailed results for each file // 每个文件的详细结果 46 | ChangedLineCount int // Total number of changed lines // 更改的总行数 47 | ChangedFileCount int // Total number of changed files // 更改的文件总数 48 | } 49 | 50 | // HasChange checks if any changes were made during batch processing 51 | // Returns true if any lines or files were modified 52 | // 53 | // HasChange 检查批量处理过程中是否有任何更改 54 | // 如果任何行或文件被修改则返回 true 55 | func (R *CodeResults) HasChange() bool { 56 | return R.ChangedLineCount > 0 || R.ChangedFileCount > 0 57 | } 58 | 59 | // CodeResult contains the result of code generation for a single file 60 | // Includes the generated code, source path, and change statistics 61 | // Used for tracking modifications during batch processing operations 62 | // 63 | // CodeResult 包含单个文件的代码生成结果 64 | // 包括生成的代码、源路径和更改统计 65 | // 用于在批量处理操作期间跟踪修改 66 | type CodeResult struct { 67 | OutputCode []byte // Generated code content // 生成的代码内容 68 | SourcePath string // Source file path // 源文件路径 69 | ChangedLineCount int // Number of lines changed // 更改的行数 70 | } 71 | 72 | // HasChange checks if this file result contains any changes 73 | // Returns true if any lines were modified in this file 74 | // 75 | // HasChange 检查此文件结果是否包含任何更改 76 | // 如果此文件中有任何行被修改则返回 true 77 | func (R *CodeResult) HasChange() bool { 78 | return R.ChangedLineCount > 0 79 | } 80 | 81 | // Generate performs batch code generation and file replacement operations 82 | // Processes all configurations and applies changes to source files with formatting 83 | // Returns comprehensive results with statistics about modifications made 84 | // 85 | // Generate 执行批量代码生成和文件替换操作 86 | // 处理所有配置并将更改带格式化地应用到源文件 87 | // 返回包含修改统计的全面结果 88 | func (configs Configs) Generate() *CodeResults { 89 | results := configs.Preview() 90 | for _, item := range results.Items { 91 | if item.HasChange() { 92 | srcCode := must.Have(rese.A1(formatgo.FormatBytes(item.OutputCode))) 93 | must.Done(os.WriteFile(item.SourcePath, srcCode, 0644)) 94 | item.OutputCode = srcCode 95 | } 96 | } 97 | return results 98 | } 99 | 100 | // Preview performs batch code generation without modifying source files 101 | // Processes all configurations and returns generated code without writing to disk 102 | // Returns comprehensive results with statistics about potential modifications 103 | // 104 | // Preview 执行批量代码生成但不修改源文件 105 | // 处理所有配置并返回生成的代码而不写入磁盘 106 | // 返回包含潜在修改统计的全面结果 107 | func (configs Configs) Preview() *CodeResults { 108 | hashMap := linkedhashmap.New[string, *CodeResult]() 109 | for _, config := range configs { 110 | srcPath := config.structI.sourcePath 111 | 112 | previous, exist := hashMap.Get(srcPath) 113 | if exist { 114 | must.Full(previous) 115 | must.Same(previous.SourcePath, srcPath) 116 | } 117 | 118 | sourceCode := tern.BFF(exist, func() []byte { 119 | return previous.OutputCode 120 | }, func() []byte { 121 | return rese.A1(os.ReadFile(srcPath)) 122 | }) 123 | 124 | newCode := config.makeNewCode(sourceCode) 125 | must.Same(newCode.SourcePath, srcPath) 126 | if exist { 127 | must.Same(previous.SourcePath, newCode.SourcePath) 128 | previous.OutputCode = newCode.OutputCode 129 | previous.ChangedLineCount += newCode.ChangedLineCount 130 | } else { 131 | hashMap.Put(newCode.SourcePath, newCode) 132 | } 133 | } 134 | changedFileCount := 0 135 | changedLineCount := 0 136 | hashMap.Each(func(srcPath string, newCode *CodeResult) { 137 | must.Same(newCode.SourcePath, srcPath) 138 | if newCode.HasChange() { 139 | changedLineCount += newCode.ChangedLineCount 140 | changedFileCount++ 141 | } 142 | }) 143 | return &CodeResults{ 144 | Items: hashMap.Values(), 145 | ChangedLineCount: changedLineCount, 146 | ChangedFileCount: changedFileCount, 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /gorm_struct.go: -------------------------------------------------------------------------------- 1 | package gormmom 2 | 3 | import ( 4 | "github.com/emirpasic/gods/v2/maps/linkedhashmap" 5 | "github.com/yyle88/gormmom/internal/utils" 6 | "github.com/yyle88/must" 7 | "github.com/yyle88/rese" 8 | "github.com/yyle88/rese/resb" 9 | "github.com/yyle88/syntaxgo/syntaxgo_ast" 10 | "github.com/yyle88/syntaxgo/syntaxgo_reflect" 11 | "github.com/yyle88/syntaxgo/syntaxgo_search" 12 | "github.com/yyle88/zaplog" 13 | "go.uber.org/zap" 14 | "gorm.io/gorm/schema" 15 | ) 16 | 17 | // GormStruct represents a GORM struct with its location and field information 18 | // Contains the source file path, struct name, and comprehensive field mappings 19 | // Provides structured access to GORM schema and field definitions for processing 20 | // Maintains ordered field mapping using linked hash map for deterministic generation 21 | // 22 | // GormStruct 代表一个 GORM 结构体及其位置和字段信息 23 | // 包含源文件路径、结构体名称和全面的字段映射 24 | // 为处理提供对 GORM 模式和字段定义的结构化访问 25 | // 使用链式哈希映射维护有序字段映射,确保确定性生成 26 | type GormStruct struct { 27 | sourcePath string // Source file path where struct is defined // 定义结构体的源文件路径 28 | structName string // Name of the target struct // 目标结构体的名称 29 | gormSchema *schema.Schema // GORM schema information // GORM 模式信息 30 | gormFields *linkedhashmap.Map[string, *schema.Field] // Ordered field mapping for deterministic processing // 确定性处理的有序字段映射 31 | } 32 | 33 | // NewGormStruct creates a new GormStruct instance with field information 34 | // Reads and processes struct field information from the source file and GORM schema 35 | // Builds ordered field mapping for deterministic tag generation and processing 36 | // Returns configured GormStruct prepared for native language tag processing 37 | // 38 | // NewGormStruct 创建新的 GormStruct 实例并读取字段信息 39 | // 从源文件和 GORM 模式中读取和处理结构体字段信息 40 | // 构建有序字段映射以进行确定性标签生成和处理 41 | // 返回配置好的 GormStruct,准备进行原生语言标签处理 42 | func NewGormStruct(sourcePath string, structName string, gormSchema *schema.Schema) *GormStruct { 43 | zaplog.LOG.Debug("new-struct-schema-info", zap.String("struct_name", structName), zap.String("source_path", sourcePath)) 44 | 45 | // Validate table name for ASCII compatibility before processing 46 | // Ensures database compatibility and prevents downstream issues 47 | // 48 | // 在处理前验证表名的 ASCII 兼容性 49 | // 确保数据库兼容性并避免下游问题 50 | utils.ValidateTableName(gormSchema.Table, structName) 51 | 52 | return &GormStruct{ 53 | sourcePath: must.Nice(sourcePath), 54 | structName: must.Nice(structName), 55 | gormSchema: must.Full(gormSchema), 56 | gormFields: must.Full(utils.NewSchemaFieldsMap(gormSchema)), //这里提前把列做成map方便使用 57 | } 58 | } 59 | 60 | // ParseStruct creates GormStruct using generic type parameter 61 | // T must be the struct type name without pointer (e.g., User not *User) 62 | // Returns configured GormStruct with schema information extracted from type 63 | // 64 | // ParseStruct 使用泛型类型参数创建 GormStruct 65 | // T 只能传类型名称而非带指针的类型名(如 User 而非 *User) 66 | // 返回配置好的 GormStruct,包含从类型中提取的模式信息 67 | func ParseStruct[StructType any](sourcePath string) *GormStruct { 68 | return ParseObject(sourcePath, new(StructType)) 69 | } 70 | 71 | // ParseObject creates GormStruct from an object instance 72 | // Accepts both struct value and struct pointer as the object parameter 73 | // Returns configured GormStruct with schema information extracted from object 74 | // 75 | // ParseObject 从对象实例创建 GormStruct 76 | // object 参数可以传对象值或对象指针 77 | // 返回配置好的 GormStruct,包含从对象中提取的模式信息 78 | func ParseObject(sourcePath string, object interface{}) *GormStruct { 79 | return NewGormStruct(sourcePath, syntaxgo_reflect.GetTypeNameV3(object), utils.ParseSchema(object)) 80 | } 81 | 82 | // ParseObjects creates multiple GormStruct instances from a collection of objects 83 | // Scans Go files in the root DIR to locate struct definitions and build mappings 84 | // Uses ordered map to ensure deterministic processing sequence across executions 85 | // Returns slice of configured GormStruct instances matched with source locations 86 | // 87 | // ParseObjects 从对象集合创建多个 GormStruct 实例 88 | // 扫描根 DIR 中的 Go 文件以定位结构体定义并构建映射 89 | // 使用有序映射确保跨执行的确定性处理顺序 90 | // 返回与源代码位置匹配的配置好的 GormStruct 实例切片 91 | func ParseObjects(root string, objects []interface{}) []*GormStruct { 92 | var objectMap = linkedhashmap.New[string, any]() // 使用有序map来存储对象,避免乱序执行导致每次执行结果不同 93 | for idx, object := range objects { 94 | structName := syntaxgo_reflect.GetTypeNameV3(object) // 获取结构体名称 95 | if structName == "" { // 这里不允许获取不到名称的 96 | zaplog.LOG.Panic("object doesn't have struct name", zap.Int("idx", idx)) 97 | } 98 | objectMap.Put(structName, object) 99 | } 100 | 101 | var results = make([]*GormStruct, 0, len(objects)) 102 | for _, sourcePath := range utils.ListGoFiles(root) { 103 | astBundle := rese.P1(syntaxgo_ast.NewAstBundleV4(sourcePath)) 104 | astFile, _ := astBundle.GetBundle() 105 | for _, structName := range objectMap.Keys() { 106 | if _, ok := syntaxgo_search.FindStructDeclarationByName(astFile, structName); !ok { 107 | continue //说明这个结构体的定义不在这个文件里 108 | } 109 | // 得到相应的结构体对象 110 | oneObject := resb.V1(objectMap.Get(structName)) 111 | // 得到结构体类型的定义,代码文件路径,结构体名称,内部字段列表 112 | results = append(results, ParseObject(sourcePath, oneObject)) 113 | // 移除,以确保只处理一次,这样也能避免重复搜索代码块 114 | objectMap.Remove(structName) 115 | } 116 | } 117 | return results 118 | } 119 | -------------------------------------------------------------------------------- /gormidxname/pattern.go: -------------------------------------------------------------------------------- 1 | // Package gormidxname: Database index naming strategy engine for GORM optimization 2 | // Provides intelligent index name generation from Unicode field names with pattern-based validation 3 | // Supports multiple naming strategies including lowercase and uppercase patterns with length constraints 4 | // Ensures cross-database compatibility by implementing intersection of various database index naming rules 5 | // 6 | // gormidxname: GORM 优化的数据库索引命名策略引擎 7 | // 提供基于模式验证的 Unicode 字段名智能索引名生成 8 | // 支持多种命名策略,包括带长度约束的小写和大写模式 9 | // 通过实现各种数据库索引命名规则的交集来确保跨数据库兼容性 10 | package gormidxname 11 | 12 | import ( 13 | "github.com/yyle88/must" 14 | "gorm.io/gorm/schema" 15 | ) 16 | 17 | // IndexPatternTagEnum represents the index pattern tag type identifier 18 | // Defines enum values for different index types with specific prefixes 19 | // Used to distinguish between standard indexes and unique indexes 20 | // 21 | // IndexPatternTagEnum 代表索引模式标签类型标识符 22 | // 为不同索引类型定义带特定前缀的枚举值 23 | // 用于区分标准索引和唯一索引 24 | type IndexPatternTagEnum string 25 | 26 | const ( 27 | IdxPatternTagName IndexPatternTagEnum = "idx" // Standard index prefix // 标准索引前缀 28 | UdxPatternTagName IndexPatternTagEnum = "udx" // Unique index prefix // 唯一索引前缀 29 | ) 30 | 31 | // PatternEnum represents the index name validation pattern type 32 | // Custom enum type defining validation strategies for index name processing 33 | // Different databases have different index naming rules, so patterns implement intersections 34 | // Ensures compatibility across multiple database systems with unified naming standards 35 | // 36 | // PatternEnum 代表索引名验证模式类型 37 | // 自定义枚举类型,定义索引名处理的验证策略 38 | // 由于不同数据库有不同的索引命名规则,模式实现了规则交集 39 | // 通过统一命名标准确保多个数据库系统的兼容性 40 | type PatternEnum string 41 | 42 | // Pattern defines the interface for index name generation and validation 43 | // Provides pattern identification, name validation, and index name construction 44 | // Ensures generated index names meet specific database requirements and constraints 45 | // 46 | // Pattern 定义索引名生成和验证的接口 47 | // 提供模式识别、名称验证和索引名构建 48 | // 确保生成的索引名满足特定的数据库要求和约束 49 | type Pattern interface { 50 | GetPatternEnum() PatternEnum // Get pattern type identifier // 获取模式类型标识符 51 | CheckIndexName(indexName string) bool // Validate index name format // 验证索引名格式 52 | BuildIndexName(schemaIndex *schema.Index, param *BuildIndexParam) *IndexNameResult // Generate index name from schema // 从模式生成索引名 53 | } 54 | 55 | // BuildIndexParam contains parameters for index name construction 56 | // Provides table, field, and column information for intelligent naming 57 | // Used as input for pattern-based index name generation algorithms 58 | // 59 | // BuildIndexParam 包含索引名构建的参数 60 | // 提供表、字段和列信息以进行智能命名 61 | // 用作基于模式的索引名生成算法的输入 62 | type BuildIndexParam struct { 63 | TableName string // Database table name // 数据库表名 64 | FieldName string // Struct field name // 结构体字段名 65 | ColumnName string // Database column name // 数据库列名 66 | } 67 | 68 | // IndexNameResult contains the result of index name generation 69 | // Provides generated tag field name, index name, and prefix type 70 | // Used as output from pattern-based index name construction algorithms 71 | // 72 | // IndexNameResult 包含索引名生成的结果 73 | // 提供生成的标签字段名、索引名和前缀类型 74 | // 用作基于模式的索引名构建算法的输出 75 | type IndexNameResult struct { 76 | TagFieldName string // Generated tag field name // 生成的标签字段名 77 | NewIndexName string // Generated index name // 生成的索引名 78 | IdxUdxPrefix IndexPatternTagEnum // Index type prefix // 索引类型前缀 79 | } 80 | 81 | // Strategies manages multiple index naming patterns with smart defaults 82 | // Contains default pattern selection and strategy mapping for flexible naming 83 | // Supports pattern registration and selection based on database requirements 84 | // 85 | // Strategies 管理多种索引命名模式,带有智能默认值 86 | // 包含默认模式选择和策略映射,支持灵活命名 87 | // 支持基于数据库需求的模式注册和选择 88 | type Strategies struct { 89 | defaultPattern Pattern // Default naming pattern // 默认命名模式 90 | nameStrategies map[PatternEnum]Pattern // Pattern strategy mapping // 模式策略映射 91 | } 92 | 93 | // NewStrategies creates a new Strategies instance with default index naming patterns 94 | // Initializes with lowercase and uppercase patterns optimized for database compatibility 95 | // Returns configured strategies prepared for index name generation with Unicode field support 96 | // 97 | // NewStrategies 创建新的策略实例,使用默认的索引命名模式 98 | // 使用针对数据库兼容性优化的小写和大写模式进行初始化 99 | // 返回配置好的策略,准备进行支持 Unicode 字段的索引名生成 100 | func NewStrategies() *Strategies { 101 | nameStrategies := make(map[PatternEnum]Pattern) 102 | for _, pattern := range []Pattern{ 103 | NewLowercase63pattern(), 104 | NewUppercase63pattern(), 105 | } { 106 | nameStrategies[pattern.GetPatternEnum()] = pattern 107 | } 108 | return &Strategies{ 109 | defaultPattern: NewLowercase63pattern(), 110 | nameStrategies: nameStrategies, 111 | } 112 | } 113 | 114 | func (s *Strategies) GetDefault() Pattern { 115 | return s.defaultPattern 116 | } 117 | 118 | func (s *Strategies) SetDefault(pattern Pattern) { 119 | s.defaultPattern = pattern 120 | s.SetPattern(pattern) // 同时也要把它设置到 map 里面 121 | } 122 | 123 | func (s *Strategies) GetPattern(patternEnum PatternEnum) Pattern { 124 | namePattern, ok := s.nameStrategies[patternEnum] 125 | must.True(ok) 126 | return namePattern 127 | } 128 | 129 | func (s *Strategies) SetPattern(pattern Pattern) { 130 | s.nameStrategies[pattern.GetPatternEnum()] = pattern 131 | } 132 | -------------------------------------------------------------------------------- /validate_gorm_tags.go: -------------------------------------------------------------------------------- 1 | package gormmom 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yyle88/erero" 7 | "github.com/yyle88/gormmom/gormidxname" 8 | "github.com/yyle88/gormmom/gormmomname" 9 | "github.com/yyle88/gormmom/internal/utils" 10 | "github.com/yyle88/tern/zerotern" 11 | "gorm.io/gorm/schema" 12 | ) 13 | 14 | // ValidateGormTags validates GORM tags to ensure naming conventions 15 | // Checks to find non-ASCII characters in index names and tag issues 16 | // Returns each detected issue as an error 17 | // 18 | // ValidateGormTags 验证 GORM 标签的命名规范 19 | // 检查索引名中的非 ASCII 字符和其他标签问题 20 | // 如果发现问题标签则返回错误 21 | func (configs Configs) ValidateGormTags() error { 22 | for _, config := range configs { 23 | if err := config.validateGormTags(); err != nil { 24 | return erero.Wro(err) 25 | } 26 | } 27 | return nil 28 | } 29 | 30 | // validateGormTags validates GORM tags on a single config instance 31 | // Checks table name, column names, and index names against configured patterns 32 | // Returns error if any validation fails during the checking process 33 | // 34 | // validateGormTags 验证单个配置实例的 GORM 标签 35 | // 根据配置的模式检查表名、列名和索引名 36 | // 如果在检查过程中任何验证失败则返回错误 37 | func (cfg *Config) validateGormTags() error { 38 | // Use GORM schema fields with naming strategies 39 | // 使用 GORM 模式字段结合命名策略进行验证 40 | structName := cfg.structI.structName 41 | gormSchema := cfg.structI.gormSchema 42 | 43 | // Check schema table name 44 | // 检查模式表名 45 | utils.ValidateTableName(gormSchema.Table, structName) 46 | 47 | // Validate column names in each field 48 | // 验证每个字段的列名 49 | for _, field := range gormSchema.Fields { 50 | if err := cfg.validateColumnNaming(structName, field); err != nil { 51 | return erero.Wro(err) 52 | } 53 | } 54 | 55 | // Validate index names (parse indexes once) 56 | // 验证索引名(只解析一次索引) 57 | if err := cfg.validateAllIndexNames(structName, gormSchema); err != nil { 58 | return erero.Wro(err) 59 | } 60 | 61 | return nil 62 | } 63 | 64 | // validateColumnNaming checks column names using configured naming strategies 65 | // Extracts pattern from tag and validates the column name against that pattern 66 | // Returns error if column name does not match the expected naming convention 67 | // 68 | // validateColumnNaming 使用配置的命名策略检查列名 69 | // 从标签中提取模式并根据该模式验证列名 70 | // 如果列名不匹配预期的命名规范则返回错误 71 | func (cfg *Config) validateColumnNaming(structName string, field *schema.Field) error { 72 | // Extract pattern from mom tag, with default fallback 73 | // 从 mom 标签中提取模式,有默认值作为后备 74 | patternName := zerotern.VF(cfg.extractTagGetCnmPattern(fmt.Sprintf("`%s`", field.Tag)), func() string { 75 | return string(cfg.options.columnNamingStrategies.GetDefault().GetPatternEnum()) 76 | }) 77 | 78 | // Get the pattern validation instance 79 | // 获取模式验证实例 80 | pattern := cfg.options.columnNamingStrategies.GetPattern(gormmomname.PatternEnum(patternName)) 81 | 82 | // Use pattern's CheckColumnName method for validation 83 | // 使用模式的 CheckColumnName 方法进行验证 84 | if !pattern.CheckColumnName(field.DBName) { 85 | return erero.Errorf("field %s.%s column '%s' not match pattern '%s'", structName, field.Name, field.DBName, patternName) 86 | } 87 | return nil 88 | } 89 | 90 | // validateAllIndexNames checks all index names using indexNamingStrategies 91 | // Iterates through each index and validates naming against configured patterns 92 | // Prevents corrupted index names like "idx_products_p名���" from being generated 93 | // 94 | // validateAllIndexNames 使用 indexNamingStrategies 检查所有索引名 95 | // 遍历每个索引并根据配置的模式验证命名 96 | // 防止生成损坏的索引名如 "idx_products_p名���" 97 | func (cfg *Config) validateAllIndexNames(structName string, gormSchema *schema.Schema) error { 98 | // Parse indexes from the schema once 99 | // 一次性从模式中解析索引 100 | indexes := gormSchema.ParseIndexes() 101 | 102 | for _, index := range indexes { 103 | for _, indexOption := range index.Fields { 104 | patternName := zerotern.VF(cfg.extractFieldIndexPattern(indexOption.Field, index), func() string { 105 | return string(cfg.options.indexNamingStrategies.GetDefault().GetPatternEnum()) 106 | }) 107 | 108 | // Get the pattern validation instance 109 | // 获取模式验证实例 110 | pattern := cfg.options.indexNamingStrategies.GetPattern(gormidxname.PatternEnum(patternName)) 111 | 112 | // Use pattern's CheckIndexName method for validation 113 | // 使用模式的 CheckIndexName 方法进行验证 114 | if !pattern.CheckIndexName(index.Name) { 115 | return erero.Errorf("struct %s index '%s' not match pattern '%s'", structName, index.Name, patternName) 116 | } 117 | } 118 | } 119 | 120 | return nil 121 | } 122 | 123 | // extractFieldIndexPattern extracts index pattern from field's mom tag based on index type 124 | // Determines the pattern tag name based on index class (UNIQUE or standard) 125 | // Returns the pattern string for index name validation 126 | // 127 | // extractFieldIndexPattern 根据索引类型从字段的 mom 标签中提取索引模式 128 | // 基于索引类别(UNIQUE 或标准)确定模式标签名 129 | // 返回用于索引名验证的模式字符串 130 | func (cfg *Config) extractFieldIndexPattern(field *schema.Field, index *schema.Index) string { 131 | // Get the struct field tag to extract pattern information 132 | // 获取结构体字段标签以提取模式信息 133 | tagCode := fmt.Sprintf("`%s`", field.Tag) 134 | 135 | // Determine index pattern tag based on index type 136 | // 根据索引类型确定索引模式标签 137 | var patternTagEnum gormidxname.IndexPatternTagEnum 138 | if index.Class == "UNIQUE" { 139 | patternTagEnum = gormidxname.UdxPatternTagName 140 | } else { 141 | patternTagEnum = gormidxname.IdxPatternTagName 142 | } 143 | 144 | return cfg.extractTagFieldGetValue(tagCode, cfg.options.systemTagName, string(patternTagEnum)) 145 | } 146 | -------------------------------------------------------------------------------- /internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | "unicode" 13 | 14 | "github.com/emirpasic/gods/v2/maps/linkedhashmap" 15 | "github.com/yyle88/gormcngen" 16 | "github.com/yyle88/must" 17 | "github.com/yyle88/rese" 18 | "github.com/yyle88/rese/resb" 19 | "github.com/yyle88/syntaxgo/syntaxgo_ast" 20 | "github.com/yyle88/syntaxgo/syntaxgo_reflect" 21 | "github.com/yyle88/syntaxgo/syntaxgo_search" 22 | "github.com/yyle88/zaplog" 23 | "go.uber.org/zap" 24 | "gorm.io/gorm/schema" 25 | ) 26 | 27 | // NewSchemaFieldsMap 把字段列表由 slice 转换为 map,以结构体中go的字段名为主键 28 | func NewSchemaFieldsMap(gormSchema *schema.Schema) *linkedhashmap.Map[string, *schema.Field] { 29 | gormcngen.ShowSchemaEnglish(gormSchema) 30 | gormcngen.ShowSchemaChinese(gormSchema) 31 | 32 | res := linkedhashmap.New[string, *schema.Field]() 33 | for _, field := range gormSchema.Fields { 34 | res.Put(field.Name, field) //键是Go结构体成员名称 35 | } 36 | return res 37 | } 38 | 39 | // ListGoFiles 获取指定目录下的所有 .go 文件路径(不递归子目录) 40 | func ListGoFiles(root string) []string { 41 | var paths []string 42 | for _, one := range rese.A1(os.ReadDir(root)) { 43 | // 检查是否是文件和扩展名为 .go 44 | if !one.IsDir() && filepath.Ext(one.Name()) == ".go" { 45 | paths = append(paths, filepath.Join(root, one.Name())) 46 | } 47 | } 48 | return paths 49 | } 50 | 51 | func ParseSchema(object interface{}) *schema.Schema { 52 | return rese.P1(schema.Parse(object, &sync.Map{}, &schema.NamingStrategy{ 53 | SingularTable: false, //和默认值相同 54 | NoLowerCase: false, //和默认值相同 55 | })) 56 | } 57 | 58 | func ParseTags[T any](sourceCode []byte, structObject *T) *linkedhashmap.Map[string, string] { 59 | astBundle := rese.P1(syntaxgo_ast.NewAstBundleV1(sourceCode)) 60 | astFile, fileSet := astBundle.GetBundle() 61 | zaplog.LOG.Debug("ast-get-package-name", zap.String("package_name", astFile.Name.Name)) 62 | 63 | structName := syntaxgo_reflect.GetTypeNameV4(structObject) 64 | zaplog.LOG.Debug("reflect-get-struct-name", zap.String("name", structName)) 65 | 66 | structType := resb.P1(syntaxgo_search.FindStructTypeByName(astFile, structName)) 67 | must.Done(ast.Print(fileSet, structType)) 68 | 69 | var results = linkedhashmap.New[string, string]() 70 | if structType.Fields != nil { 71 | for _, field := range structType.Fields.List { 72 | for _, name := range field.Names { 73 | if field.Tag != nil { 74 | results.Put(name.Name, field.Tag.Value) 75 | } else { 76 | results.Put(name.Name, "") 77 | } 78 | } 79 | } 80 | } 81 | return results 82 | } 83 | 84 | func TrimQuotes(s string) string { 85 | return strings.Trim(s, `"`) 86 | } 87 | 88 | func TrimBackticks(s string) string { 89 | return strings.Trim(s, "`") 90 | } 91 | 92 | func ParseTagsTrimBackticks[T any](sourceCode []byte, structObject *T) *linkedhashmap.Map[string, string] { 93 | results := linkedhashmap.New[string, string]() 94 | ParseTags(sourceCode, structObject).Each(func(key string, value string) { 95 | results.Put(key, TrimBackticks(value)) 96 | }) 97 | return results 98 | } 99 | 100 | // NewCommonRegexp 创建一个正则表达式,检查列名的长度和字符 101 | // 当列名前带个前导空格 比如 `gorm:"column: name;"` 时,在gorm中也是可以用的,但该规则里不允许这种情况,避免出现其它问题 102 | func NewCommonRegexp(maxLen int) *regexp.Regexp { 103 | return regexp.MustCompile(`^[a-zA-Z0-9_]{1,` + strconv.Itoa(maxLen) + `}$`) 104 | } 105 | 106 | // MustMatchRegexp 检查字符串是否匹配正则表达式,如果不匹配则抛出 panic 107 | func MustMatchRegexp(regexpRegexp *regexp.Regexp, value string) { 108 | if !regexpRegexp.MatchString(value) { 109 | zaplog.LOG.Panic("regexp does not match", zap.String("regexp", regexpRegexp.String()), zap.String("value", value)) 110 | } 111 | } 112 | 113 | // ValidateTableName validates table name to ensure database support 114 | // Checks if table name contains ASCII characters suitable to generate indexes 115 | // Provides actionable message with solution when validation fails 116 | // 117 | // ValidateTableName 验证表名的数据库兼容性 118 | // 检查表名是否包含适合索引生成的 ASCII 字符 119 | // 在验证失败时提供带解决方案的有用信息 120 | func ValidateTableName(tableName string, structName string) { 121 | // Check if table name contains non-ASCII characters 122 | if !IsASCII(tableName) { 123 | zaplog.LOG.Panic( 124 | "Table name contains non-ASCII characters which is not compatible. Please add a TableName() method to the struct with an ASCII table name.", 125 | zap.String("struct_name", structName), 126 | zap.String("current_table_name", tableName), 127 | zap.String("solution", fmt.Sprintf("Add this method to the struct: func (%s) TableName() string { return \"ascii_table_name\" }", structName)), 128 | zap.String("example", fmt.Sprintf("func (%s) TableName() string { return \"users\" }", structName)), 129 | ) 130 | } 131 | 132 | // Check length constraint for table names 133 | if len(tableName) > 63 { 134 | zaplog.LOG.Panic( 135 | "Table name exceeds maximum length (63 characters). Please add a TableName() method to the struct with a compact name.", 136 | zap.String("struct_name", structName), 137 | zap.String("current_table_name", tableName), 138 | zap.Int("current_length", len(tableName)), 139 | zap.Int("max_length", 63), 140 | zap.String("solution", fmt.Sprintf("Add this method to the struct: func (%s) TableName() string { return \"compact_name\" }", structName)), 141 | ) 142 | } 143 | } 144 | 145 | // IsASCII checks if string contains just ASCII characters 146 | // IsASCII 检查字符串是否仅包含 ASCII 字符 147 | func IsASCII(s string) bool { 148 | for _, r := range s { 149 | if r > unicode.MaxASCII { 150 | return false 151 | } 152 | } 153 | return true 154 | } 155 | -------------------------------------------------------------------------------- /gen_options.go: -------------------------------------------------------------------------------- 1 | package gormmom 2 | 3 | import ( 4 | "github.com/yyle88/gormmom/gormidxname" 5 | "github.com/yyle88/gormmom/gormmomname" 6 | ) 7 | 8 | // Options represents configuration settings for GORM tag generation 9 | // Contains naming strategies and behavior controls with native language field processing 10 | // Provides customizable tag names, column naming rules, and index generation settings 11 | // Supports smart skipping of basic fields and flexible configuration options 12 | // 13 | // Options 代表 GORM 标签生成的配置设置 14 | // 包含原生语言字段处理的命名策略和行为控制 15 | // 提供可自定义的标签名称、列命名规则和索引生成设置 16 | // 支持智能跳过基础字段和灵活的配置选项 17 | type Options struct { 18 | systemTagName string // System tag name // 系统标签名称 19 | subTagName string // Sub-tag name for column naming patterns // 列命名模式的子标签名称 20 | columnNamingStrategies *gormmomname.Strategies // Column naming strategies with native languages // 原生语言的列命名策略 21 | skipBasicColumnName bool // Skip simple fields without custom configurations // 跳过没有自定义配置的简单字段 22 | autoIndexName bool // Enable index name generation // 启用索引名称生成 23 | indexNamingStrategies *gormidxname.Strategies // Index naming strategies with database optimization // 数据库优化的索引命名策略 24 | } 25 | 26 | // NewOptions creates a new Options instance with default settings optimized with native language processing 27 | // Initializes with smart defaults including tag naming, column strategies, and index generation 28 | // Returns configured options prepared for GORM tag generation with Unicode field support 29 | // 30 | // NewOptions 创建新的选项实例,使用针对原生语言处理优化的默认设置 31 | // 使用智能默认值初始化,包括标签命名、列策略和索引生成 32 | // 返回配置好的选项,准备进行支持 Unicode 字段的 GORM 标签生成 33 | func NewOptions() *Options { 34 | return &Options{ 35 | systemTagName: "mom", // mother_tongue native_language. // 因为我也不太能熟练拼写更高级的单词,还不如返璞归真直接用口语表示 36 | subTagName: "mcp", // m(mother_tongue) c(column_name) p(pattern) // 表示列名的样式标签 37 | columnNamingStrategies: gormmomname.NewStrategies(), 38 | skipBasicColumnName: true, 39 | autoIndexName: true, 40 | indexNamingStrategies: gormidxname.NewStrategies(), 41 | } 42 | } 43 | 44 | // WithTagName sets the system tag name used in struct tags 45 | // Configures the tag key name that gormmom uses to store pattern information 46 | // Returns the Options instance to enable method chaining 47 | // 48 | // WithTagName 设置结构体标签中使用的系统标签名 49 | // 配置 gormmom 用于存储模式信息的标签键名 50 | // 返回 Options 实例以启用方法链 51 | func (opt *Options) WithTagName(systemTagName string) *Options { 52 | opt.systemTagName = systemTagName 53 | return opt 54 | } 55 | 56 | // WithSubTagName sets the sub-tag name for column naming patterns 57 | // Configures the nested tag field name within the system tag 58 | // Returns the Options instance to enable method chaining 59 | // 60 | // WithSubTagName 设置列命名模式的子标签名 61 | // 配置系统标签内的嵌套标签字段名 62 | // 返回 Options 实例以启用方法链 63 | func (opt *Options) WithSubTagName(subTagName string) *Options { 64 | opt.subTagName = subTagName 65 | return opt 66 | } 67 | 68 | // WithColumnPattern registers a custom column naming pattern 69 | // Adds the pattern to the strategies collection for column name generation 70 | // Returns the Options instance to enable method chaining 71 | // 72 | // WithColumnPattern 注册自定义的列命名模式 73 | // 将模式添加到列名生成的策略集合中 74 | // 返回 Options 实例以启用方法链 75 | func (opt *Options) WithColumnPattern(pattern gormmomname.Pattern) *Options { 76 | opt.columnNamingStrategies.SetPattern(pattern) 77 | return opt 78 | } 79 | 80 | // WithDefaultColumnPattern sets the default column naming pattern 81 | // Used when no specific pattern is configured in struct tags 82 | // Returns the Options instance to enable method chaining 83 | // 84 | // WithDefaultColumnPattern 设置默认的列命名模式 85 | // 当结构体标签中没有配置特定模式时使用 86 | // 返回 Options 实例以启用方法链 87 | func (opt *Options) WithDefaultColumnPattern(pattern gormmomname.Pattern) *Options { 88 | opt.columnNamingStrategies.SetDefault(pattern) 89 | return opt 90 | } 91 | 92 | // WithSkipBasicColumnName enables or disables skipping of basic column names 93 | // When true, fields with standard ASCII column names are skipped 94 | // Returns the Options instance to enable method chaining 95 | // 96 | // WithSkipBasicColumnName 启用或禁用跳过基本列名 97 | // 当为 true 时,具有标准 ASCII 列名的字段被跳过 98 | // 返回 Options 实例以启用方法链 99 | func (opt *Options) WithSkipBasicColumnName(skipBasicColumnName bool) *Options { 100 | opt.skipBasicColumnName = skipBasicColumnName 101 | return opt 102 | } 103 | 104 | // WithAutoIndexName enables or disables index name regeneration 105 | // When true, index names are regenerated based on configured patterns 106 | // Returns the Options instance to enable method chaining 107 | // 108 | // WithAutoIndexName 启用或禁用索引名重新生成 109 | // 当为 true 时,索引名基于配置的模式重新生成 110 | // 返回 Options 实例以启用方法链 111 | func (opt *Options) WithAutoIndexName(autoIndexName bool) *Options { 112 | opt.autoIndexName = autoIndexName 113 | return opt 114 | } 115 | 116 | // WithIndexPattern registers a custom index naming pattern 117 | // Adds the pattern to the strategies collection for index name generation 118 | // Returns the Options instance to enable method chaining 119 | // 120 | // WithIndexPattern 注册自定义的索引命名模式 121 | // 将模式添加到索引名生成的策略集合中 122 | // 返回 Options 实例以启用方法链 123 | func (opt *Options) WithIndexPattern(pattern gormidxname.Pattern) *Options { 124 | opt.indexNamingStrategies.SetPattern(pattern) 125 | return opt 126 | } 127 | 128 | // WithDefaultIndexPattern sets the default index naming pattern 129 | // Used when no specific pattern is configured for index generation 130 | // Returns the Options instance to enable method chaining 131 | // 132 | // WithDefaultIndexPattern 设置默认的索引命名模式 133 | // 当索引生成没有配置特定模式时使用 134 | // 返回 Options 实例以启用方法链 135 | func (opt *Options) WithDefaultIndexPattern(pattern gormidxname.Pattern) *Options { 136 | opt.indexNamingStrategies.SetDefault(pattern) 137 | return opt 138 | } 139 | -------------------------------------------------------------------------------- /internal/examples/example1/example1_test.go: -------------------------------------------------------------------------------- 1 | package example1 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/google/uuid" 9 | "github.com/stretchr/testify/require" 10 | "github.com/yyle88/done" 11 | "github.com/yyle88/gormmom/internal/examples/example1/internal/models" 12 | "github.com/yyle88/gormrepo" 13 | "github.com/yyle88/gormrepo/gormclass" 14 | "github.com/yyle88/neatjson/neatjsons" 15 | "github.com/yyle88/rese" 16 | "gorm.io/driver/sqlite" 17 | "gorm.io/gorm" 18 | "gorm.io/gorm/logger" 19 | ) 20 | 21 | var caseDB *gorm.DB 22 | 23 | func TestMain(m *testing.M) { 24 | dsn := fmt.Sprintf("file:db-%s?mode=memory&cache=shared", uuid.New().String()) 25 | db := rese.P1(gorm.Open(sqlite.Open(dsn), &gorm.Config{ 26 | Logger: logger.Default.LogMode(logger.Info), 27 | })) 28 | defer rese.F0(rese.P1(db.DB()).Close) 29 | 30 | done.Done(db.AutoMigrate(&models.T用户{})) 31 | 32 | caseDB = db 33 | m.Run() 34 | } 35 | 36 | func TestChineseUserExample(t *testing.T) { 37 | // Test Chinese fields using gormrepo enterprise repository pattern 38 | repo := gormrepo.NewRepo(gormclass.Use(&models.T用户{})) 39 | ctx := context.Background() 40 | 41 | // Create test data 42 | users := []*models.T用户{ 43 | { 44 | U用户名: "张三", 45 | E邮箱: "zhang@example.com", 46 | A年龄: 25, 47 | D电话: "13800138000", 48 | J住所: "北京市海淀区", 49 | S状态: "活跃", 50 | }, 51 | { 52 | U用户名: "李四", 53 | E邮箱: "li@example.com", 54 | A年龄: 30, 55 | D电话: "13800138001", 56 | J住所: "上海市浦东区", 57 | S状态: "活跃", 58 | }, 59 | { 60 | U用户名: "王五", 61 | E邮箱: "wang@example.com", 62 | A年龄: 28, 63 | D电话: "13800138002", 64 | J住所: "深圳市南山区", 65 | S状态: "非活跃", 66 | }, 67 | } 68 | 69 | // Batch insert data 70 | for _, user := range users { 71 | require.NoError(t, caseDB.Create(user).Error) 72 | } 73 | 74 | // Test 1: Use First to find single record 75 | t.Run("First Find", func(t *testing.T) { 76 | result, err := repo.With(ctx, caseDB).First(func(db *gorm.DB, cls *models.T用户Columns) *gorm.DB { 77 | return db.Where(cls.U用户名.Eq("张三")) 78 | }) 79 | require.NoError(t, err) 80 | require.Equal(t, "zhang@example.com", result.E邮箱) 81 | require.Equal(t, 25, result.A年龄) 82 | t.Log("Find single record:", neatjsons.S(result)) 83 | }) 84 | 85 | // Test 2: Use Find to get multiple records 86 | t.Run("Find Multiple", func(t *testing.T) { 87 | results, err := repo.With(ctx, caseDB).Find(func(db *gorm.DB, cls *models.T用户Columns) *gorm.DB { 88 | return db.Where(cls.S状态.Eq("活跃")).Order(cls.A年龄.Ob("asc").Ox()) 89 | }) 90 | require.NoError(t, err) 91 | require.Len(t, results, 2) 92 | require.Equal(t, "张三", results[0].U用户名) // younger user first 93 | require.Equal(t, "李四", results[1].U用户名) 94 | t.Log("Find multiple records:", neatjsons.S(results)) 95 | }) 96 | 97 | // Test 3: Use Count to get statistics 98 | t.Run("Count Records", func(t *testing.T) { 99 | count, err := repo.With(ctx, caseDB).Count(func(db *gorm.DB, cls *models.T用户Columns) *gorm.DB { 100 | return db.Where(cls.A年龄.Gte(26)) 101 | }) 102 | require.NoError(t, err) 103 | require.Equal(t, int64(2), count) // 2 users with age >= 26 104 | t.Log("Records with age >= 26:", count) 105 | }) 106 | 107 | // Test 4: Complex condition search 108 | t.Run("Complex Search", func(t *testing.T) { 109 | results, err := repo.With(ctx, caseDB).Find(func(db *gorm.DB, cls *models.T用户Columns) *gorm.DB { 110 | return db.Where(cls.A年龄.Between(25, 30)).Where(cls.J住所.Like("%市%")) 111 | }) 112 | require.NoError(t, err) 113 | require.Len(t, results, 3) // all users match conditions 114 | t.Log("Complex search result:", len(results), "records") 115 | }) 116 | 117 | // Test 5: Use Update to modify data 118 | t.Run("Update Record", func(t *testing.T) { 119 | // Update Zhang San's age 120 | err := repo.With(ctx, caseDB).Update( 121 | func(db *gorm.DB, cls *models.T用户Columns) *gorm.DB { 122 | return db.Where(cls.U用户名.Eq("张三")) 123 | }, 124 | func(cls *models.T用户Columns) (string, interface{}) { 125 | return cls.A年龄.Kv(26) 126 | }, 127 | ) 128 | require.NoError(t, err) 129 | t.Log("Successfully updated Zhang San's age") 130 | 131 | // Verify update result 132 | updated, err := repo.With(ctx, caseDB).First(func(db *gorm.DB, cls *models.T用户Columns) *gorm.DB { 133 | return db.Where(cls.U用户名.Eq("张三")) 134 | }) 135 | require.NoError(t, err) 136 | require.Equal(t, 26, updated.A年龄) 137 | t.Log("Updated Zhang San:", neatjsons.S(updated)) 138 | }) 139 | 140 | // Test 6: Use Updates for batch modification 141 | t.Run("Updates Batch", func(t *testing.T) { 142 | // Batch update inactive record information 143 | err := repo.With(ctx, caseDB).Updates( 144 | func(db *gorm.DB, cls *models.T用户Columns) *gorm.DB { 145 | return db.Where(cls.S状态.Eq("非活跃")) 146 | }, 147 | func(cls *models.T用户Columns) map[string]interface{} { 148 | return cls. 149 | Kw(cls.A年龄.Kv(32)). 150 | Kw(cls.J住所.Kv("广州市天河区")). 151 | AsMap() 152 | }, 153 | ) 154 | require.NoError(t, err) 155 | t.Log("Successfully batch updated inactive record information") 156 | }) 157 | 158 | // Test 7: Use Exist to check record existence 159 | t.Run("Exist Check", func(t *testing.T) { 160 | // Check if records with age > 35 exist 161 | exists, err := repo.With(ctx, caseDB).Exist(func(db *gorm.DB, cls *models.T用户Columns) *gorm.DB { 162 | return db.Where(cls.A年龄.Gt(35)) 163 | }) 164 | require.NoError(t, err) 165 | require.False(t, exists) // should not exist 166 | t.Log("Records with age > 35 exist:", exists) 167 | 168 | // Check if records with age = 26 exist 169 | exists, err = repo.With(ctx, caseDB).Exist(func(db *gorm.DB, cls *models.T用户Columns) *gorm.DB { 170 | return db.Where(cls.A年龄.Eq(26)) 171 | }) 172 | require.NoError(t, err) 173 | require.True(t, exists) // should exist (Zhang San) 174 | t.Log("Records with age = 26 exist:", exists) 175 | }) 176 | 177 | // Test 8: Advanced enterprise search combinations 178 | t.Run("Advanced Enterprise Search", func(t *testing.T) { 179 | // Use multiple conditions and IN filters 180 | results, err := repo.With(ctx, caseDB).Find(func(db *gorm.DB, cls *models.T用户Columns) *gorm.DB { 181 | return db.Where(cls.U用户名.In([]string{"张三", "李四", "王五"})). 182 | Where(cls.A年龄.Gte(25)). 183 | Where(cls.E邮箱.NotLike("%不存在%")). 184 | Order(cls.A年龄.Ob("desc").Ox()). 185 | Order(cls.U用户名.Ob("asc").Ox()) 186 | }) 187 | require.NoError(t, err) 188 | require.Len(t, results, 3) 189 | t.Log("Advanced enterprise search result:", len(results), "records") 190 | 191 | // Verify sorting result: by age desc, then by name asc 192 | ages := make([]int, len(results)) 193 | for i, user := range results { 194 | ages[i] = user.A年龄 195 | } 196 | t.Log("Age sorting result:", ages) 197 | }) 198 | } 199 | -------------------------------------------------------------------------------- /internal/examples/example3/example3_test.go: -------------------------------------------------------------------------------- 1 | package example3 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/google/uuid" 9 | "github.com/stretchr/testify/require" 10 | "github.com/yyle88/done" 11 | "github.com/yyle88/gormmom/internal/examples/example3/internal/models" 12 | "github.com/yyle88/gormrepo" 13 | "github.com/yyle88/gormrepo/gormclass" 14 | "github.com/yyle88/neatjson/neatjsons" 15 | "github.com/yyle88/rese" 16 | "gorm.io/driver/sqlite" 17 | "gorm.io/gorm" 18 | "gorm.io/gorm/logger" 19 | ) 20 | 21 | var caseDB *gorm.DB 22 | 23 | func TestMain(m *testing.M) { 24 | dsn := fmt.Sprintf("file:db-%s?mode=memory&cache=shared", uuid.New().String()) 25 | db := rese.P1(gorm.Open(sqlite.Open(dsn), &gorm.Config{ 26 | Logger: logger.Default.LogMode(logger.Info), 27 | })) 28 | defer rese.F0(rese.P1(db.DB()).Close) 29 | 30 | done.Done(db.AutoMigrate(&models.T사용자{})) 31 | 32 | caseDB = db 33 | m.Run() 34 | } 35 | 36 | func TestKoreanUserExample(t *testing.T) { 37 | // Test Korean fields using gormrepo enterprise repository pattern 38 | repo := gormrepo.NewRepo(gormclass.Use(&models.T사용자{})) 39 | ctx := context.Background() 40 | 41 | // Create Korean test data 42 | users := []*models.T사용자{ 43 | { 44 | U사용자명: "김민수", 45 | E이메일: "kim@example.kr", 46 | N나이: 27, 47 | J전화: "010-1234-5678", 48 | J주소: "서울특별시 강남구", 49 | S상태: "활성", 50 | }, 51 | { 52 | U사용자명: "박지은", 53 | E이메일: "park@example.kr", 54 | N나이: 32, 55 | J전화: "010-2345-6789", 56 | J주소: "부산광역시 해운대구", 57 | S상태: "활성", 58 | }, 59 | { 60 | U사용자명: "이철호", 61 | E이메일: "lee@example.kr", 62 | N나이: 29, 63 | J전화: "010-3456-7890", 64 | J주소: "대구광역시 중구", 65 | S상태: "비활성", 66 | }, 67 | } 68 | 69 | // Batch insert Korean test data 70 | for _, user := range users { 71 | require.NoError(t, caseDB.Create(user).Error) 72 | } 73 | 74 | // Test 1: Find single record using Korean fields 75 | t.Run("Korean fields single find", func(t *testing.T) { 76 | result, err := repo.With(ctx, caseDB).First(func(db *gorm.DB, cls *models.T사용자Columns) *gorm.DB { 77 | return db.Where(cls.U사용자명.Eq("김민수")) 78 | }) 79 | require.NoError(t, err) 80 | require.Equal(t, "kim@example.kr", result.E이메일) 81 | require.Equal(t, 27, result.N나이) 82 | t.Log("Korean record find result:", neatjsons.S(result)) 83 | }) 84 | 85 | // Test 2: Find multiple records using Korean fields 86 | t.Run("Korean fields multiple find", func(t *testing.T) { 87 | results, err := repo.With(ctx, caseDB).Find(func(db *gorm.DB, cls *models.T사용자Columns) *gorm.DB { 88 | return db.Where(cls.S상태.Eq("활성")).Order(cls.N나이.Ob("asc").Ox()) 89 | }) 90 | require.NoError(t, err) 91 | require.Len(t, results, 2) 92 | require.Equal(t, "김민수", results[0].U사용자명) // younger user first 93 | require.Equal(t, "박지은", results[1].U사용자명) 94 | t.Log("Active records find result:", len(results), "items") 95 | }) 96 | 97 | // Test 3: Count statistics using Korean fields 98 | t.Run("Korean fields count stats", func(t *testing.T) { 99 | count, err := repo.With(ctx, caseDB).Count(func(db *gorm.DB, cls *models.T사용자Columns) *gorm.DB { 100 | return db.Where(cls.N나이.Gte(28)) 101 | }) 102 | require.NoError(t, err) 103 | require.Equal(t, int64(2), count) // 2 users with age >= 28 104 | t.Log("Records with age > 28:", count) 105 | }) 106 | 107 | // Test 4: Complex condition search using Korean fields 108 | t.Run("Korean fields complex search", func(t *testing.T) { 109 | results, err := repo.With(ctx, caseDB).Find(func(db *gorm.DB, cls *models.T사용자Columns) *gorm.DB { 110 | return db.Where(cls.J주소.Like("%시%")). 111 | Where(cls.N나이.Between(25, 35)). 112 | Where(cls.E이메일.Like("%@example.kr")) 113 | }) 114 | require.NoError(t, err) 115 | require.Len(t, results, 3) // all users match conditions 116 | t.Log("Complex condition search result:", len(results), "items") 117 | }) 118 | 119 | // Test 5: Update operation using Korean fields 120 | t.Run("Korean fields update operation", func(t *testing.T) { 121 | // Update Kim Min-su's age 122 | err := repo.With(ctx, caseDB).Update( 123 | func(db *gorm.DB, cls *models.T사용자Columns) *gorm.DB { 124 | return db.Where(cls.U사용자명.Eq("김민수")) 125 | }, 126 | func(cls *models.T사용자Columns) (string, interface{}) { 127 | return cls.N나이.Kv(28) 128 | }, 129 | ) 130 | require.NoError(t, err) 131 | t.Log("Successfully updated Kim Min-su's age") 132 | 133 | // Verify update result 134 | updated, err := repo.With(ctx, caseDB).First(func(db *gorm.DB, cls *models.T사용자Columns) *gorm.DB { 135 | return db.Where(cls.U사용자명.Eq("김민수")) 136 | }) 137 | require.NoError(t, err) 138 | require.Equal(t, 28, updated.N나이) 139 | t.Log("Updated Kim Min-su info:", neatjsons.S(updated)) 140 | }) 141 | 142 | // Test 6: Batch update using Korean fields 143 | t.Run("Korean fields batch update", func(t *testing.T) { 144 | // Batch update inactive record status 145 | err := repo.With(ctx, caseDB).Updates( 146 | func(db *gorm.DB, cls *models.T사용자Columns) *gorm.DB { 147 | return db.Where(cls.S상태.Eq("비활성")) 148 | }, 149 | func(cls *models.T사용자Columns) map[string]interface{} { 150 | return cls. 151 | Kw(cls.S상태.Kv("휴면")). 152 | Kw(cls.J주소.Kv("주소 미확인")). 153 | AsMap() 154 | }, 155 | ) 156 | require.NoError(t, err) 157 | t.Log("Successfully batch updated inactive users") 158 | }) 159 | 160 | // Test 7: Existence check using Korean fields 161 | t.Run("Korean fields existence check", func(t *testing.T) { 162 | // Check if records with age > 40 exist 163 | exists, err := repo.With(ctx, caseDB).Exist(func(db *gorm.DB, cls *models.T사용자Columns) *gorm.DB { 164 | return db.Where(cls.N나이.Gt(40)) 165 | }) 166 | require.NoError(t, err) 167 | require.False(t, exists) // should not exist 168 | t.Log("Records with age > 40 exist:", exists) 169 | 170 | // Check if Seoul records exist 171 | exists, err = repo.With(ctx, caseDB).Exist(func(db *gorm.DB, cls *models.T사용자Columns) *gorm.DB { 172 | return db.Where(cls.J주소.Like("서울%")) 173 | }) 174 | require.NoError(t, err) 175 | require.True(t, exists) // should exist 176 | t.Log("Seoul records exist:", exists) 177 | }) 178 | 179 | // Test 8: Validate documentation examples 180 | t.Run("Validate documentation Korean examples", func(t *testing.T) { 181 | // Verify Korean field names can correctly map to database columns 182 | cols := (&models.T사용자{}).Columns() 183 | 184 | t.Log("Korean field mapping validation:") 185 | t.Logf("U사용자명 -> %s", cols.U사용자명.Name()) 186 | t.Logf("E이메일 -> %s", cols.E이메일.Name()) 187 | t.Logf("N나이 -> %s", cols.N나이.Name()) 188 | t.Logf("S상태 -> %s", cols.S상태.Name()) 189 | 190 | // Ensure all fields have correct column name mapping 191 | require.NotEmpty(t, cols.U사용자명.Name()) 192 | require.NotEmpty(t, cols.E이메일.Name()) 193 | require.NotEmpty(t, cols.N나이.Name()) 194 | require.NotEmpty(t, cols.S상태.Name()) 195 | 196 | t.Log("✅ Documentation Korean examples validated successfully!") 197 | }) 198 | } 199 | -------------------------------------------------------------------------------- /internal/examples/example2/example2_test.go: -------------------------------------------------------------------------------- 1 | package example2 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/google/uuid" 9 | "github.com/stretchr/testify/require" 10 | "github.com/yyle88/done" 11 | "github.com/yyle88/gormmom/internal/examples/example2/internal/models" 12 | "github.com/yyle88/gormrepo" 13 | "github.com/yyle88/gormrepo/gormclass" 14 | "github.com/yyle88/neatjson/neatjsons" 15 | "github.com/yyle88/rese" 16 | "gorm.io/driver/sqlite" 17 | "gorm.io/gorm" 18 | "gorm.io/gorm/logger" 19 | ) 20 | 21 | var caseDB *gorm.DB 22 | 23 | func TestMain(m *testing.M) { 24 | dsn := fmt.Sprintf("file:db-%s?mode=memory&cache=shared", uuid.New().String()) 25 | db := rese.P1(gorm.Open(sqlite.Open(dsn), &gorm.Config{ 26 | Logger: logger.Default.LogMode(logger.Info), 27 | })) 28 | defer rese.F0(rese.P1(db.DB()).Close) 29 | 30 | done.Done(db.AutoMigrate(&models.Tユーザー{})) 31 | 32 | caseDB = db 33 | m.Run() 34 | } 35 | 36 | func TestJapaneseUserExample(t *testing.T) { 37 | // Test Japanese fields using gormrepo enterprise repository pattern 38 | repo := gormrepo.NewRepo(gormclass.Use(&models.Tユーザー{})) 39 | ctx := context.Background() 40 | 41 | // Create Japanese test data 42 | users := []*models.Tユーザー{ 43 | { 44 | Uユーザー名: "田中太郎", 45 | Eメール: "tanaka@example.jp", 46 | A年齢: 25, 47 | D電話: "090-1234-5678", 48 | J住所: "東京都渋谷区", 49 | Sステータス: "アクティブ", 50 | }, 51 | { 52 | Uユーザー名: "佐藤花子", 53 | Eメール: "sato@example.jp", 54 | A年齢: 30, 55 | D電話: "090-2345-6789", 56 | J住所: "大阪市中央区", 57 | Sステータス: "アクティブ", 58 | }, 59 | { 60 | Uユーザー名: "鈴木一郎", 61 | Eメール: "suzuki@example.jp", 62 | A年齢: 28, 63 | D電話: "090-3456-7890", 64 | J住所: "名古屋市中区", 65 | Sステータス: "非アクティブ", 66 | }, 67 | } 68 | 69 | // Batch insert Japanese test data 70 | for _, user := range users { 71 | require.NoError(t, caseDB.Create(user).Error) 72 | } 73 | 74 | // Test 1: Find single record using Japanese fields 75 | t.Run("Japanese fields single find", func(t *testing.T) { 76 | result, err := repo.With(ctx, caseDB).First(func(db *gorm.DB, cls *models.TユーザーColumns) *gorm.DB { 77 | return db.Where(cls.Uユーザー名.Eq("田中太郎")) 78 | }) 79 | require.NoError(t, err) 80 | require.Equal(t, "tanaka@example.jp", result.Eメール) 81 | require.Equal(t, 25, result.A年齢) 82 | t.Log("Japanese record find result:", neatjsons.S(result)) 83 | }) 84 | 85 | // Test 2: Find multiple records using Japanese fields 86 | t.Run("Japanese fields multiple find", func(t *testing.T) { 87 | results, err := repo.With(ctx, caseDB).Find(func(db *gorm.DB, cls *models.TユーザーColumns) *gorm.DB { 88 | return db.Where(cls.Sステータス.Eq("アクティブ")).Order(cls.A年齢.Ob("asc").Ox()) 89 | }) 90 | require.NoError(t, err) 91 | require.Len(t, results, 2) 92 | require.Equal(t, "田中太郎", results[0].Uユーザー名) // younger user first 93 | require.Equal(t, "佐藤花子", results[1].Uユーザー名) 94 | t.Log("Active records find result:", len(results), "items") 95 | }) 96 | 97 | // Test 3: Count statistics using Japanese fields 98 | t.Run("Japanese fields count stats", func(t *testing.T) { 99 | count, err := repo.With(ctx, caseDB).Count(func(db *gorm.DB, cls *models.TユーザーColumns) *gorm.DB { 100 | return db.Where(cls.A年齢.Gte(26)) 101 | }) 102 | require.NoError(t, err) 103 | require.Equal(t, int64(2), count) // 2 users with age >= 26 104 | t.Log("Records with age > 26:", count) 105 | }) 106 | 107 | // Test 4: Complex condition search using Japanese fields 108 | t.Run("Japanese fields complex search", func(t *testing.T) { 109 | results, err := repo.With(ctx, caseDB).Find(func(db *gorm.DB, cls *models.TユーザーColumns) *gorm.DB { 110 | return db.Where(cls.J住所.Like("%区%")). 111 | Where(cls.A年齢.Between(25, 30)). 112 | Where(cls.Eメール.Like("%@example.jp")) 113 | }) 114 | require.NoError(t, err) 115 | require.Len(t, results, 3) // all users match conditions 116 | t.Log("Complex search result:", len(results), "items") 117 | }) 118 | 119 | // Test 5: Update operation using Japanese fields 120 | t.Run("Japanese fields update operation", func(t *testing.T) { 121 | // Update Tanaka Taro's age 122 | err := repo.With(ctx, caseDB).Update( 123 | func(db *gorm.DB, cls *models.TユーザーColumns) *gorm.DB { 124 | return db.Where(cls.Uユーザー名.Eq("田中太郎")) 125 | }, 126 | func(cls *models.TユーザーColumns) (string, interface{}) { 127 | return cls.A年齢.Kv(26) 128 | }, 129 | ) 130 | require.NoError(t, err) 131 | t.Log("Successfully updated Tanaka Taro's age") 132 | 133 | // Verify update result 134 | updated, err := repo.With(ctx, caseDB).First(func(db *gorm.DB, cls *models.TユーザーColumns) *gorm.DB { 135 | return db.Where(cls.Uユーザー名.Eq("田中太郎")) 136 | }) 137 | require.NoError(t, err) 138 | require.Equal(t, 26, updated.A年齢) 139 | t.Log("Updated Tanaka Taro info:", neatjsons.S(updated)) 140 | }) 141 | 142 | // Test 6: Batch update using Japanese fields 143 | t.Run("Japanese fields batch update", func(t *testing.T) { 144 | // Batch update inactive record status 145 | err := repo.With(ctx, caseDB).Updates( 146 | func(db *gorm.DB, cls *models.TユーザーColumns) *gorm.DB { 147 | return db.Where(cls.Sステータス.Eq("非アクティブ")) 148 | }, 149 | func(cls *models.TユーザーColumns) map[string]interface{} { 150 | return cls. 151 | Kw(cls.Sステータス.Kv("休眠")). 152 | Kw(cls.J住所.Kv("未確認")). 153 | AsMap() 154 | }, 155 | ) 156 | require.NoError(t, err) 157 | t.Log("Successfully batch updated inactive record status") 158 | }) 159 | 160 | // Test 7: Existence check using Japanese fields 161 | t.Run("Japanese fields existence check", func(t *testing.T) { 162 | // Check if records with age > 35 exist 163 | exists, err := repo.With(ctx, caseDB).Exist(func(db *gorm.DB, cls *models.TユーザーColumns) *gorm.DB { 164 | return db.Where(cls.A年齢.Gt(35)) 165 | }) 166 | require.NoError(t, err) 167 | require.False(t, exists) // should not exist 168 | t.Log("Records with age > 35 exist:", exists) 169 | 170 | // Check if Tokyo records exist 171 | exists, err = repo.With(ctx, caseDB).Exist(func(db *gorm.DB, cls *models.TユーザーColumns) *gorm.DB { 172 | return db.Where(cls.J住所.Like("東京%")) 173 | }) 174 | require.NoError(t, err) 175 | require.True(t, exists) // should exist 176 | t.Log("Tokyo records exist:", exists) 177 | }) 178 | 179 | // Test 8: Validate documentation examples 180 | t.Run("Validate documentation Japanese examples", func(t *testing.T) { 181 | // Verify Japanese field names can correctly map to database columns 182 | cols := (&models.Tユーザー{}).Columns() 183 | 184 | t.Log("Japanese field mapping validation:") 185 | t.Logf("Uユーザー名 -> %s", cols.Uユーザー名.Name()) 186 | t.Logf("Eメール -> %s", cols.Eメール.Name()) 187 | t.Logf("A年齢 -> %s", cols.A年齢.Name()) 188 | t.Logf("Sステータス -> %s", cols.Sステータス.Name()) 189 | 190 | // Ensure all fields have correct column name mapping 191 | require.NotEmpty(t, cols.Uユーザー名.Name()) 192 | require.NotEmpty(t, cols.Eメール.Name()) 193 | require.NotEmpty(t, cols.A年齢.Name()) 194 | require.NotEmpty(t, cols.Sステータス.Name()) 195 | 196 | t.Log("✅ Documentation Japanese examples validated successfully!") 197 | }) 198 | } 199 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/brianvoe/gofakeit/v7 v7.12.1 h1:df1tiI4SL1dR5Ix4D/r6a3a+nXBJ/OBGU5jEKRBmmqg= 2 | github.com/brianvoe/gofakeit/v7 v7.12.1/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= 3 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 4 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU= 6 | github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A= 7 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 8 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 9 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 10 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 11 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 12 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 13 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 14 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 15 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 16 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 17 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 18 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 19 | github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= 20 | github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 21 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 22 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 23 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 24 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 25 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 26 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 27 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 28 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 29 | github.com/yyle88/done v1.0.28 h1:ZlC5ENTHAR0CQm19t1WhpbtKsKNPwsrXRtDewFsq4HA= 30 | github.com/yyle88/done v1.0.28/go.mod h1:dc0SzvQkX4NLEIz2shgYvETprQ6c0VZb+DCDtIi9n2Q= 31 | github.com/yyle88/erero v1.0.24 h1:yroawlW4IohY4bK4SonMBNI2tlZftPjtfhYYBtBfCxw= 32 | github.com/yyle88/erero v1.0.24/go.mod h1:KoTJmpyuKtQddt1QEQidwmewilCWwEH8rn5NsIAZ8XE= 33 | github.com/yyle88/formatgo v1.0.28 h1:xetQgoIpSnUkgL3EkPx3AP1Tl0uS/1d03NhmcFrWcqo= 34 | github.com/yyle88/formatgo v1.0.28/go.mod h1:3IwR7CXcv1UcZm6tLpaG/tCB4tXf5UktXS810zaPj7c= 35 | github.com/yyle88/gormcngen v1.0.49 h1:zcv5g1MQPGiILGv966Xx8bxKcT0L+Ttp2gsjL3xEpr4= 36 | github.com/yyle88/gormcngen v1.0.49/go.mod h1:DyKnpLHttdfJEL9AnXutpdPfq+Es6eUxjreM7z8Hhr8= 37 | github.com/yyle88/gormcnm v1.0.60 h1:akNjjvQXN4jE6ZZddC8cX6wj3xlSjMXIRJ2tZhoRxj4= 38 | github.com/yyle88/gormcnm v1.0.60/go.mod h1:8dPXfBA8+4d95EZugp+hrIZscGljaLWOM8rXTsYwlck= 39 | github.com/yyle88/gormrepo v1.0.59 h1:DNaxq7FM68TTKpmtH2M4pgYUjstaB0RRLrHjZz4qvkY= 40 | github.com/yyle88/gormrepo v1.0.59/go.mod h1:eKV5NSdtjMPsqdbk1dgOcDkIpsWl1/AcMQGMwN8VTvU= 41 | github.com/yyle88/must v0.0.29 h1:hEcuuWSkpFB97gkcrXHSfjxGSkYiv8J6wy18yWAUuyw= 42 | github.com/yyle88/must v0.0.29/go.mod h1:zdbS6m5NzOhEqsO45wOaxDdQsh5YT3Tyz0DlY4rBpko= 43 | github.com/yyle88/mutexmap v1.0.15 h1:vqwtvomfzddcuBNg8hofWnILRFK2STJhWU7AufuNS50= 44 | github.com/yyle88/mutexmap v1.0.15/go.mod h1:NqwsKlK+NkL18i4BepeyCgtenXuw4N5UUnEX9XBfPA8= 45 | github.com/yyle88/neatjson v0.0.13 h1:+1Ihb43IZLkYAd+lapnvUJN20bTjSAkoTE/2gssBew8= 46 | github.com/yyle88/neatjson v0.0.13/go.mod h1:BOgA69f27Bd/yj2xnIWldratF3WwIrMKrBgo9eIZGb0= 47 | github.com/yyle88/osexistpath v0.0.18 h1:mBi01278glkoTlPE3xTbO5/GT9iGEowOZ5pL/29qas0= 48 | github.com/yyle88/osexistpath v0.0.18/go.mod h1:iTuJ9S07VIiq2azsSYUiheZtDXKlU819v9YAuSL40Uk= 49 | github.com/yyle88/printgo v1.0.6 h1:b53uyCdlijObvuyHVECiiEWQIDfuOpuHVrkNQIdR5bU= 50 | github.com/yyle88/printgo v1.0.6/go.mod h1:14qsuuTovdfgHtle0e4ln6zci47Xtkdx9CCkUo3vxoc= 51 | github.com/yyle88/rese v0.0.12 h1:3cbPm5XmqPiRK2yj+nAUl50ci+9t8pEYOAVp+cKOX8w= 52 | github.com/yyle88/rese v0.0.12/go.mod h1:FGfU5brwe1PcyRobQh40/9gse51QVfJOLmBV/0DXSfA= 53 | github.com/yyle88/runpath v1.0.25 h1:WfeVd/7pLxpCvMhrLptKYNaplApJldveY9O7un67d3U= 54 | github.com/yyle88/runpath v1.0.25/go.mod h1:Iv9y6waZR95gUMMsK96WNAbF3wknfoPUd7xuQf/+CNY= 55 | github.com/yyle88/sortx v1.0.11 h1:zUoviJVkrp5daRssn2Q9fi+8rhVaLTrNIM+n7dgucSw= 56 | github.com/yyle88/sortx v1.0.11/go.mod h1:vVwHKufNGCoi9DQo2BBGmBcsvhcOyWWpkUWDxPQ1l90= 57 | github.com/yyle88/sure v0.0.42 h1:dsA7p/46AR9vanEPWYLJrlXEmwb+E/DePtImKzlKaBc= 58 | github.com/yyle88/sure v0.0.42/go.mod h1:G1YnfMptgQkkM+rOXoiAnFYt78z0fZMV3NTwHXfaCJU= 59 | github.com/yyle88/syntaxgo v0.0.54 h1:CBQA4HqLgo5Hoze/PDUsnDJ8J+raoYLa1OTH+sQtFwY= 60 | github.com/yyle88/syntaxgo v0.0.54/go.mod h1:3hNzb1C8zfk527A45181sFB/3gCl1hgIcDyTC1UqKzk= 61 | github.com/yyle88/tern v0.0.10 h1:2MvDoVyfIeEzskMpLE6B07dtRpSqI6LaeUD/mGuLW7o= 62 | github.com/yyle88/tern v0.0.10/go.mod h1:OHHE2G1gYaX4q0uu3sG9JAK9dBjHboxGcTXbRPVoGeQ= 63 | github.com/yyle88/zaplog v0.0.27 h1:Bd/XWeAeRDEsFdtHphEqPK+W3M9WNd/dzf5x6YXeSkY= 64 | github.com/yyle88/zaplog v0.0.27/go.mod h1:0BOxIR1lFh4vdiCyR5zuj4DmTFK36FbpjOWAdjMwSDU= 65 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 66 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 67 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 68 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 69 | go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= 70 | go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 71 | golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM= 72 | golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= 73 | golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= 74 | golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= 75 | golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= 76 | golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 77 | golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= 78 | golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 79 | golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= 80 | golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 81 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 82 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 83 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 84 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 85 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 86 | gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= 87 | gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= 88 | gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= 89 | gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= 90 | -------------------------------------------------------------------------------- /gen_index.go: -------------------------------------------------------------------------------- 1 | package gormmom 2 | 3 | import ( 4 | "fmt" 5 | "unicode/utf8" 6 | 7 | "github.com/yyle88/gormmom/gormidxname" 8 | "github.com/yyle88/must" 9 | "github.com/yyle88/syntaxgo/syntaxgo_tag" 10 | "github.com/yyle88/zaplog" 11 | "go.uber.org/zap" 12 | "gorm.io/gorm/schema" 13 | ) 14 | 15 | // validateSingleColumnIndex validates single column index naming compliance 16 | // Logs debug information with single column index validation process 17 | // 18 | // validateSingleColumnIndex 验证单列索引命名的符合性 19 | // 为单列索引验证过程记录调试信息 20 | func (cfg *Config) validateSingleColumnIndex(indexName string, fieldName string) { 21 | zaplog.LOG.Debug("validate-single-column-index", zap.String("index_name", indexName), zap.String("field_name", fieldName)) 22 | } 23 | 24 | // validateCompositeIndex validates composite index naming compliance 25 | // Logs debug information with composite index validation and field count 26 | // 27 | // validateCompositeIndex 验证复合索引命名的符合性 28 | // 为复合索引验证记录调试信息和字段数量 29 | func (cfg *Config) validateCompositeIndex(indexName string, fields []schema.IndexOption) { 30 | zaplog.LOG.Debug("validate-composite-index", zap.String("index_name", indexName), zap.Int("field_size", len(fields))) 31 | } 32 | 33 | // correctIndexNames processes and corrects index names based on tag modifications 34 | // Analyzes GORM schema indexes and applies naming corrections for single column indexes 35 | // Validates composite indexes and ensures consistent naming patterns across the schema 36 | // 37 | // correctIndexNames 基于标签修改处理和纠正索引名 38 | // 分析 GORM 模式索引并对单列索引应用命名纠正 39 | // 验证复合索引并确保整个模式中的一致命名模式 40 | func (cfg *Config) correctIndexNames(modifications []*defineTagModification) { 41 | var mapTagModifications = make(map[string]*defineTagModification, len(modifications)) 42 | for _, step := range modifications { 43 | mapTagModifications[step.structFieldName] = step 44 | } 45 | 46 | schemaIndexes := cfg.structI.gormSchema.ParseIndexes() 47 | zaplog.LOG.Debug("check_indexes", zap.String("object_class", cfg.structI.gormSchema.Name), zap.String("table_name", cfg.structI.gormSchema.Table), zap.Int("index_count", len(schemaIndexes))) 48 | for idx, node := range schemaIndexes { 49 | zaplog.LOG.Debug("foreach_index", zap.String("index_desc", fmt.Sprintf("(%d/%d)", idx, len(schemaIndexes)))) 50 | zaplog.LOG.Debug("check_a_index", zap.String("index_name", node.Name), zap.Int("field_size", len(node.Fields))) 51 | if len(node.Fields) == 1 { //只检查单列索引,因为复合索引就得手写名称,因此没有问题 52 | item, ok := mapTagModifications[node.Fields[0].Name] 53 | if ok { 54 | cfg.rewriteSingleColumnIndex(node, item) 55 | } else { 56 | cfg.validateSingleColumnIndex(node.Name, node.Fields[0].Name) 57 | } 58 | } else { 59 | cfg.validateCompositeIndex(node.Name, node.Fields) 60 | } 61 | } 62 | } 63 | 64 | // rewriteSingleColumnIndex rewrites single column index with pattern-based naming 65 | // Generates new index names using configured naming strategies and pattern validation 66 | // Updates GORM tags with appropriate index names and pattern specifications 67 | // 68 | // rewriteSingleColumnIndex 使用基于模式的命名重写单列索引 69 | // 使用配置的命名策略和模式验证生成新的索引名 70 | // 使用适当的索引名和模式规范更新 GORM 标签 71 | func (cfg *Config) rewriteSingleColumnIndex(schemaIndex *schema.Index, modification *defineTagModification) { 72 | zaplog.LOG.Debug("rewrite_single_column_index", zap.String("table_name", cfg.structI.gormSchema.Table), zap.String("field_name", modification.structFieldName), zap.String("index_name", schemaIndex.Name), zap.String("index_class", schemaIndex.Class)) 73 | 74 | columnName := must.Nice(modification.columnName) 75 | zaplog.LOG.Debug("new_column_name", zap.String("name", modification.structFieldName), zap.String("new_column_name", columnName)) 76 | 77 | //这个是规则的枚举名称 78 | var patternTagName gormidxname.IndexPatternTagEnum 79 | switch schemaIndex.Class { 80 | case "": 81 | patternTagName = gormidxname.IdxPatternTagName 82 | case "UNIQUE": 83 | patternTagName = gormidxname.UdxPatternTagName 84 | default: 85 | patternTagName = "" 86 | } 87 | 88 | defaultPattern := cfg.options.indexNamingStrategies.GetDefault() 89 | 90 | var patternEnum gormidxname.PatternEnum 91 | if patternTagName != "" { 92 | exist := false 93 | patternEnum, exist = cfg.resolveIndexPattern(modification, patternTagName) 94 | if !exist { 95 | patternEnum = defaultPattern.GetPatternEnum() 96 | 97 | match := defaultPattern.CheckIndexName(schemaIndex.Name) 98 | zaplog.LOG.Debug("check_idx_match", zap.Bool("match", match)) 99 | if !match { 100 | //当没有配置规则,而默认规则检查不正确时,就需要把规则名设置到标签里 101 | modification.newTagCode = syntaxgo_tag.SetTagFieldValue(modification.newTagCode, cfg.options.systemTagName, string(patternTagName), string(patternEnum), syntaxgo_tag.INSERT_LOCATION_END) 102 | } else { 103 | //当没有配置规则,而且能够满足检查时,就不做任何事情(不要破坏用户自己配置的正确索引名) 104 | return 105 | } 106 | } 107 | } else { 108 | //就是不确定时 使用默认值 的情况 109 | patternEnum = defaultPattern.GetPatternEnum() 110 | } 111 | 112 | pattern := cfg.options.indexNamingStrategies.GetPattern(patternEnum) 113 | 114 | indexNameResult := must.Nice(pattern.BuildIndexName(schemaIndex, &gormidxname.BuildIndexParam{ 115 | TableName: cfg.structI.gormSchema.Table, 116 | FieldName: modification.structFieldName, 117 | ColumnName: columnName, 118 | })) 119 | if indexNameResult.NewIndexName == "" { 120 | return 121 | } 122 | if indexNameResult.TagFieldName == "" { 123 | return 124 | } 125 | zaplog.LOG.Debug("compare", zap.String("which_enum_code_name", string(patternTagName)), zap.String("enum_code_name", string(indexNameResult.IdxUdxPrefix))) 126 | must.Equals(patternTagName, indexNameResult.IdxUdxPrefix) 127 | 128 | zaplog.LOG.Debug("new_index_name", zap.String("new_index_name", indexNameResult.NewIndexName), zap.String("old_index_name", schemaIndex.Name)) 129 | if indexNameResult.NewIndexName == schemaIndex.Name && !cfg.hasOneIdxTagUdxTagValue(modification.newTagCode, patternTagName) { 130 | return 131 | } 132 | 133 | zaplog.LOG.Debug("tag_field_name", zap.String("tag_field_name", indexNameResult.TagFieldName)) 134 | 135 | gormTagContent, stx, etx := syntaxgo_tag.ExtractTagValueIndex(modification.newTagCode, "gorm") 136 | must.TRUE(stx >= 0) 137 | must.TRUE(etx >= 0) 138 | must.Nice(gormTagContent) //就是排除 gorm: 以后得到的双引号里面的内容 139 | zaplog.LOG.Debug("gorm_tag_content", zap.String("gorm_tag_content", gormTagContent)) 140 | 141 | var changed = false 142 | //假如连 UTF-8 编码 都不满足,就说明这个索引名是完全错误的 143 | if utf8.ValidString(schemaIndex.Name) { 144 | zaplog.LOG.Debug("schema_index_name", zap.String("tag_field_name", indexNameResult.TagFieldName), zap.String("name", schemaIndex.Name)) 145 | //因为这个正则不能匹配非 UTF-8 编码,在前面先判断编码是否正确,编码正确以后再匹配索引名 146 | sfx, efx := syntaxgo_tag.ExtractFieldEqualsValueIndex(gormTagContent, indexNameResult.TagFieldName, schemaIndex.Name) 147 | if sfx > 0 && efx > 0 { 148 | spx := stx + sfx //把起点坐标补上前面的 149 | epx := stx + efx 150 | modification.newTagCode = modification.newTagCode[:spx] + indexNameResult.NewIndexName + modification.newTagCode[epx:] 151 | changed = true 152 | } 153 | zaplog.LOG.Debug("check_tag_index", zap.Int("sfx", sfx), zap.Int("efx", efx), zap.Bool("changed", changed)) 154 | } 155 | if !changed { 156 | zaplog.LOG.Debug("schema_index_name", zap.String("tag_field_name", indexNameResult.TagFieldName), zap.String("name", schemaIndex.Name)) 157 | sfx, efx := syntaxgo_tag.ExtractNoValueFieldNameIndex(gormTagContent, indexNameResult.TagFieldName) 158 | if sfx >= 0 && efx >= 0 { 159 | spx := stx + sfx //把起点坐标补上前面的 160 | epx := stx + efx 161 | modification.newTagCode = modification.newTagCode[:spx] + indexNameResult.TagFieldName + ":" + indexNameResult.NewIndexName + modification.newTagCode[epx:] 162 | changed = true 163 | } 164 | zaplog.LOG.Debug("check_tag_index", zap.Int("sfx", sfx), zap.Int("efx", efx), zap.Bool("changed", changed)) 165 | } 166 | if !changed { 167 | zaplog.LOG.Debug("not_change_tag", zap.String("not_change_tag", modification.newTagCode)) 168 | } 169 | 170 | zaplog.LOG.Debug("new_tag_string", zap.String("new_tag_string", modification.newTagCode)) 171 | } 172 | 173 | // resolveIndexPattern resolves index pattern from tag configuration 174 | // Extracts pattern enum from system tag or returns default pattern if not found 175 | // Returns the resolved pattern enum and existence flag for pattern validation 176 | // 177 | // resolveIndexPattern 从标签配置中解析索引模式 178 | // 从系统标签中提取模式枚举,如果找不到则返回默认模式 179 | // 返回解析的模式枚举和用于模式验证的存在标志 180 | func (cfg *Config) resolveIndexPattern(modification *defineTagModification, patternTagName gormidxname.IndexPatternTagEnum) (gormidxname.PatternEnum, bool) { 181 | var name = cfg.extractTagFieldGetValue(modification.newTagCode, cfg.options.systemTagName, string(patternTagName)) 182 | if name == "" { 183 | defaultPattern := cfg.options.indexNamingStrategies.GetDefault() 184 | return defaultPattern.GetPatternEnum(), false 185 | } 186 | zaplog.LOG.Debug("resolve-index-pattern", zap.String("index_name_pattern", name)) 187 | return gormidxname.PatternEnum(name), true 188 | } 189 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | [![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/yyle88/gormmom/release.yml?branch=main&label=BUILD)](https://github.com/yyle88/gormmom/actions/workflows/release.yml?query=branch%3Amain) 2 | [![GoDoc](https://pkg.go.dev/badge/github.com/yyle88/gormmom)](https://pkg.go.dev/github.com/yyle88/gormmom) 3 | [![Coverage Status](https://img.shields.io/coveralls/github/yyle88/gormmom/main.svg)](https://coveralls.io/github/yyle88/gormmom?branch=main) 4 | [![Supported Go Versions](https://img.shields.io/badge/Go-1.24+-lightgrey.svg)](https://go.dev/) 5 | [![GitHub Release](https://img.shields.io/github/release/yyle88/gormmom.svg)](https://github.com/yyle88/gormmom/releases) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/yyle88/gormmom)](https://goreportcard.com/report/github.com/yyle88/gormmom) 7 | 8 | # 🌍 GORMMOM - GORM 原生语言编程革命 9 | 10 | **gormmom** 是 **原生语言编程引擎**,打破数据库开发中的语言壁垒。作为 GORM 生态系统的 **智能标签生成引擎**,它赋能全球团队使用原生语言编写数据库模型,同时自动生成数据库兼容的 GORM 标签和列名。 11 | 12 | > 🎯 **语言解放**: 用中文、阿拉伯语、日语和各种语言编程 - gormmom 架起人类表达与数据库需求之间的桥梁。 13 | 14 | --- 15 | 16 | ## 生态系统 17 | 18 | ![GORM Type-Safe Ecosystem](https://github.com/yyle88/gormcnm/raw/main/assets/gormcnm-ecosystem.svg) 19 | 20 | --- 21 | 22 | 23 | 24 | ## 英文文档 25 | 26 | [ENGLISH README](README.md) 27 | 28 | 29 | --- 30 | 31 | ## 🚀 安装 32 | 33 | ```bash 34 | go get github.com/yyle88/gormmom 35 | ``` 36 | 37 | --- 38 | 39 | ## 🔄 技术对比 40 | 41 | | 生态系统 | Java MyBatis Plus | Python SQLAlchemy | Go GORM 生态系统 | 42 | |-----------|--------------------|-------------------|-----------------| 43 | | **类型安全列** | `Example::getName` | `Example.name` | `cls.Name.Eq()` | 44 | | **代码生成** | ✅ 插件支持 | ✅ 反射机制 | ✅ AST 精度 | 45 | | **仓储模式** | ✅ BaseMapper | ✅ Session API | ✅ GormRepo | 46 | | **原生语言** | 🟡 有限支持 | 🟡 有限支持 | ✅ 完整支持 | 47 | 48 | --- 49 | 50 | ## 🌟 问题与解决方案 51 | 52 | ### ⚡ 标准方法 53 | ```go 54 | // ❌ 常见方法:开发者被限制在英语命名 55 | type Account struct { 56 | ID uint `gorm:"primaryKey"` 57 | Username string `gorm:"column:username;uniqueIndex"` 58 | Nickname string `gorm:"column:nickname;index"` 59 | Age int `gorm:"column:age"` 60 | PhoneNum string `gorm:"column:phone_num"` 61 | Mailbox string `gorm:"column:mailbox"` 62 | Address string `gorm:"column:address"` 63 | Status string `gorm:"column:status;index"` 64 | } 65 | ``` 66 | 67 | ### ✅ GORMMOM 解决方案 68 | ```go 69 | // ✅ GORMMOM: 用原生语言编程! 70 | type T账户信息 struct { 71 | ID uint `gorm:"primaryKey"` 72 | Z账号 string `gorm:"uniqueIndex"` 73 | N昵称 string `gorm:"index"` 74 | A年龄 int `gorm:""` 75 | D电话 string `gorm:""` 76 | E邮箱 string `gorm:""` 77 | J住址 string `gorm:""` 78 | S状态 string `gorm:"index"` 79 | } 80 | 81 | func (*T账户信息) TableName() string { 82 | return "accounts" // 数据库兼容的表名 83 | } 84 | ``` 85 | 86 | --- 87 | 88 | ## 🌍 多语言示例 89 | 90 | ### 繁體中文 91 | ```go 92 | type T賬戶信息 struct { 93 | ID uint `gorm:"primaryKey"` 94 | Z賬號 string `gorm:"uniqueIndex"` 95 | N暱稱 string `gorm:"index"` 96 | A年齡 int `gorm:""` 97 | D電話 string `gorm:""` 98 | E郵箱 string `gorm:""` 99 | J住址 string `gorm:""` 100 | S狀態 string `gorm:"index"` 101 | } 102 | 103 | func (*T賬戶信息) TableName() string { 104 | return "accounts" 105 | } 106 | ``` 107 | 108 | ### 日本語 109 | ```go 110 | type Tアカウント情報 struct { 111 | ID uint `gorm:"primaryKey"` 112 | Aアカウント string `gorm:"uniqueIndex"` 113 | Nニックネーム string `gorm:"index"` 114 | N年齢 int `gorm:""` 115 | D電話番号 string `gorm:""` 116 | Eメール string `gorm:""` 117 | J住所 string `gorm:""` 118 | Sステータス string `gorm:"index"` 119 | } 120 | 121 | func (*Tアカウント情報) TableName() string { 122 | return "accounts" 123 | } 124 | ``` 125 | 126 | ### 한국어 127 | ```go 128 | type T계정정보 struct { 129 | ID uint `gorm:"primaryKey"` 130 | G계정 string `gorm:"uniqueIndex"` 131 | N닉네임 string `gorm:"index"` 132 | N나이 int `gorm:""` 133 | J전화번호 string `gorm:""` 134 | E이메일 string `gorm:""` 135 | J주소 string `gorm:""` 136 | S상태 string `gorm:"index"` 137 | } 138 | 139 | func (*T계정정보) TableName() string { 140 | return "accounts" 141 | } 142 | ``` 143 | 144 | --- 145 | 146 | ## 🛠️ 使用方法 147 | 148 | ### 1. 自动标签生成 149 | 150 | gormmom 执行后,结构体获得数据库兼容的列标签: 151 | 152 | ```go 153 | // 生成的数据库兼容列名 154 | type T账户信息 struct { 155 | ID uint `gorm:"primaryKey"` 156 | Z账号 string `gorm:"column:z_zhang_hao;uniqueIndex"` 157 | N昵称 string `gorm:"column:n_ni_cheng;index"` 158 | A年龄 int `gorm:"column:a_nian_ling"` 159 | D电话 string `gorm:"column:d_dian_hua"` 160 | E邮箱 string `gorm:"column:e_you_xiang"` 161 | J住址 string `gorm:"column:j_zhu_zhi"` 162 | S状态 string `gorm:"column:s_zhuang_tai;index"` 163 | } 164 | ``` 165 | 166 | ### 2. 生成命令 167 | 168 | ```bash 169 | # 步骤 1:生成原生语言字段的 GORM 标签 170 | go test -v -run TestGen/GenGormMom 171 | 172 | # 步骤 2:生成类型安全列方法(配合 gormcngen) 173 | go test -v -run TestGen/GenGormCnm 174 | ``` 175 | 176 | ### 3. 配合 gormrepo 使用 177 | 178 | **English Version:** 179 | 180 | ```go 181 | // Create repo 182 | repo := gormrepo.NewGormRepo(&Account{}, (&Account{}).Columns()) 183 | 184 | // Select - First (by username) 185 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *AccountColumns) *gorm.DB { 186 | return db.Where(cls.Username.Eq("alice")) 187 | }) 188 | 189 | // Select - First (by nickname) 190 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *AccountColumns) *gorm.DB { 191 | return db.Where(cls.Nickname.Eq("Alice")) 192 | }) 193 | 194 | // Select - Find 195 | accounts, err := repo.With(ctx, db).Find(func(db *gorm.DB, cls *AccountColumns) *gorm.DB { 196 | return db.Where(cls.Age.Gte(18)) 197 | }) 198 | 199 | // Select - FindPage 200 | accounts, err := repo.With(ctx, db).FindPage( 201 | func(db *gorm.DB, cls *AccountColumns) *gorm.DB { 202 | return db.Where(cls.Age.Gte(18)) 203 | }, 204 | func(cls *AccountColumns) gormcnm.OrderByBottle { 205 | return cls.ID.OrderByBottle("DESC") 206 | }, 207 | &gormrepo.Pagination{Limit: 10, Offset: 0}, 208 | ) 209 | 210 | // Create 211 | err := repo.With(ctx, db).Create(&Account{Username: "bob", Nickname: "Bob", Age: 25}) 212 | 213 | // Update 214 | err := repo.With(ctx, db).Updates( 215 | func(db *gorm.DB, cls *AccountColumns) *gorm.DB { 216 | return db.Where(cls.ID.Eq(1)) 217 | }, 218 | func(cls *AccountColumns) map[string]interface{} { 219 | return cls.Kw(cls.Age.Kv(26)).AsMap() 220 | }, 221 | ) 222 | 223 | // Delete 224 | err := repo.With(ctx, db).DeleteW(func(db *gorm.DB, cls *AccountColumns) *gorm.DB { 225 | return db.Where(cls.ID.Eq(1)) 226 | }) 227 | ``` 228 | 229 | **中文(简体)版本:** 230 | 231 | ```go 232 | // Create repo 233 | repo := gormrepo.NewGormRepo(&T账户信息{}, (&T账户信息{}).Columns()) 234 | 235 | // Select - First (by username) 236 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *T账户信息Columns) *gorm.DB { 237 | return db.Where(cls.Z账号.Eq("wang-xiao-ming")) 238 | }) 239 | 240 | // Select - First (by nickname) 241 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *T账户信息Columns) *gorm.DB { 242 | return db.Where(cls.N昵称.Eq("王小明")) 243 | }) 244 | 245 | // Select - Find 246 | accounts, err := repo.With(ctx, db).Find(func(db *gorm.DB, cls *T账户信息Columns) *gorm.DB { 247 | return db.Where(cls.A年龄.Gte(18)) 248 | }) 249 | 250 | // Select - FindPage 251 | accounts, err := repo.With(ctx, db).FindPage( 252 | func(db *gorm.DB, cls *T账户信息Columns) *gorm.DB { 253 | return db.Where(cls.A年龄.Gte(18)) 254 | }, 255 | func(cls *T账户信息Columns) gormcnm.OrderByBottle { 256 | return cls.ID.OrderByBottle("DESC") 257 | }, 258 | &gormrepo.Pagination{Limit: 10, Offset: 0}, 259 | ) 260 | 261 | // Create 262 | err := repo.With(ctx, db).Create(&T账户信息{Z账号: "han-mei-mei", N昵称: "韩梅梅", A年龄: 25}) 263 | 264 | // Update 265 | err := repo.With(ctx, db).Updates( 266 | func(db *gorm.DB, cls *T账户信息Columns) *gorm.DB { 267 | return db.Where(cls.ID.Eq(1)) 268 | }, 269 | func(cls *T账户信息Columns) map[string]interface{} { 270 | return cls.Kw(cls.A年龄.Kv(26)).AsMap() 271 | }, 272 | ) 273 | 274 | // Delete 275 | err := repo.With(ctx, db).DeleteW(func(db *gorm.DB, cls *T账户信息Columns) *gorm.DB { 276 | return db.Where(cls.ID.Eq(1)) 277 | }) 278 | ``` 279 | 280 | **中文(繁體)版本:** 281 | 282 | ```go 283 | // Create repo 284 | repo := gormrepo.NewGormRepo(&T賬戶信息{}, (&T賬戶信息{}).Columns()) 285 | 286 | // Select - First (by username) 287 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *T賬戶信息Columns) *gorm.DB { 288 | return db.Where(cls.Z賬號.Eq("wang-xiao-ming")) 289 | }) 290 | 291 | // Select - First (by nickname) 292 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *T賬戶信息Columns) *gorm.DB { 293 | return db.Where(cls.N暱稱.Eq("王小明")) 294 | }) 295 | 296 | // Select - Find 297 | accounts, err := repo.With(ctx, db).Find(func(db *gorm.DB, cls *T賬戶信息Columns) *gorm.DB { 298 | return db.Where(cls.A年齡.Gte(18)) 299 | }) 300 | 301 | // Select - FindPage 302 | accounts, err := repo.With(ctx, db).FindPage( 303 | func(db *gorm.DB, cls *T賬戶信息Columns) *gorm.DB { 304 | return db.Where(cls.A年齡.Gte(18)) 305 | }, 306 | func(cls *T賬戶信息Columns) gormcnm.OrderByBottle { 307 | return cls.ID.OrderByBottle("DESC") 308 | }, 309 | &gormrepo.Pagination{Limit: 10, Offset: 0}, 310 | ) 311 | 312 | // Create 313 | err := repo.With(ctx, db).Create(&T賬戶信息{Z賬號: "han-mei-mei", N暱稱: "韓梅梅", A年齡: 25}) 314 | 315 | // Update 316 | err := repo.With(ctx, db).Updates( 317 | func(db *gorm.DB, cls *T賬戶信息Columns) *gorm.DB { 318 | return db.Where(cls.ID.Eq(1)) 319 | }, 320 | func(cls *T賬戶信息Columns) map[string]interface{} { 321 | return cls.Kw(cls.A年齡.Kv(26)).AsMap() 322 | }, 323 | ) 324 | 325 | // Delete 326 | err := repo.With(ctx, db).DeleteW(func(db *gorm.DB, cls *T賬戶信息Columns) *gorm.DB { 327 | return db.Where(cls.ID.Eq(1)) 328 | }) 329 | ``` 330 | 331 | **日本語版:** 332 | 333 | ```go 334 | // Create repo 335 | repo := gormrepo.NewGormRepo(&Tアカウント情報{}, (&Tアカウント情報{}).Columns()) 336 | 337 | // Select - First (by username) 338 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *Tアカウント情報Columns) *gorm.DB { 339 | return db.Where(cls.Aアカウント.Eq("tanaka")) 340 | }) 341 | 342 | // Select - First (by nickname) 343 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *Tアカウント情報Columns) *gorm.DB { 344 | return db.Where(cls.Nニックネーム.Eq("田中太郎")) 345 | }) 346 | 347 | // Select - Find 348 | accounts, err := repo.With(ctx, db).Find(func(db *gorm.DB, cls *Tアカウント情報Columns) *gorm.DB { 349 | return db.Where(cls.N年齢.Gte(18)) 350 | }) 351 | 352 | // Select - FindPage 353 | accounts, err := repo.With(ctx, db).FindPage( 354 | func(db *gorm.DB, cls *Tアカウント情報Columns) *gorm.DB { 355 | return db.Where(cls.N年齢.Gte(18)) 356 | }, 357 | func(cls *Tアカウント情報Columns) gormcnm.OrderByBottle { 358 | return cls.ID.OrderByBottle("DESC") 359 | }, 360 | &gormrepo.Pagination{Limit: 10, Offset: 0}, 361 | ) 362 | 363 | // Create 364 | err := repo.With(ctx, db).Create(&Tアカウント情報{Aアカウント: "suzuki", Nニックネーム: "鈴木花子", N年齢: 25}) 365 | 366 | // Update 367 | err := repo.With(ctx, db).Updates( 368 | func(db *gorm.DB, cls *Tアカウント情報Columns) *gorm.DB { 369 | return db.Where(cls.ID.Eq(1)) 370 | }, 371 | func(cls *Tアカウント情報Columns) map[string]interface{} { 372 | return cls.Kw(cls.N年齢.Kv(26)).AsMap() 373 | }, 374 | ) 375 | 376 | // Delete 377 | err := repo.With(ctx, db).DeleteW(func(db *gorm.DB, cls *Tアカウント情報Columns) *gorm.DB { 378 | return db.Where(cls.ID.Eq(1)) 379 | }) 380 | ``` 381 | 382 | **한국어판:** 383 | 384 | ```go 385 | // Create repo 386 | repo := gormrepo.NewGormRepo(&T계정정보{}, (&T계정정보{}).Columns()) 387 | 388 | // Select - First (by username) 389 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *T계정정보Columns) *gorm.DB { 390 | return db.Where(cls.G계정.Eq("kim-cheol-su")) 391 | }) 392 | 393 | // Select - First (by nickname) 394 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *T계정정보Columns) *gorm.DB { 395 | return db.Where(cls.N닉네임.Eq("김철수")) 396 | }) 397 | 398 | // Select - Find 399 | accounts, err := repo.With(ctx, db).Find(func(db *gorm.DB, cls *T계정정보Columns) *gorm.DB { 400 | return db.Where(cls.N나이.Gte(18)) 401 | }) 402 | 403 | // Select - FindPage 404 | accounts, err := repo.With(ctx, db).FindPage( 405 | func(db *gorm.DB, cls *T계정정보Columns) *gorm.DB { 406 | return db.Where(cls.N나이.Gte(18)) 407 | }, 408 | func(cls *T계정정보Columns) gormcnm.OrderByBottle { 409 | return cls.ID.OrderByBottle("DESC") 410 | }, 411 | &gormrepo.Pagination{Limit: 10, Offset: 0}, 412 | ) 413 | 414 | // Create 415 | err := repo.With(ctx, db).Create(&T계정정보{G계정: "lee-young-hee", N닉네임: "이영희", N나이: 25}) 416 | 417 | // Update 418 | err := repo.With(ctx, db).Updates( 419 | func(db *gorm.DB, cls *T계정정보Columns) *gorm.DB { 420 | return db.Where(cls.ID.Eq(1)) 421 | }, 422 | func(cls *T계정정보Columns) map[string]interface{} { 423 | return cls.Kw(cls.N나이.Kv(26)).AsMap() 424 | }, 425 | ) 426 | 427 | // Delete 428 | err := repo.With(ctx, db).DeleteW(func(db *gorm.DB, cls *T계정정보Columns) *gorm.DB { 429 | return db.Where(cls.ID.Eq(1)) 430 | }) 431 | ``` 432 | 433 | ## 📝 完整示例 434 | 435 | 查看 [examples](internal/examples/) 目录获取完整集成示例。 436 | 437 | --- 438 | 439 | ## 关联项目 440 | 441 | 探索完整的 GORM 生态系统集成包: 442 | 443 | ### 核心生态 444 | 445 | - **[gormcnm](https://github.com/yyle88/gormcnm)** - GORM 基础层,提供类型安全的列操作和条件构建 446 | - **[gormcngen](https://github.com/yyle88/gormcngen)** - 使用 AST 的代码生成引擎,支持类型安全的 GORM 操作 447 | - **[gormrepo](https://github.com/yyle88/gormrepo)** - 仓储模式实现,遵循 GORM 最佳实践 448 | - **[gormmom](https://github.com/yyle88/gormmom)** - 原生语言 GORM 标签生成引擎,支持智能列名(本项目) 449 | - **[gormzhcn](https://github.com/go-zwbc/gormzhcn)** - 完整的 GORM 中文编程接口 450 | 451 | 每个包针对 GORM 开发的不同方面,包括本地化、类型安全操作和代码生成。 452 | 453 | --- 454 | 455 | 456 | 457 | 458 | ## 📄 许可证类型 459 | 460 | MIT 许可证 - 详见 [LICENSE](LICENSE)。 461 | 462 | --- 463 | 464 | ## 💬 联系与反馈 465 | 466 | 非常欢迎贡献代码!报告 BUG、建议功能、贡献代码: 467 | 468 | - 🐛 **问题报告?** 在 GitHub 上提交问题并附上重现步骤 469 | - 💡 **新颖思路?** 创建 issue 讨论 470 | - 📖 **文档疑惑?** 报告问题,帮助我们完善文档 471 | - 🚀 **需要功能?** 分享使用场景,帮助理解需求 472 | - ⚡ **性能瓶颈?** 报告慢操作,协助解决性能问题 473 | - 🔧 **配置困扰?** 询问复杂设置的相关问题 474 | - 📢 **关注进展?** 关注仓库以获取新版本和功能 475 | - 🌟 **成功案例?** 分享这个包如何改善工作流程 476 | - 💬 **反馈意见?** 欢迎提出建议和意见 477 | 478 | --- 479 | 480 | ## 🔧 代码贡献 481 | 482 | 新代码贡献,请遵循此流程: 483 | 484 | 1. **Fork**:在 GitHub 上 Fork 仓库(使用网页界面) 485 | 2. **克隆**:克隆 Fork 的项目(`git clone https://github.com/yourname/repo-name.git`) 486 | 3. **导航**:进入克隆的项目(`cd repo-name`) 487 | 4. **分支**:创建功能分支(`git checkout -b feature/xxx`) 488 | 5. **编码**:实现您的更改并编写全面的测试 489 | 6. **测试**:(Golang 项目)确保测试通过(`go test ./...`)并遵循 Go 代码风格约定 490 | 7. **文档**:面向用户的更改需要更新文档 491 | 8. **暂存**:暂存更改(`git add .`) 492 | 9. **提交**:提交更改(`git commit -m "Add feature xxx"`)确保向后兼容的代码 493 | 10. **推送**:推送到分支(`git push origin feature/xxx`) 494 | 11. **PR**:在 GitHub 上打开 Merge Request(在 GitHub 网页上)并提供详细描述 495 | 496 | 请确保测试通过并包含相关的文档更新。 497 | 498 | --- 499 | 500 | ## 🌟 项目支持 501 | 502 | 非常欢迎通过提交 Merge Request 和报告问题来贡献此项目。 503 | 504 | **项目支持:** 505 | 506 | - ⭐ **给予星标**如果项目对您有帮助 507 | - 🤝 **分享项目**给团队成员和(golang)编程朋友 508 | - 📝 **撰写博客**关于开发工具和工作流程 - 我们提供写作支持 509 | - 🌟 **加入生态** - 致力于支持开源和(golang)开发场景 510 | 511 | **祝你用这个包编程愉快!** 🎉🎉🎉 512 | 513 | 514 | 515 | --- 516 | 517 | ## 📈 GitHub Stars 518 | 519 | [![Stargazers](https://starchart.cc/yyle88/gormmom.svg?variant=adaptive)](https://starchart.cc/yyle88/gormmom) 520 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/yyle88/gormmom/release.yml?branch=main&label=BUILD)](https://github.com/yyle88/gormmom/actions/workflows/release.yml?query=branch%3Amain) 2 | [![GoDoc](https://pkg.go.dev/badge/github.com/yyle88/gormmom)](https://pkg.go.dev/github.com/yyle88/gormmom) 3 | [![Coverage Status](https://img.shields.io/coveralls/github/yyle88/gormmom/main.svg)](https://coveralls.io/github/yyle88/gormmom?branch=main) 4 | [![Supported Go Versions](https://img.shields.io/badge/Go-1.24+-lightgrey.svg)](https://go.dev/) 5 | [![GitHub Release](https://img.shields.io/github/release/yyle88/gormmom.svg)](https://github.com/yyle88/gormmom/releases) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/yyle88/gormmom)](https://goreportcard.com/report/github.com/yyle88/gormmom) 7 | 8 | # 🌍 GORMMOM - Native Language Programming Revolution with GORM 9 | 10 | **gormmom** is the **native language programming engine** that breaks down language barriers in database development. As the **smart tag generation engine** of the GORM ecosystem, it empowers teams worldwide to write database models in native languages while auto generating database-compatible GORM tags and column names. 11 | 12 | > 🎯 **Language Liberation**: Code in Chinese, Arabic, Japanese, and various languages - gormmom bridges the gap between human expression and database requirements. 13 | 14 | --- 15 | 16 | ## Ecosystem 17 | 18 | ![GORM Type-Safe Ecosystem](https://github.com/yyle88/gormcnm/raw/main/assets/gormcnm-ecosystem.svg) 19 | 20 | --- 21 | 22 | 23 | 24 | ## CHINESE README 25 | 26 | [中文说明](README.zh.md) 27 | 28 | 29 | --- 30 | 31 | ## 🚀 Installation 32 | 33 | ```bash 34 | go get github.com/yyle88/gormmom 35 | ``` 36 | 37 | --- 38 | 39 | ## 🔄 Tech Comparison 40 | 41 | | Ecosystem | Java MyBatis Plus | Python SQLAlchemy | Go GORM Ecosystem | 42 | |-----------------------|--------------------|-------------------|--------------------| 43 | | **Type-Safe Columns** | `Example::getName` | `Example.name` | `cls.Name.Eq()` | 44 | | **Code Generation** | ✅ Plugin support | ✅ Reflection | ✅ AST precision | 45 | | **Repo Pattern** | ✅ BaseMapper | ✅ Session API | ✅ GormRepo | 46 | | **Native Language** | 🟡 Limited | 🟡 Limited | ✅ Complete support | 47 | 48 | --- 49 | 50 | ## 🌟 The Problem & Solution 51 | 52 | ### ⚡ Standard Approach 53 | ```go 54 | // ❌ Common approach: Developers constrained to English naming 55 | type Account struct { 56 | ID uint `gorm:"primaryKey"` 57 | Username string `gorm:"column:username;uniqueIndex"` 58 | Nickname string `gorm:"column:nickname;index"` 59 | Age int `gorm:"column:age"` 60 | PhoneNum string `gorm:"column:phone_num"` 61 | Mailbox string `gorm:"column:mailbox"` 62 | Address string `gorm:"column:address"` 63 | Status string `gorm:"column:status;index"` 64 | } 65 | ``` 66 | 67 | ### ✅ GORMMOM Solution 68 | ```go 69 | // ✅ GORMMOM: Program in native language! 70 | type T账户信息 struct { 71 | ID uint `gorm:"primaryKey"` 72 | Z账号 string `gorm:"uniqueIndex"` 73 | N昵称 string `gorm:"index"` 74 | A年龄 int `gorm:""` 75 | D电话 string `gorm:""` 76 | E邮箱 string `gorm:""` 77 | J住址 string `gorm:""` 78 | S状态 string `gorm:"index"` 79 | } 80 | 81 | func (*T账户信息) TableName() string { 82 | return "accounts" // Database-compatible table name 83 | } 84 | ``` 85 | 86 | --- 87 | 88 | ## 🌍 Multi-Language Examples 89 | 90 | ### 繁體中文 91 | ```go 92 | type T賬戶信息 struct { 93 | ID uint `gorm:"primaryKey"` 94 | Z賬號 string `gorm:"uniqueIndex"` 95 | N暱稱 string `gorm:"index"` 96 | A年齡 int `gorm:""` 97 | D電話 string `gorm:""` 98 | E郵箱 string `gorm:""` 99 | J住址 string `gorm:""` 100 | S狀態 string `gorm:"index"` 101 | } 102 | 103 | func (*T賬戶信息) TableName() string { 104 | return "accounts" 105 | } 106 | ``` 107 | 108 | ### 日本語 109 | ```go 110 | type Tアカウント情報 struct { 111 | ID uint `gorm:"primaryKey"` 112 | Aアカウント string `gorm:"uniqueIndex"` 113 | Nニックネーム string `gorm:"index"` 114 | N年齢 int `gorm:""` 115 | D電話番号 string `gorm:""` 116 | Eメール string `gorm:""` 117 | J住所 string `gorm:""` 118 | Sステータス string `gorm:"index"` 119 | } 120 | 121 | func (*Tアカウント情報) TableName() string { 122 | return "accounts" 123 | } 124 | ``` 125 | 126 | ### 한국어 127 | ```go 128 | type T계정정보 struct { 129 | ID uint `gorm:"primaryKey"` 130 | G계정 string `gorm:"uniqueIndex"` 131 | N닉네임 string `gorm:"index"` 132 | N나이 int `gorm:""` 133 | J전화번호 string `gorm:""` 134 | E이메일 string `gorm:""` 135 | J주소 string `gorm:""` 136 | S상태 string `gorm:"index"` 137 | } 138 | 139 | func (*T계정정보) TableName() string { 140 | return "accounts" 141 | } 142 | ``` 143 | 144 | --- 145 | 146 | ## 🛠️ Usage 147 | 148 | ### 1. Auto Tag Generation 149 | 150 | Once gormmom executes, the struct gets database-compatible column tags: 151 | 152 | ```go 153 | // Generated with database-compatible column names 154 | type T账户信息 struct { 155 | ID uint `gorm:"primaryKey"` 156 | Z账号 string `gorm:"column:z_zhang_hao;uniqueIndex"` 157 | N昵称 string `gorm:"column:n_ni_cheng;index"` 158 | A年龄 int `gorm:"column:a_nian_ling"` 159 | D电话 string `gorm:"column:d_dian_hua"` 160 | E邮箱 string `gorm:"column:e_you_xiang"` 161 | J住址 string `gorm:"column:j_zhu_zhi"` 162 | S状态 string `gorm:"column:s_zhuang_tai;index"` 163 | } 164 | ``` 165 | 166 | ### 2. Generate Commands 167 | 168 | ```bash 169 | # Step 1: Generate GORM tags with native language fields 170 | go test -v -run TestGen/GenGormMom 171 | 172 | # Step 2: Generate type-safe column methods (with gormcngen) 173 | go test -v -run TestGen/GenGormCnm 174 | ``` 175 | 176 | ### 3. Use with gormrepo 177 | 178 | **English Version:** 179 | 180 | ```go 181 | // Create repo 182 | repo := gormrepo.NewGormRepo(&Account{}, (&Account{}).Columns()) 183 | 184 | // Select - First (by username) 185 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *AccountColumns) *gorm.DB { 186 | return db.Where(cls.Username.Eq("alice")) 187 | }) 188 | 189 | // Select - First (by nickname) 190 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *AccountColumns) *gorm.DB { 191 | return db.Where(cls.Nickname.Eq("Alice")) 192 | }) 193 | 194 | // Select - Find 195 | accounts, err := repo.With(ctx, db).Find(func(db *gorm.DB, cls *AccountColumns) *gorm.DB { 196 | return db.Where(cls.Age.Gte(18)) 197 | }) 198 | 199 | // Select - FindPage 200 | accounts, err := repo.With(ctx, db).FindPage( 201 | func(db *gorm.DB, cls *AccountColumns) *gorm.DB { 202 | return db.Where(cls.Age.Gte(18)) 203 | }, 204 | func(cls *AccountColumns) gormcnm.OrderByBottle { 205 | return cls.ID.OrderByBottle("DESC") 206 | }, 207 | &gormrepo.Pagination{Limit: 10, Offset: 0}, 208 | ) 209 | 210 | // Create 211 | err := repo.With(ctx, db).Create(&Account{Username: "bob", Nickname: "Bob", Age: 25}) 212 | 213 | // Update 214 | err := repo.With(ctx, db).Updates( 215 | func(db *gorm.DB, cls *AccountColumns) *gorm.DB { 216 | return db.Where(cls.ID.Eq(1)) 217 | }, 218 | func(cls *AccountColumns) map[string]interface{} { 219 | return cls.Kw(cls.Age.Kv(26)).AsMap() 220 | }, 221 | ) 222 | 223 | // Delete 224 | err := repo.With(ctx, db).DeleteW(func(db *gorm.DB, cls *AccountColumns) *gorm.DB { 225 | return db.Where(cls.ID.Eq(1)) 226 | }) 227 | ``` 228 | 229 | **中文(简体)版本:** 230 | 231 | ```go 232 | // Create repo 233 | repo := gormrepo.NewGormRepo(&T账户信息{}, (&T账户信息{}).Columns()) 234 | 235 | // Select - First (by username) 236 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *T账户信息Columns) *gorm.DB { 237 | return db.Where(cls.Z账号.Eq("wang-xiao-ming")) 238 | }) 239 | 240 | // Select - First (by nickname) 241 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *T账户信息Columns) *gorm.DB { 242 | return db.Where(cls.N昵称.Eq("王小明")) 243 | }) 244 | 245 | // Select - Find 246 | accounts, err := repo.With(ctx, db).Find(func(db *gorm.DB, cls *T账户信息Columns) *gorm.DB { 247 | return db.Where(cls.A年龄.Gte(18)) 248 | }) 249 | 250 | // Select - FindPage 251 | accounts, err := repo.With(ctx, db).FindPage( 252 | func(db *gorm.DB, cls *T账户信息Columns) *gorm.DB { 253 | return db.Where(cls.A年龄.Gte(18)) 254 | }, 255 | func(cls *T账户信息Columns) gormcnm.OrderByBottle { 256 | return cls.ID.OrderByBottle("DESC") 257 | }, 258 | &gormrepo.Pagination{Limit: 10, Offset: 0}, 259 | ) 260 | 261 | // Create 262 | err := repo.With(ctx, db).Create(&T账户信息{Z账号: "han-mei-mei", N昵称: "韩梅梅", A年龄: 25}) 263 | 264 | // Update 265 | err := repo.With(ctx, db).Updates( 266 | func(db *gorm.DB, cls *T账户信息Columns) *gorm.DB { 267 | return db.Where(cls.ID.Eq(1)) 268 | }, 269 | func(cls *T账户信息Columns) map[string]interface{} { 270 | return cls.Kw(cls.A年龄.Kv(26)).AsMap() 271 | }, 272 | ) 273 | 274 | // Delete 275 | err := repo.With(ctx, db).DeleteW(func(db *gorm.DB, cls *T账户信息Columns) *gorm.DB { 276 | return db.Where(cls.ID.Eq(1)) 277 | }) 278 | ``` 279 | 280 | **中文(繁體)版本:** 281 | 282 | ```go 283 | // Create repo 284 | repo := gormrepo.NewGormRepo(&T賬戶信息{}, (&T賬戶信息{}).Columns()) 285 | 286 | // Select - First (by username) 287 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *T賬戶信息Columns) *gorm.DB { 288 | return db.Where(cls.Z賬號.Eq("wang-xiao-ming")) 289 | }) 290 | 291 | // Select - First (by nickname) 292 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *T賬戶信息Columns) *gorm.DB { 293 | return db.Where(cls.N暱稱.Eq("王小明")) 294 | }) 295 | 296 | // Select - Find 297 | accounts, err := repo.With(ctx, db).Find(func(db *gorm.DB, cls *T賬戶信息Columns) *gorm.DB { 298 | return db.Where(cls.A年齡.Gte(18)) 299 | }) 300 | 301 | // Select - FindPage 302 | accounts, err := repo.With(ctx, db).FindPage( 303 | func(db *gorm.DB, cls *T賬戶信息Columns) *gorm.DB { 304 | return db.Where(cls.A年齡.Gte(18)) 305 | }, 306 | func(cls *T賬戶信息Columns) gormcnm.OrderByBottle { 307 | return cls.ID.OrderByBottle("DESC") 308 | }, 309 | &gormrepo.Pagination{Limit: 10, Offset: 0}, 310 | ) 311 | 312 | // Create 313 | err := repo.With(ctx, db).Create(&T賬戶信息{Z賬號: "han-mei-mei", N暱稱: "韓梅梅", A年齡: 25}) 314 | 315 | // Update 316 | err := repo.With(ctx, db).Updates( 317 | func(db *gorm.DB, cls *T賬戶信息Columns) *gorm.DB { 318 | return db.Where(cls.ID.Eq(1)) 319 | }, 320 | func(cls *T賬戶信息Columns) map[string]interface{} { 321 | return cls.Kw(cls.A年齡.Kv(26)).AsMap() 322 | }, 323 | ) 324 | 325 | // Delete 326 | err := repo.With(ctx, db).DeleteW(func(db *gorm.DB, cls *T賬戶信息Columns) *gorm.DB { 327 | return db.Where(cls.ID.Eq(1)) 328 | }) 329 | ``` 330 | 331 | **日本語版:** 332 | 333 | ```go 334 | // Create repo 335 | repo := gormrepo.NewGormRepo(&Tアカウント情報{}, (&Tアカウント情報{}).Columns()) 336 | 337 | // Select - First (by username) 338 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *Tアカウント情報Columns) *gorm.DB { 339 | return db.Where(cls.Aアカウント.Eq("tanaka")) 340 | }) 341 | 342 | // Select - First (by nickname) 343 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *Tアカウント情報Columns) *gorm.DB { 344 | return db.Where(cls.Nニックネーム.Eq("田中太郎")) 345 | }) 346 | 347 | // Select - Find 348 | accounts, err := repo.With(ctx, db).Find(func(db *gorm.DB, cls *Tアカウント情報Columns) *gorm.DB { 349 | return db.Where(cls.N年齢.Gte(18)) 350 | }) 351 | 352 | // Select - FindPage 353 | accounts, err := repo.With(ctx, db).FindPage( 354 | func(db *gorm.DB, cls *Tアカウント情報Columns) *gorm.DB { 355 | return db.Where(cls.N年齢.Gte(18)) 356 | }, 357 | func(cls *Tアカウント情報Columns) gormcnm.OrderByBottle { 358 | return cls.ID.OrderByBottle("DESC") 359 | }, 360 | &gormrepo.Pagination{Limit: 10, Offset: 0}, 361 | ) 362 | 363 | // Create 364 | err := repo.With(ctx, db).Create(&Tアカウント情報{Aアカウント: "suzuki", Nニックネーム: "鈴木花子", N年齢: 25}) 365 | 366 | // Update 367 | err := repo.With(ctx, db).Updates( 368 | func(db *gorm.DB, cls *Tアカウント情報Columns) *gorm.DB { 369 | return db.Where(cls.ID.Eq(1)) 370 | }, 371 | func(cls *Tアカウント情報Columns) map[string]interface{} { 372 | return cls.Kw(cls.N年齢.Kv(26)).AsMap() 373 | }, 374 | ) 375 | 376 | // Delete 377 | err := repo.With(ctx, db).DeleteW(func(db *gorm.DB, cls *Tアカウント情報Columns) *gorm.DB { 378 | return db.Where(cls.ID.Eq(1)) 379 | }) 380 | ``` 381 | 382 | **한국어판:** 383 | 384 | ```go 385 | // Create repo 386 | repo := gormrepo.NewGormRepo(&T계정정보{}, (&T계정정보{}).Columns()) 387 | 388 | // Select - First (by username) 389 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *T계정정보Columns) *gorm.DB { 390 | return db.Where(cls.G계정.Eq("kim-cheol-su")) 391 | }) 392 | 393 | // Select - First (by nickname) 394 | account, err := repo.With(ctx, db).First(func(db *gorm.DB, cls *T계정정보Columns) *gorm.DB { 395 | return db.Where(cls.N닉네임.Eq("김철수")) 396 | }) 397 | 398 | // Select - Find 399 | accounts, err := repo.With(ctx, db).Find(func(db *gorm.DB, cls *T계정정보Columns) *gorm.DB { 400 | return db.Where(cls.N나이.Gte(18)) 401 | }) 402 | 403 | // Select - FindPage 404 | accounts, err := repo.With(ctx, db).FindPage( 405 | func(db *gorm.DB, cls *T계정정보Columns) *gorm.DB { 406 | return db.Where(cls.N나이.Gte(18)) 407 | }, 408 | func(cls *T계정정보Columns) gormcnm.OrderByBottle { 409 | return cls.ID.OrderByBottle("DESC") 410 | }, 411 | &gormrepo.Pagination{Limit: 10, Offset: 0}, 412 | ) 413 | 414 | // Create 415 | err := repo.With(ctx, db).Create(&T계정정보{G계정: "lee-young-hee", N닉네임: "이영희", N나이: 25}) 416 | 417 | // Update 418 | err := repo.With(ctx, db).Updates( 419 | func(db *gorm.DB, cls *T계정정보Columns) *gorm.DB { 420 | return db.Where(cls.ID.Eq(1)) 421 | }, 422 | func(cls *T계정정보Columns) map[string]interface{} { 423 | return cls.Kw(cls.N나이.Kv(26)).AsMap() 424 | }, 425 | ) 426 | 427 | // Delete 428 | err := repo.With(ctx, db).DeleteW(func(db *gorm.DB, cls *T계정정보Columns) *gorm.DB { 429 | return db.Where(cls.ID.Eq(1)) 430 | }) 431 | ``` 432 | 433 | ## 📝 Complete Examples 434 | 435 | Check [examples](internal/examples/) DIR with complete integration examples 436 | 437 | --- 438 | 439 | ## Related Projects 440 | 441 | Explore the complete GORM ecosystem with these integrated packages: 442 | 443 | ### Core Ecosystem 444 | 445 | - **[gormcnm](https://github.com/yyle88/gormcnm)** - GORM foundation providing type-safe column operations and query builders 446 | - **[gormcngen](https://github.com/yyle88/gormcngen)** - AST-based code generation engine with type-safe GORM operations 447 | - **[gormrepo](https://github.com/yyle88/gormrepo)** - Repo pattern implementation with GORM best practices 448 | - **[gormmom](https://github.com/yyle88/gormmom)** - Native language GORM tag generation engine with smart column naming (this project) 449 | - **[gormzhcn](https://github.com/go-zwbc/gormzhcn)** - Complete Chinese programming interface with GORM 450 | 451 | Each package targets different aspects of GORM development, including localization, type-safe operations, and code generation. 452 | 453 | --- 454 | 455 | 456 | 457 | 458 | ## 📄 License 459 | 460 | MIT License - see [LICENSE](LICENSE). 461 | 462 | --- 463 | 464 | ## 💬 Contact & Feedback 465 | 466 | Contributions are welcome! Report bugs, suggest features, and contribute code: 467 | 468 | - 🐛 **Mistake reports?** Open an issue on GitHub with reproduction steps 469 | - 💡 **Fresh ideas?** Create an issue to discuss 470 | - 📖 **Documentation confusing?** Report it so we can improve 471 | - 🚀 **Need new features?** Share the use cases to help us understand requirements 472 | - ⚡ **Performance issue?** Help us optimize through reporting slow operations 473 | - 🔧 **Configuration problem?** Ask questions about complex setups 474 | - 📢 **Follow project progress?** Watch the repo to get new releases and features 475 | - 🌟 **Success stories?** Share how this package improved the workflow 476 | - 💬 **Feedback?** We welcome suggestions and comments 477 | 478 | --- 479 | 480 | ## 🔧 Development 481 | 482 | New code contributions, follow this process: 483 | 484 | 1. **Fork**: Fork the repo on GitHub (using the webpage UI). 485 | 2. **Clone**: Clone the forked project (`git clone https://github.com/yourname/repo-name.git`). 486 | 3. **Navigate**: Navigate to the cloned project (`cd repo-name`) 487 | 4. **Branch**: Create a feature branch (`git checkout -b feature/xxx`). 488 | 5. **Code**: Implement the changes with comprehensive tests 489 | 6. **Testing**: (Golang project) Ensure tests pass (`go test ./...`) and follow Go code style conventions 490 | 7. **Documentation**: Update documentation to support client-facing changes 491 | 8. **Stage**: Stage changes (`git add .`) 492 | 9. **Commit**: Commit changes (`git commit -m "Add feature xxx"`) ensuring backward compatible code 493 | 10. **Push**: Push to the branch (`git push origin feature/xxx`). 494 | 11. **PR**: Open a merge request on GitHub (on the GitHub webpage) with detailed description. 495 | 496 | Please ensure tests pass and include relevant documentation updates. 497 | 498 | --- 499 | 500 | ## 🌟 Support 501 | 502 | Welcome to contribute to this project via submitting merge requests and reporting issues. 503 | 504 | **Project Support:** 505 | 506 | - ⭐ **Give GitHub stars** if this project helps you 507 | - 🤝 **Share with teammates** and (golang) programming friends 508 | - 📝 **Write tech blogs** about development tools and workflows - we provide content writing support 509 | - 🌟 **Join the ecosystem** - committed to supporting open source and the (golang) development scene 510 | 511 | **Have Fun Coding with this package!** 🎉🎉🎉 512 | 513 | 514 | 515 | --- 516 | 517 | ## 📈 GitHub Stars 518 | 519 | [![Stargazers](https://starchart.cc/yyle88/gormmom.svg?variant=adaptive)](https://starchart.cc/yyle88/gormmom) 520 | -------------------------------------------------------------------------------- /gen.go: -------------------------------------------------------------------------------- 1 | // Package gormmom: Native language programming engine that eliminates language constraints in database development 2 | // As the smart tag generation engine of the GORM ecosystem, it empowers teams worldwide to write database models 3 | // in native languages while auto generating database-compatible GORM tags and column names 4 | // Supports Unicode-compatible field names in Chinese, Japanese, Korean, and additional languages 5 | // 6 | // gormmom: 原生语言编程引擎,消除数据库开发中的语言限制 7 | // 作为 GORM 生态系统的智能标签生成引擎,它赋能全球团队使用原生语言编写数据库模型 8 | // 同时自动生成数据库兼容的 GORM 标签和列名 9 | // 支持 Unicode 兼容的中文、日语、韩语和其他语言字段名 10 | package gormmom 11 | 12 | import ( 13 | "bytes" 14 | "fmt" 15 | "go/ast" 16 | "os" 17 | "slices" 18 | 19 | "github.com/yyle88/done" 20 | "github.com/yyle88/formatgo" 21 | "github.com/yyle88/gormmom/gormidxname" 22 | "github.com/yyle88/gormmom/gormmomname" 23 | "github.com/yyle88/must" 24 | "github.com/yyle88/rese" 25 | "github.com/yyle88/syntaxgo/syntaxgo_ast" 26 | "github.com/yyle88/syntaxgo/syntaxgo_astnode" 27 | "github.com/yyle88/syntaxgo/syntaxgo_search" 28 | "github.com/yyle88/syntaxgo/syntaxgo_tag" 29 | "github.com/yyle88/zaplog" 30 | "go.uber.org/zap" 31 | "gorm.io/gorm/schema" 32 | ) 33 | 34 | // Config represents the configuration for GORM tag generation operations 35 | // Contains the target struct information and generation options with customizing output 36 | // Provides smart mapping and deterministic generation of database-compatible tags 37 | // 38 | // Config 代表 GORM 标签生成操作的配置 39 | // 包含目标结构体信息和生成选项,用于自定义输出 40 | // 提供智能映射和确定性的数据库兼容标签生成 41 | type Config struct { 42 | structI *GormStruct // Target GORM struct to process // 要处理的目标 GORM 结构体 43 | options *Options // Generation options and settings // 生成选项和设置 44 | } 45 | 46 | // NewConfig creates a new configuration instance with GORM tag generation 47 | // Takes the target struct and generation options, returns configured instance 48 | // Used to initialize the generation workflow with custom settings 49 | // 50 | // NewConfig 创建新的 GORM 标签生成配置实例 51 | // 接收目标结构体和生成选项,返回配置好的实例 52 | // 用于使用自定义设置初始化生成工作流程 53 | func NewConfig(structI *GormStruct, options *Options) *Config { 54 | return &Config{ 55 | structI: structI, 56 | options: options, 57 | } 58 | } 59 | 60 | // Generate generates new GORM tags and replaces the original source file 61 | // Processes the struct definition to add database-compatible tags and column names 62 | // Formats the generated code and writes back to the source file when changes detected 63 | // Returns the result containing the new code and change status 64 | // 65 | // Generate 生成新的 GORM 标签并替换原始源文件 66 | // 处理结构体定义以添加数据库兼容的标签和列名 67 | // 检测到变化时自动格式化生成的代码并写回源文件 68 | // 返回包含新代码和变化状态的结果 69 | func (cfg *Config) Generate() *CodeResult { 70 | newCode := cfg.Preview() 71 | srcPath := must.SameNice(cfg.structI.sourcePath, newCode.SourcePath) 72 | if newCode.HasChange() { // 只有当有变化时才写文件 73 | srcCode := must.Have(rese.A1(formatgo.FormatBytes(newCode.OutputCode))) 74 | must.Done(os.WriteFile(srcPath, srcCode, 0644)) 75 | newCode.OutputCode = srcCode 76 | } 77 | return newCode 78 | } 79 | 80 | // Preview generates new code with GORM tags without modifying the original file 81 | // Reads the source file and processes it to add native language field mappings 82 | // Returns the new code result with updated tags and column definitions 83 | // 84 | // Preview 生成带有 GORM 标签的新代码而不修改原文件 85 | // 读取源文件并处理以添加原生语言字段映射 86 | // 返回包含更新标签和列定义的新代码结果 87 | func (cfg *Config) Preview() *CodeResult { 88 | return cfg.makeNewCode(rese.A1(os.ReadFile(cfg.structI.sourcePath))) 89 | } 90 | 91 | func (cfg *Config) makeNewCode(sourceCode []byte) *CodeResult { 92 | astBundle := rese.C1(syntaxgo_ast.NewAstBundleV1(sourceCode)) 93 | astFile, fileSet := astBundle.GetBundle() 94 | // 使用语法分析树 ast 找到结构体的代码,就像这样 95 | // struct { 96 | // Name string 97 | // } 98 | // 结果只包含结构体的内容 99 | structType, ok := syntaxgo_search.FindStructTypeByName(astFile, cfg.structI.structName) 100 | if !ok { 101 | const reason = "CAN NOT FIND STRUCT TYPE" 102 | zaplog.LOG.Panic(reason, zap.String("struct_name", cfg.structI.structName)) 103 | panic(reason) 104 | } 105 | done.Done(ast.Print(fileSet, structType)) 106 | 107 | // 这里拿到的是要修改的操作,具体指改哪个文件哪个结构体的哪个字段,哪个位置的代码,以及新代码的内容 108 | modifications := cfg.collectTagModifications(structType) 109 | 110 | if cfg.options.autoIndexName { 111 | //这里增加个新逻辑,就是单列索引的索引名称不正确,需要也校正索引名,因此这个函数会补充标签内容 112 | cfg.correctIndexNames(modifications) 113 | 114 | zaplog.LOG.Debug("change_index_names") 115 | for _, step := range modifications { 116 | zaplog.LOG.Debug("check_index:", zap.String("struct_filed_name", step.structFieldName), zap.String("new_tag_code", step.newTagCode)) 117 | } 118 | } 119 | 120 | //需要翻转下从后往前替换,因为替换以后源码会变,假如从前往后替换坐标就对不上啦,而从后往前替换则不存在这个问题 121 | slices.Reverse(modifications) 122 | 123 | //接下来替换代码,把需要 新增 或者 替换 的标签都设置到代码里 124 | newCode := sourceCode 125 | changedLineCount := 0 126 | for _, step := range modifications { 127 | oldTagCode := syntaxgo_astnode.GetCode(newCode, step.tagPosNode) 128 | newTagCode := []byte(step.newTagCode) 129 | if !bytes.Equal(oldTagCode, newTagCode) { 130 | zaplog.LOG.Debug("change", zap.ByteString("old_tag_code", oldTagCode), zap.ByteString("new_tag_code", newTagCode)) 131 | } else { 132 | continue 133 | } 134 | newCode = syntaxgo_astnode.ChangeNodeCode(newCode, step.tagPosNode, newTagCode) 135 | changedLineCount++ 136 | } 137 | return &CodeResult{ 138 | OutputCode: newCode, 139 | SourcePath: cfg.structI.sourcePath, 140 | ChangedLineCount: changedLineCount, 141 | } 142 | } 143 | 144 | // defineTagModification contains tag modification details for a single field 145 | // Holds field name, column name, position, and new tag content for replacement 146 | // Used internally to track and apply tag changes during generation 147 | // 148 | // defineTagModification 包含单个字段的标签修改详情 149 | // 保存字段名、列名、位置和用于替换的新标签内容 150 | // 在生成过程中内部使用以跟踪和应用标签更改 151 | type defineTagModification struct { 152 | structFieldName string // Struct field name // 结构体的字段名 153 | columnName string // Database column name // 数据表中的列名 154 | tagPosNode ast.Node // Tag position node in source code // 标签在源码中的起止位置 155 | newTagCode string // New tag code content // 新标签的完整内容 156 | } 157 | 158 | // collectTagModifications analyzes struct fields and collects necessary tag modifications 159 | // Processes each field to determine if native language column naming is needed 160 | // Returns a collection of modifications required for proper GORM tag generation 161 | // 162 | // collectTagModifications 分析结构体字段并收集必要的标签修改 163 | // 处理每个字段以确定是否需要原生语言列名 164 | // 返回正确 GORM 标签生成所需的修改集合 165 | func (cfg *Config) collectTagModifications(structType *ast.StructType) []*defineTagModification { 166 | var results []*defineTagModification 167 | 168 | // 默认的样式配置 169 | defaultPattern := cfg.options.columnNamingStrategies.GetDefault() 170 | 171 | // 遍历结构体字段 172 | for _, fieldItem := range structType.Fields.List { 173 | // 打印字段名称和类型 174 | for _, fieldName := range fieldItem.Names { 175 | zaplog.LOG.Debug("--") 176 | zaplog.LOG.Debug("process", zap.String("struct_field_name:", fieldName.Name)) 177 | zaplog.LOG.Debug("--") 178 | 179 | schemaColumn, exist := cfg.structI.gormFields.Get(fieldName.Name) 180 | if !exist { //比如字段是 "V哈哈" 就没事 而假如是 "v哈哈" 或者 "哈哈" 就不行,因为非以大写字母开始的字段,就没有gorm的列名 181 | zaplog.LOG.Debug("NO SCHEMA_FIELD - MAYBE NAME IS UNEXPORTED", zap.String("struct_name", cfg.structI.structName), zap.String("struct_field_name", fieldName.Name)) 182 | continue 183 | } 184 | 185 | if fieldItem.Tag == nil { 186 | zaplog.LOG.Debug("NO TAG", zap.String("struct_name", cfg.structI.structName), zap.String("struct_field_name", fieldName.Name)) 187 | 188 | // 假如配置跳过简单字段,而这个字段恰好是简单字段时,就跳过(因为没有标签,也就是没有配置规则) 189 | if cfg.options.skipBasicColumnName && defaultPattern.CheckColumnName(schemaColumn.DBName) { 190 | zaplog.LOG.Debug("SKIP SIMPLE FIELD", zap.String("struct_name", cfg.structI.structName), zap.String("struct_field_name", fieldName.Name)) 191 | continue 192 | } 193 | 194 | // 假如没有标签,也就是没配置规则,但假如字段不符合默认规则,就得重新创建字段 195 | if !defaultPattern.CheckColumnName(schemaColumn.DBName) { 196 | if len(fieldItem.Names) >= 2 { //比如 a,b int 这种两个字段在一起,但其中一个字段的列名不正确时,就没法自动解决啦(其实有办法但不想实现,因为代价较大而没有收益) 197 | const reason = "CAN NOT HANDLE THIS SITUATION" 198 | zaplog.LOG.Panic(reason, zap.String("struct_name", cfg.structI.structName), zap.String("struct_field_name", fieldName.Name)) 199 | panic(reason) //这种情况下当有错时,就不处理这种情况,就需要程序员先把两个字段定义到两行里 200 | } 201 | // 需要修改标签内容 202 | changeTag := cfg.modifyFieldTagCorrection(schemaColumn, "``", defaultPattern.GetPatternEnum()) //这里应该走创建标签的逻辑,但和修改标签的逻辑是相同的 203 | // 收集标签修改操作 204 | results = append(results, &defineTagModification{ 205 | structFieldName: fieldName.Name, 206 | columnName: changeTag.columnName, 207 | tagPosNode: syntaxgo_astnode.NewNode(fieldItem.End(), fieldItem.End()), //在尾部插入新的标签,要紧贴字段而且在换行符前面 208 | newTagCode: changeTag.newTagCode, 209 | }) 210 | } 211 | } else if patternName := cfg.extractTagGetCnmPattern(fieldItem.Tag.Value); patternName != "" { 212 | patternEnum := gormmomname.PatternEnum(patternName) 213 | zaplog.LOG.Debug("process", zap.String("column_name_pattern", string(patternEnum))) 214 | // 需要修改标签内容 215 | changeTag := cfg.modifyFieldTagCorrection(schemaColumn, fieldItem.Tag.Value, patternEnum) 216 | // 收集标签修改操作 217 | results = append(results, &defineTagModification{ 218 | structFieldName: fieldName.Name, 219 | columnName: changeTag.columnName, 220 | tagPosNode: fieldItem.Tag, //完整替换原来的标签 221 | newTagCode: changeTag.newTagCode, 222 | }) 223 | } else { 224 | if cfg.options.skipBasicColumnName && defaultPattern.CheckColumnName(schemaColumn.DBName) { 225 | zaplog.LOG.Debug("SKIP SIMPLE FIELD", zap.String("struct_name", cfg.structI.structName), zap.String("struct_field_name", fieldName.Name)) 226 | if cfg.options.autoIndexName && cfg.hasAnyIdxTagUdxTagValue(fieldItem) { 227 | results = append(results, cfg.newFirstNotModification(fieldItem, schemaColumn.DBName)) 228 | } 229 | continue 230 | } 231 | 232 | if !defaultPattern.CheckColumnName(schemaColumn.DBName) { //按照比较宽泛的规则也校验不过的时候就需要修正字段名 233 | // 需要修改标签内容 234 | changeTag := cfg.modifyFieldTagCorrection(schemaColumn, fieldItem.Tag.Value, defaultPattern.GetPatternEnum()) 235 | // 收集标签修改操作 236 | results = append(results, &defineTagModification{ 237 | structFieldName: fieldName.Name, 238 | columnName: changeTag.columnName, 239 | tagPosNode: fieldItem.Tag, //完整替换原来的标签 240 | newTagCode: changeTag.newTagCode, 241 | }) 242 | } else { 243 | zaplog.LOG.Debug("match-pattern-so-skip", zap.String("name", fieldName.Name), zap.String("tag", fieldItem.Tag.Value)) 244 | if cfg.options.autoIndexName && cfg.hasAnyIdxTagUdxTagValue(fieldItem) { 245 | results = append(results, cfg.newFirstNotModification(fieldItem, schemaColumn.DBName)) 246 | } 247 | continue 248 | } 249 | } 250 | } 251 | } 252 | 253 | zaplog.LOG.Debug("change_column_names") 254 | for _, item := range results { 255 | zaplog.LOG.Debug("check_column:", zap.String("field_name", item.structFieldName), zap.String("new_column_name", item.columnName), zap.String("new_tag_code", item.newTagCode)) 256 | 257 | must.Nice(item.structFieldName) 258 | must.Nice(item.columnName) 259 | must.Nice(item.newTagCode) 260 | must.Nice(item.tagPosNode.Pos()) 261 | must.Nice(item.tagPosNode.End()) 262 | } 263 | return results 264 | } 265 | 266 | // hasAnyIdxTagUdxTagValue checks if field has any index pattern tag values 267 | // Examines field tags for idx or udx pattern configurations 268 | // Returns true if any index pattern tag is found in the field 269 | // 270 | // hasAnyIdxTagUdxTagValue 检查字段是否有任何索引模式标签值 271 | // 检查字段标签中的 idx 或 udx 模式配置 272 | // 如果在字段中找到任何索引模式标签则返回 true 273 | func (cfg *Config) hasAnyIdxTagUdxTagValue(fieldItem *ast.Field) bool { 274 | if len(fieldItem.Names) != 1 { 275 | return false 276 | } 277 | if fieldItem.Tag == nil { 278 | return false 279 | } 280 | for _, patternTagEnum := range []gormidxname.IndexPatternTagEnum{ 281 | gormidxname.IdxPatternTagName, 282 | gormidxname.UdxPatternTagName, 283 | } { 284 | var name = cfg.extractTagFieldGetValue(fieldItem.Tag.Value, cfg.options.systemTagName, string(patternTagEnum)) 285 | if name != "" { 286 | zaplog.LOG.Debug("match-pattern-so-has-any-tag", zap.String("pattern", string(patternTagEnum)), zap.String("name", name)) 287 | return true 288 | } 289 | } 290 | return false 291 | } 292 | 293 | // hasOneIdxTagUdxTagValue checks if tag code contains specific index pattern 294 | // Validates the presence of a particular index pattern in the tag string 295 | // Returns true if the specified pattern tag is found in the code 296 | // 297 | // hasOneIdxTagUdxTagValue 检查标签代码是否包含特定的索引模式 298 | // 验证标签字符串中是否存在特定的索引模式 299 | // 如果在代码中找到指定的模式标签则返回 true 300 | func (cfg *Config) hasOneIdxTagUdxTagValue(tagCode string, patternTagEnum gormidxname.IndexPatternTagEnum) bool { 301 | var name = cfg.extractTagFieldGetValue(tagCode, cfg.options.systemTagName, string(patternTagEnum)) 302 | if name != "" { 303 | zaplog.LOG.Debug("match-pattern-so-has-one-tag", zap.String("pattern", string(patternTagEnum)), zap.String("name", name)) 304 | return true 305 | } 306 | return false 307 | } 308 | 309 | // newFirstNotModification creates modification for fields without existing tags 310 | // Generates initial tag modification structure for untagged fields 311 | // Used when field requires native language column naming but has no current tags 312 | // 313 | // newFirstNotModification 为没有现有标签的字段创建修改 314 | // 为未标记的字段生成初始标签修改结构 315 | // 在字段需要原生语言列名但没有当前标签时使用 316 | func (cfg *Config) newFirstNotModification(fieldItem *ast.Field, columnName string) *defineTagModification { 317 | must.Full(fieldItem) 318 | must.Length(fieldItem.Names, 1) 319 | must.Nice(fieldItem.Names[0].Name) 320 | 321 | return &defineTagModification{ 322 | structFieldName: fieldItem.Names[0].Name, 323 | columnName: columnName, 324 | tagPosNode: fieldItem.Tag, 325 | newTagCode: fieldItem.Tag.Value, 326 | } 327 | } 328 | 329 | // extractTagGetCnmPattern extracts column naming pattern from tag code 330 | // Retrieves the column naming pattern configuration from system tag 331 | // Returns pattern string for column name generation validation 332 | // 333 | // extractTagGetCnmPattern 从标签代码中提取列名模式 334 | // 从系统标签中检索列名模式配置 335 | // 返回用于列名生成验证的模式字符串 336 | func (cfg *Config) extractTagGetCnmPattern(tagCode string) string { 337 | return cfg.extractTagFieldGetValue(tagCode, cfg.options.systemTagName, cfg.options.subTagName) 338 | } 339 | 340 | // extractTagFieldGetValue extracts nested tag field value using dual key lookup 341 | // Performs two-level extraction from tag code using primary and secondary keys 342 | // Returns the extracted field value or empty string if not found 343 | // 344 | // extractTagFieldGetValue 使用双键查找提取嵌套标签字段值 345 | // 使用主键和辅助键从标签代码进行两级提取 346 | // 返回提取的字段值,如果找不到则返回空字符串 347 | func (cfg *Config) extractTagFieldGetValue(tagCode string, key1 string, key2 string) string { 348 | tagValue := syntaxgo_tag.ExtractTagValue(tagCode, key1) 349 | if tagValue == "" { 350 | return "" 351 | } 352 | tagField := syntaxgo_tag.ExtractTagField(tagValue, key2, syntaxgo_tag.EXCLUDE_WHITESPACE_PREFIX) 353 | if tagField == "" { 354 | return "" 355 | } 356 | return tagField 357 | } 358 | 359 | // correctionNewTag contains the result of tag correction operations 360 | // Holds the corrected column name and updated tag code for field processing 361 | // Used as intermediate result during tag modification workflow 362 | // 363 | // correctionNewTag 包含标签纠正操作的结果 364 | // 保存纠正的列名和更新的标签代码,用于字段处理 365 | // 在标签修改工作流中作为中间结果使用 366 | type correctionNewTag struct { 367 | columnName string // Corrected database column name // 纠正的数据库列名 368 | newTagCode string // Updated tag code with corrections // 带有纠正的更新标签代码 369 | } 370 | 371 | // modifyFieldTagCorrection applies corrections to field tag based on schema and pattern 372 | // Processes GORM field with specified pattern to generate corrected column name and tag 373 | // Returns corrected tag structure with updated column name and tag code 374 | // 375 | // modifyFieldTagCorrection 基于模式和模式对字段标签应用纠正 376 | // 使用指定模式处理 GORM 字段,生成纠正的列名和标签 377 | // 返回带有更新列名和标签代码的纠正标签结构 378 | func (cfg *Config) modifyFieldTagCorrection(schemaField *schema.Field, tag string, patternType gormmomname.PatternEnum) *correctionNewTag { 379 | zaplog.LOG.Debug("new_fix_tag_code", zap.String("name", schemaField.Name), zap.String("tag", tag)) 380 | //在 gorm 里修改 column 内容 381 | newTag := cfg.modifyGormTagWithColumn(schemaField, tag, patternType) 382 | //在 规则 里修改 column-name-pattern 内容 383 | newTag.newTagCode = cfg.modifyPatternTagWithName(newTag.newTagCode, patternType) 384 | //这是替换后的结果,即替换整个标签内容,获得新的完整标签内容 385 | zaplog.LOG.Debug("new_fix_tag_code", zap.String("name", schemaField.Name), zap.String("column_name", newTag.columnName), zap.String("new_tag_code", newTag.newTagCode)) 386 | return newTag 387 | } 388 | 389 | // modifyGormTagWithColumn modifies GORM tag to include appropriate column name 390 | // Generates database-compatible column name using specified pattern and updates GORM tag 391 | // Returns corrected tag with correct column specification for database mapping 392 | // 393 | // modifyGormTagWithColumn 修改 GORM 标签以包含适当的列名 394 | // 使用指定模式生成数据库兼容的列名并更新 GORM 标签 395 | // 返回带有正确列规范的纠正标签,用于数据库映射 396 | func (cfg *Config) modifyGormTagWithColumn(schemaField *schema.Field, tag string, patternType gormmomname.PatternEnum) *correctionNewTag { 397 | pattern := cfg.options.columnNamingStrategies.GetPattern(patternType) 398 | columnName := pattern.BuildColumnName(schemaField.Name) 399 | zaplog.LOG.Debug("new_fix_gorm_tag", zap.String("name", schemaField.Name), zap.String("column_name", columnName)) 400 | must.Nice(columnName) 401 | 402 | tagValue, sdx, edx := syntaxgo_tag.ExtractTagValueIndex(tag, "gorm") 403 | if sdx < 0 || edx < 0 { //表示没找到 gorm 相关的内容 404 | if tagValue != "" { 405 | zaplog.LOG.Panic("IMPOSSIBLE") 406 | } 407 | part := fmt.Sprintf(`gorm:"column:%s;"`, columnName) 408 | if tag[1] != ' ' && tag[1] != '`' { 409 | part += " " //说明后面还有别的标签 410 | } 411 | p := 1 //插在第一个"`"的后面,即第一位的位置 412 | return &correctionNewTag{ 413 | columnName: columnName, 414 | newTagCode: tag[:p] + part + tag[p:], 415 | } 416 | } 417 | //设置这个标签的这个字段的值 418 | return &correctionNewTag{ 419 | columnName: columnName, 420 | newTagCode: syntaxgo_tag.SetTagFieldValue(tag, "gorm", "column", columnName, syntaxgo_tag.INSERT_LOCATION_TOP), 421 | } 422 | } 423 | 424 | // modifyPatternTagWithName adds or updates pattern tag with specified naming pattern 425 | // Inserts or modifies system tag to include column naming pattern specification 426 | // Returns updated tag string with pattern information for consistent processing 427 | // 428 | // modifyPatternTagWithName 使用指定的命名模式添加或更新模式标签 429 | // 插入或修改系统标签以包含列命名模式规范 430 | // 返回带有模式信息的更新标签字符串,用于一致性处理 431 | func (cfg *Config) modifyPatternTagWithName(tag string, patternType gormmomname.PatternEnum) string { 432 | zaplog.LOG.Debug("modify-pattern-tag-with-name", zap.String("column_name_pattern", string(patternType))) 433 | 434 | tagValue, sdx, edx := syntaxgo_tag.ExtractTagValueIndex(tag, cfg.options.systemTagName) 435 | if sdx < 0 || edx < 0 { //表示没找到 gorm 相关的内容 436 | if tagValue != "" { 437 | zaplog.LOG.Panic("IMPOSSIBLE") 438 | } 439 | part := fmt.Sprintf(`%s:"%s:%s;"`, cfg.options.systemTagName, cfg.options.subTagName, string(patternType)) 440 | if rch := tag[len(tag)-2]; rch != ' ' && rch != '`' { 441 | part = " " + part //说明前面还有别的标签 442 | } 443 | p := len(tag) - 1 //插在最后一个"`"的前面,即最后一位的位置 444 | return tag[:p] + part + tag[p:] 445 | } 446 | //设置这个标签的这个字段的值 447 | return syntaxgo_tag.SetTagFieldValue(tag, cfg.options.systemTagName, cfg.options.subTagName, string(patternType), syntaxgo_tag.INSERT_LOCATION_TOP) 448 | } 449 | --------------------------------------------------------------------------------