diff --git a/model/condition.go b/model/condition.go index ae2d329..efa0cb6 100644 --- a/model/condition.go +++ b/model/condition.go @@ -1,16 +1,17 @@ package model type QueryCondition struct { - Where ParseWhere - From string - Fields string - Group string - Order SqlBuilder - Join SqlBuilder - Having SqlBuilder - Limit int - Offset int - In [][]any + Where ParseWhere + From string + Fields string + Group string + Order SqlBuilder + Join SqlBuilder + Having SqlBuilder + Limit int + Offset int + In [][]any + Relation map[string]*QueryCondition } func Conditions(fns ...Condition) QueryCondition { @@ -23,6 +24,16 @@ func Conditions(fns ...Condition) QueryCondition { } return r } +func WithConditions(fns ...Condition) *QueryCondition { + r := QueryCondition{} + for _, fn := range fns { + fn(&r) + } + if r.Fields == "" { + r.Fields = "*" + } + return &r +} type Condition func(c *QueryCondition) @@ -84,3 +95,12 @@ func In(in ...[]any) Condition { c.In = append(c.In, in...) } } + +func With(tableTag string, q *QueryCondition) Condition { + return func(c *QueryCondition) { + if c.Relation == nil { + c.Relation = map[string]*QueryCondition{} + } + c.Relation[tableTag] = q + } +} diff --git a/model/query.go b/model/query.go index 10a9bc5..1cffd9e 100644 --- a/model/query.go +++ b/model/query.go @@ -39,7 +39,10 @@ func pagination[T Model](db dbQuery, ctx context.Context, q QueryCondition, page } if q.Group != "" { qx.Fields = q.Fields - sq, in, er := BuildQuerySql[T](qx) + if qx.From == "" { + qx.From = Table[T]() + } + sq, in, er := BuildQuerySql(qx) qx.In = [][]any{in} if er != nil { err = er @@ -91,24 +94,18 @@ func FindOneById[T Model, I constraints.Integer](ctx context.Context, id I) (T, return gets[T](globalBb, ctx, QueryCondition{ Fields: "*", Where: SqlBuilder{ - {PrimaryKey[T](), "=", number.ToString(id), "int"}, + {PrimaryKey[T](), "=", number.IntToString(id), "int"}, }, }) } -func FirstOne[T Model](ctx context.Context, where ParseWhere, fields string, order SqlBuilder, in ...[]any) (r T, err error) { - s, args, err := BuildQuerySql[T](QueryCondition{ - Where: where, - Fields: fields, - Order: order, - In: in, - Limit: 1, - }) - if err != nil { - return - } - err = globalBb.Get(ctx, &r, s, args...) - return +func FirstOne[T Model](ctx context.Context, where ParseWhere, fields string, order SqlBuilder, in ...[]any) (T, error) { + return gets[T](globalBb, ctx, Conditions( + Where(where), + Fields(fields), + Order(order), + In(in...), + )) } func LastOne[T Model](ctx context.Context, where ParseWhere, fields string, in ...[]any) (T, error) { @@ -122,10 +119,11 @@ func LastOne[T Model](ctx context.Context, where ParseWhere, fields string, in . } func SimpleFind[T Model](ctx context.Context, where ParseWhere, fields string, in ...[]any) (r []T, err error) { - s, args, err := BuildQuerySql[T](QueryCondition{ + s, args, err := BuildQuerySql(QueryCondition{ Where: where, Fields: fields, In: in, + From: Table[T](), }) if err != nil { return @@ -155,8 +153,9 @@ func Find[T Model](ctx context.Context, where ParseWhere, fields, group string, Having: having, Limit: limit, In: in, + From: Table[T](), } - s, args, err := BuildQuerySql[T](q) + s, args, err := BuildQuerySql(q) if err != nil { return } diff --git a/model/query_test.go b/model/query_test.go index e82238c..87a6e13 100644 --- a/model/query_test.go +++ b/model/query_test.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "github.com/fthvgb1/wp-go/app/pkg/models" "github.com/fthvgb1/wp-go/safety" _ "github.com/go-sql-driver/mysql" "github.com/jmoiron/sqlx" @@ -13,29 +14,31 @@ import ( ) type post struct { - Id uint64 `gorm:"column:ID" db:"ID" json:"ID" form:"ID"` - PostAuthor uint64 `gorm:"column:post_author" db:"post_author" json:"post_author" form:"post_author"` - PostDate time.Time `gorm:"column:post_date" db:"post_date" json:"post_date" form:"post_date"` - PostDateGmt time.Time `gorm:"column:post_date_gmt" db:"post_date_gmt" json:"post_date_gmt" form:"post_date_gmt"` - PostContent string `gorm:"column:post_content" db:"post_content" json:"post_content" form:"post_content"` - PostTitle string `gorm:"column:post_title" db:"post_title" json:"post_title" form:"post_title"` - PostExcerpt string `gorm:"column:post_excerpt" db:"post_excerpt" json:"post_excerpt" form:"post_excerpt"` - PostStatus string `gorm:"column:post_status" db:"post_status" json:"post_status" form:"post_status"` - CommentStatus string `gorm:"column:comment_status" db:"comment_status" json:"comment_status" form:"comment_status"` - PingStatus string `gorm:"column:ping_status" db:"ping_status" json:"ping_status" form:"ping_status"` - PostPassword string `gorm:"column:post_password" db:"post_password" json:"post_password" form:"post_password"` - PostName string `gorm:"column:post_name" db:"post_name" json:"post_name" form:"post_name"` - ToPing string `gorm:"column:to_ping" db:"to_ping" json:"to_ping" form:"to_ping"` - Pinged string `gorm:"column:pinged" db:"pinged" json:"pinged" form:"pinged"` - PostModified time.Time `gorm:"column:post_modified" db:"post_modified" json:"post_modified" form:"post_modified"` - PostModifiedGmt time.Time `gorm:"column:post_modified_gmt" db:"post_modified_gmt" json:"post_modified_gmt" form:"post_modified_gmt"` - PostContentFiltered string `gorm:"column:post_content_filtered" db:"post_content_filtered" json:"post_content_filtered" form:"post_content_filtered"` - PostParent uint64 `gorm:"column:post_parent" db:"post_parent" json:"post_parent" form:"post_parent"` - Guid string `gorm:"column:guid" db:"guid" json:"guid" form:"guid"` - MenuOrder int `gorm:"column:menu_order" db:"menu_order" json:"menu_order" form:"menu_order"` - PostType string `gorm:"column:post_type" db:"post_type" json:"post_type" form:"post_type"` - PostMimeType string `gorm:"column:post_mime_type" db:"post_mime_type" json:"post_mime_type" form:"post_mime_type"` - CommentCount int64 `gorm:"column:comment_count" db:"comment_count" json:"comment_count" form:"comment_count"` + Id uint64 `gorm:"column:ID" db:"ID" json:"ID" form:"ID"` + PostAuthor uint64 `gorm:"column:post_author" db:"post_author" json:"post_author" form:"post_author"` + PostDate time.Time `gorm:"column:post_date" db:"post_date" json:"post_date" form:"post_date"` + PostDateGmt time.Time `gorm:"column:post_date_gmt" db:"post_date_gmt" json:"post_date_gmt" form:"post_date_gmt"` + PostContent string `gorm:"column:post_content" db:"post_content" json:"post_content" form:"post_content"` + PostTitle string `gorm:"column:post_title" db:"post_title" json:"post_title" form:"post_title"` + PostExcerpt string `gorm:"column:post_excerpt" db:"post_excerpt" json:"post_excerpt" form:"post_excerpt"` + PostStatus string `gorm:"column:post_status" db:"post_status" json:"post_status" form:"post_status"` + CommentStatus string `gorm:"column:comment_status" db:"comment_status" json:"comment_status" form:"comment_status"` + PingStatus string `gorm:"column:ping_status" db:"ping_status" json:"ping_status" form:"ping_status"` + PostPassword string `gorm:"column:post_password" db:"post_password" json:"post_password" form:"post_password"` + PostName string `gorm:"column:post_name" db:"post_name" json:"post_name" form:"post_name"` + ToPing string `gorm:"column:to_ping" db:"to_ping" json:"to_ping" form:"to_ping"` + Pinged string `gorm:"column:pinged" db:"pinged" json:"pinged" form:"pinged"` + PostModified time.Time `gorm:"column:post_modified" db:"post_modified" json:"post_modified" form:"post_modified"` + PostModifiedGmt time.Time `gorm:"column:post_modified_gmt" db:"post_modified_gmt" json:"post_modified_gmt" form:"post_modified_gmt"` + PostContentFiltered string `gorm:"column:post_content_filtered" db:"post_content_filtered" json:"post_content_filtered" form:"post_content_filtered"` + PostParent uint64 `gorm:"column:post_parent" db:"post_parent" json:"post_parent" form:"post_parent"` + Guid string `gorm:"column:guid" db:"guid" json:"guid" form:"guid"` + MenuOrder int `gorm:"column:menu_order" db:"menu_order" json:"menu_order" form:"menu_order"` + PostType string `gorm:"column:post_type" db:"post_type" json:"post_type" form:"post_type"` + PostMimeType string `gorm:"column:post_mime_type" db:"post_mime_type" json:"post_mime_type" form:"post_mime_type"` + CommentCount int64 `gorm:"column:comment_count" db:"comment_count" json:"comment_count" form:"comment_count"` + User *user `table:"wp_users user" foreignKey:"ID" local:"post_author" relation:"hasOne"` + PostMeta *[]models.PostMeta `table:"wp_postmeta meta" foreignKey:"post_id" local:"ID" relation:"hasMany"` } type user struct { @@ -362,6 +365,18 @@ func TestFirstOne(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := FirstOne[post](ctx, tt.args.where, tt.args.fields, tt.args.order, tt.args.in...) + gott, err := Gets[post](ctx, Conditions( + Where(SqlBuilder{{"post_status", "publish"}}), + Order([][]string{{"ID", "desc"}}), + With("user", WithConditions( + Fields("ID,user_login,user_pass"), + Where(SqlBuilder{ + {"user.ID", ">", "0", "int"}, + }), + )), + With("meta", nil), + )) + _ = gott if (err != nil) != tt.wantErr { t.Errorf("FirstOne() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/model/querycondition.go b/model/querycondition.go index 26bdeb8..a589d0e 100644 --- a/model/querycondition.go +++ b/model/querycondition.go @@ -26,11 +26,13 @@ func FindFromDB[T Model](db dbQuery, ctx context.Context, q QueryCondition) (r [ } func finds[T Model](db dbQuery, ctx context.Context, q QueryCondition) (r []T, err error) { - sq, args, err := BuildQuerySql[T](q) + setTable[T](&q) + sq, args, err := BuildQuerySql(q) if err != nil { return } err = db.Select(ctx, &r, sq, args...) + return } @@ -179,7 +181,8 @@ func GetFieldFromDB[T Model](db dbQuery, ctx context.Context, field string, q Qu } func getToStringMap[T Model](db dbQuery, ctx context.Context, q QueryCondition) (r map[string]string, err error) { - rawSql, in, err := BuildQuerySql[T](q) + setTable[T](&q) + rawSql, in, err := BuildQuerySql(q) if err != nil { return nil, err } @@ -193,7 +196,8 @@ func GetToStringMap[T Model](ctx context.Context, q QueryCondition) (r map[strin } func findToStringMap[T Model](db dbQuery, ctx context.Context, q QueryCondition) (r []map[string]string, err error) { - rawSql, in, err := BuildQuerySql[T](q) + setTable[T](&q) + rawSql, in, err := BuildQuerySql(q) if err != nil { return nil, err } @@ -217,7 +221,7 @@ func GetToStringMapFromDB[T Model](db dbQuery, ctx context.Context, q QueryCondi return } -func BuildQuerySql[T Model](q QueryCondition) (r string, args []any, err error) { +func BuildQuerySql(q QueryCondition) (r string, args []any, err error) { w := "" if q.Where != nil { w, args, err = q.Where.ParseWhere(&q.In) @@ -251,10 +255,7 @@ func BuildQuerySql[T Model](q QueryCondition) (r string, args []any, err error) } tp := "select %s from %s %s %s %s %s %s %s" l := "" - table := Table[T]() - if q.From != "" { - table = q.From - } + table := q.From if q.Limit > 0 { l = fmt.Sprintf(" limit %d", q.Limit) } @@ -266,7 +267,8 @@ func BuildQuerySql[T Model](q QueryCondition) (r string, args []any, err error) } func findScanner[T Model](db dbQuery, ctx context.Context, fn func(T), q QueryCondition) (err error) { - s, args, err := BuildQuerySql[T](q) + setTable[T](&q) + s, args, err := BuildQuerySql(q) if err != nil { return } @@ -295,10 +297,17 @@ func GetsFromDB[T Model](db dbQuery, ctx context.Context, q QueryCondition) (T, } func gets[T Model](db dbQuery, ctx context.Context, q QueryCondition) (r T, err error) { - s, args, err := BuildQuerySql[T](q) + setTable[T](&q) + s, args, err := BuildQuerySql(q) if err != nil { return } err = db.Get(ctx, &r, s, args...) + if err != nil { + return + } + if len(q.Relation) > 0 { + err = Relation[T](db, ctx, &r, &q) + } return } diff --git a/model/relation.go b/model/relation.go new file mode 100644 index 0000000..57257e4 --- /dev/null +++ b/model/relation.go @@ -0,0 +1,81 @@ +package model + +import ( + "context" + "fmt" + "reflect" + "strings" +) + +func setTable[T Model](q *QueryCondition) { + if q.From == "" { + q.From = Table[T]() + } +} + +func Relation[T Model](db dbQuery, ctx context.Context, r *T, q *QueryCondition) (err error) { + var rr T + t := reflect.TypeOf(rr) + v := reflect.ValueOf(r).Elem() + for tableTag, relation := range q.Relation { + if tableTag == "" { + continue + } + for i := 0; i < t.NumField(); i++ { + tag := t.Field(i).Tag + table, ok := tag.Lookup("table") + if !ok || table == "" { + continue + } + tables := strings.Split(table, " ") + if tables[len(tables)-1] != tableTag { + continue + } + if relation == nil { + relation = &QueryCondition{ + Fields: "*", + } + } + relation.From = table + id := "" + j := 0 + for ; j < t.NumField(); j++ { + vvv, ok := t.Field(j).Tag.Lookup("db") + if ok && vvv == tag.Get("local") { + id = fmt.Sprintf("%v", v.Field(j).Interface()) + break + } + } + { + var w any = relation.Where + if w == nil { + w = SqlBuilder{} + } + ww, ok := w.(SqlBuilder) + if ok { + ww = append(ww, SqlBuilder{{ + tag.Get("foreignKey"), "=", id, "int", + }}...) + relation.Where = ww + } + } + sq, args, er := BuildQuerySql(*relation) + if er != nil { + err = er + return + } + vv := reflect.New(v.Field(i).Type().Elem()).Interface() + switch tag.Get("relation") { + case "hasOne": + err = db.Get(ctx, vv, sq, args...) + case "hasMany": + err = db.Select(ctx, vv, sq, args...) + } + if err != nil { + return + } + v.Field(i).Set(reflect.ValueOf(vv)) + } + } + return +}