Skip to content

Commit

Permalink
generator & db: add db update tools for partial
Browse files Browse the repository at this point in the history
  • Loading branch information
agungdwiprasetyo committed May 10, 2023
1 parent e23b6d5 commit 29b06ef
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 36 deletions.
109 changes: 109 additions & 0 deletions candishared/database_update_tools.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package candishared

import (
"reflect"
"strings"
)

type partialUpdateOption struct {
updateFields map[string]struct{}
ignoreFields map[string]struct{}
}

// DBUpdateOptionFunc option func
type DBUpdateOptionFunc func(*partialUpdateOption)

// DBUpdateSetUpdatedFields option func
func DBUpdateSetUpdatedFields(fields ...string) DBUpdateOptionFunc {
return func(o *partialUpdateOption) {
o.updateFields = make(map[string]struct{})
for _, field := range fields {
o.updateFields[field] = struct{}{}
}
}
}

// DBUpdateSetIgnoredFields option func
func DBUpdateSetIgnoredFields(fields ...string) DBUpdateOptionFunc {
return func(o *partialUpdateOption) {
o.ignoreFields = make(map[string]struct{})
for _, field := range fields {
o.ignoreFields[field] = struct{}{}
}
}
}

// DBUpdateGORMExtractorKey struct tag key extractor for gorm model
func DBUpdateGORMExtractorKey(structTag reflect.StructTag) string {
return strings.Split(strings.TrimPrefix(structTag.Get("gorm"), "column:"), ";")[0]
}

// DBUpdateMongoExtractorKey struct tag key extractor for mongo model
func DBUpdateMongoExtractorKey(structTag reflect.StructTag) string {
return strings.TrimSuffix(structTag.Get("bson"), ",omitempty")
}

// DBUpdateTools for construct selected field to update
type DBUpdateTools struct {
KeyExtractorFunc func(structTag reflect.StructTag) string
IgnoredFields []string
}

// ToMap method
func (d DBUpdateTools) ToMap(data interface{}, opts ...DBUpdateOptionFunc) map[string]interface{} {
var (
o partialUpdateOption
updateFields = make(map[string]interface{}, 0)
)

for _, opt := range opts {
opt(&o)
}

dataValue := reflect.ValueOf(data)
dataType := reflect.TypeOf(data)
if dataValue.Kind() == reflect.Ptr {
dataValue = dataValue.Elem()
dataType = dataType.Elem()
}
isPartial := len(o.updateFields) > 0 || len(o.ignoreFields) > 0

for i := 0; i < dataValue.NumField(); i++ {
fieldValue := dataValue.Field(i)

fieldType := dataType.Field(i)
if fieldType.Anonymous {
for k, v := range d.ToMap(fieldValue.Interface(), opts...) {
updateFields[k] = v
}
continue
}

key := strings.TrimSuffix(fieldType.Tag.Get("json"), ",omitempty")
if d.KeyExtractorFunc != nil {
key = d.KeyExtractorFunc(fieldType.Tag)
}

val := fieldValue.Interface()
if fieldValue.Kind() == reflect.Pointer {
val = fieldValue.Elem().Interface()
}

if !isPartial {
updateFields[key] = val
continue
}

_, isFieldUpdated := o.updateFields[fieldType.Name]
_, isFieldIgnored := o.ignoreFields[fieldType.Name]
if (isFieldUpdated && len(o.updateFields) > 0) || (!isFieldIgnored && len(o.ignoreFields) > 0) {
updateFields[key] = val
}
}

for _, ignored := range d.IgnoredFields {
delete(updateFields, ignored)
}

return updateFields
}
45 changes: 45 additions & 0 deletions candishared/database_update_tools_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package candishared

import (
"testing"

"github.com/golangid/candi/candihelper"
"github.com/stretchr/testify/assert"
)

func TestDBUpdateTools(t *testing.T) {

type SubModel struct {
Title string `gorm:"column:title" json:"title"`
Profile string `gorm:"column:profile" json:"profile"`
}

type Model struct {
ID int `gorm:"column:db_id;" json:"id"`
Name *string `gorm:"column:db_name;" json:"name"`
Address string `gorm:"column:db_address" json:"address"`
SubModel
}

updated := DBUpdateTools{KeyExtractorFunc: DBUpdateGORMExtractorKey}.ToMap(
&Model{ID: 1, Name: candihelper.ToStringPtr("01"), Address: "street", SubModel: SubModel{Title: "test"}},
DBUpdateSetUpdatedFields("ID", "Name", "Title"),
)
assert.Equal(t, 3, len(updated))
assert.Equal(t, 1, updated["db_id"])
assert.Equal(t, "01", updated["db_name"])
assert.Equal(t, "test", updated["title"])

updated = DBUpdateTools{}.ToMap(
Model{ID: 1, Name: candihelper.ToStringPtr("01"), Address: "street", SubModel: SubModel{Title: "test"}},
DBUpdateSetIgnoredFields("ID", "Name", "Title"),
)
assert.Equal(t, 2, len(updated))
assert.Equal(t, "street", updated["address"])

updated = DBUpdateTools{}.ToMap(
Model{ID: 1, Name: candihelper.ToStringPtr("01"), Address: "street", SubModel: SubModel{Title: "test"}},
)
assert.Equal(t, 5, len(updated))
assert.Equal(t, "street", updated["address"])
}
8 changes: 4 additions & 4 deletions cmd/candi/template_domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import (
// {{upper (camel .ModuleName)}} model
type {{upper (camel .ModuleName)}} struct {
ID {{if and .MongoDeps (not .SQLDeps)}}primitive.ObjectID{{else}}int{{end}} ` + "`" + `{{if .SQLUseGORM}}gorm:"column:id;primary_key" {{end}}` + `{{if .MongoDeps}}bson:"_id" {{end}}` + `json:"id"` + "`" + `
Field string ` + "`" + `{{if .SQLUseGORM}}gorm:"column:field;type:varchar(255)" {{end}}` + `{{if .MongoDeps}}bson:"field" {{end}}` + `json:"field"` + "`" + `
CreatedAt time.Time ` + "`" + `{{if .SQLUseGORM}}gorm:"column:created_at" {{end}}` + `{{if .MongoDeps}}bson:"created_at" {{end}}` + `json:"created_at"` + "`" + `
UpdatedAt time.Time ` + "`" + `{{if .SQLUseGORM}}gorm:"column:updated_at" {{end}}` + `{{if .MongoDeps}}bson:"updated_at" {{end}}` + `json:"updated_at"` + "`" + `
ID {{if and .MongoDeps (not .SQLDeps)}}primitive.ObjectID{{else}}int{{end}} ` + "`" + `{{if .SQLUseGORM}}gorm:"column:id;primary_key" {{else}}sql:"id" {{end}}` + `{{if .MongoDeps}}bson:"_id" {{end}}` + `json:"id"` + "`" + `
Field string ` + "`" + `{{if .SQLUseGORM}}gorm:"column:field;type:varchar(255)" {{else}}sql:"field" {{end}}` + `{{if .MongoDeps}}bson:"field" {{end}}` + `json:"field"` + "`" + `
CreatedAt time.Time ` + "`" + `{{if .SQLUseGORM}}gorm:"column:created_at" {{else}}sql:"created_at" {{end}}` + `{{if .MongoDeps}}bson:"created_at" {{end}}` + `json:"created_at"` + "`" + `
UpdatedAt time.Time ` + "`" + `{{if .SQLUseGORM}}gorm:"column:updated_at" {{else}}sql:"updated_at" {{end}}` + `{{if .MongoDeps}}bson:"updated_at" {{end}}` + `json:"updated_at"` + "`" + `
}
{{if .SQLUseGORM}}
// TableName return table name of {{upper (camel .ModuleName)}} model
Expand Down
43 changes: 33 additions & 10 deletions cmd/candi/template_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,14 +332,16 @@ import (
"{{$.PackagePrefix}}/internal/modules/{{cleanPathModule .ModuleName}}/domain"
shareddomain "{{$.PackagePrefix}}/pkg/shared/domain"
"{{.LibraryName}}/candishared"
)
// {{upper (camel .ModuleName)}}Repository abstract interface
type {{upper (camel .ModuleName)}}Repository interface {
FetchAll(ctx context.Context, filter *domain.Filter{{upper (camel .ModuleName)}}) ([]shareddomain.{{upper (camel .ModuleName)}}, error)
Count(ctx context.Context, filter *domain.Filter{{upper (camel .ModuleName)}}) int
Find(ctx context.Context, filter *domain.Filter{{upper (camel .ModuleName)}}) (shareddomain.{{upper (camel .ModuleName)}}, error)
Save(ctx context.Context, data *shareddomain.{{upper (camel .ModuleName)}}) error
Save(ctx context.Context, data *shareddomain.{{upper (camel .ModuleName)}}, updateOptions ...candishared.DBUpdateOptionFunc) error
Delete(ctx context.Context, filter *domain.Filter{{upper (camel .ModuleName)}}) (err error)
}
`
Expand All @@ -361,12 +363,14 @@ import (
shareddomain "{{$.PackagePrefix}}/pkg/shared/domain"
"{{.LibraryName}}/candihelper"
"{{.LibraryName}}/candishared"
"{{.LibraryName}}/tracer"
)
type {{camel .ModuleName}}RepoMongo struct {
readDB, writeDB *mongo.Database
collection string
updateTools *candishared.DBUpdateTools
}
// New{{upper (camel .ModuleName)}}RepoMongo mongo repo constructor
Expand All @@ -375,6 +379,10 @@ func New{{upper (camel .ModuleName)}}RepoMongo(readDB, writeDB *mongo.Database)
readDB: readDB,
writeDB: writeDB,
collection: shareddomain.{{upper (camel .ModuleName)}}{}.CollectionName(),
updateTools: &candishared.DBUpdateTools{
KeyExtractorFunc: candishared.DBUpdateMongoExtractorKey,
IgnoredFields: []string{"_id"},
},
}
}
Expand Down Expand Up @@ -433,17 +441,20 @@ func (r *{{camel .ModuleName}}RepoMongo) Count(ctx context.Context, filter *doma
return int(count)
}
func (r *{{camel .ModuleName}}RepoMongo) Save(ctx context.Context, data *shareddomain.{{upper (camel .ModuleName)}}) (err error) {
func (r *{{camel .ModuleName}}RepoMongo) Save(ctx context.Context, data *shareddomain.{{upper (camel .ModuleName)}}, updateOptions ...candishared.DBUpdateOptionFunc) (err error) {
trace, ctx := tracer.StartTraceWithContext(ctx, "{{upper (camel .ModuleName)}}RepoMongo:Save")
defer func() { trace.Finish(tracer.FinishWithError(err)) }()
tracer.Log(ctx, "data", data)
data.UpdatedAt = time.Now()
if data.ID{{if and .MongoDeps (not .SQLDeps)}}.IsZero(){{else}} == 0{{end}} {
data.ID = {{if and .MongoDeps (not .SQLDeps)}}primitive.NewObjectID(){{else}}r.Count(ctx, &domain.Filter{{upper (camel .ModuleName)}}{}) + 1{{end}}
data.CreatedAt = time.Now()
_, err = r.writeDB.Collection(r.collection).InsertOne(ctx, data)
trace.Log("data", data)
} else {
updated := bson.M(r.updateTools.ToMap(data, updateOptions...))
trace.Log("updated", updated)
opt := options.UpdateOptions{
Upsert: candihelper.ToBoolPtr(true),
}
Expand All @@ -452,10 +463,11 @@ func (r *{{camel .ModuleName}}RepoMongo) Save(ctx context.Context, data *sharedd
"_id": data.ID,
},
bson.M{
"$set": data,
"$set": updated,
}, &opt)
}
trace.SetTag("id", data.ID.Hex())
return
}
Expand Down Expand Up @@ -534,7 +546,7 @@ func (r *{{camel .ModuleName}}RepoArango) Count(ctx context.Context, filter *dom
return total
}
func (r *{{camel .ModuleName}}RepoArango) Save(ctx context.Context, data *shareddomain.{{upper (camel .ModuleName)}}) (err error) {
func (r *{{camel .ModuleName}}RepoArango) Save(ctx context.Context, data *shareddomain.{{upper (camel .ModuleName)}}, updateOptions ...candishared.DBUpdateOptionFunc) (err error) {
trace, ctx := tracer.StartTraceWithContext(ctx, "{{upper (camel .ModuleName)}}RepoArango:Save")
defer func() { trace.Finish(tracer.FinishWithError(err)) }()
tracer.Log(ctx, "data", data)
Expand Down Expand Up @@ -576,12 +588,16 @@ import (
type {{camel .ModuleName}}RepoSQL struct {
readDB, writeDB *{{if .SQLUseGORM}}gorm{{else}}sql{{end}}.DB
updateTools *candishared.DBUpdateTools
}
// New{{upper (camel .ModuleName)}}RepoSQL mongo repo constructor
func New{{upper (camel .ModuleName)}}RepoSQL(readDB, writeDB *{{if .SQLUseGORM}}gorm{{else}}sql{{end}}.DB) {{upper (camel .ModuleName)}}Repository {
return &{{camel .ModuleName}}RepoSQL{
readDB, writeDB,
readDB: readDB, writeDB: writeDB,
updateTools: &candishared.DBUpdateTools{
{{if .SQLUseGORM}}KeyExtractorFunc: candishared.DBUpdateGORMExtractorKey, {{end}}IgnoredFields: []string{"id"},
},
}
}
Expand Down Expand Up @@ -652,7 +668,7 @@ func (r *{{camel .ModuleName}}RepoSQL) Find(ctx context.Context, filter *domain.
{{end}}return
}
func (r *{{camel .ModuleName}}RepoSQL) Save(ctx context.Context, data *shareddomain.{{upper (camel .ModuleName)}}) (err error) {
func (r *{{camel .ModuleName}}RepoSQL) Save(ctx context.Context, data *shareddomain.{{upper (camel .ModuleName)}}, updateOptions ...candishared.DBUpdateOptionFunc) (err error) {
trace, ctx := tracer.StartTraceWithContext(ctx, "{{upper (camel .ModuleName)}}RepoSQL:Save")
defer func() { trace.Finish(tracer.FinishWithError(err)) }()
Expand All @@ -667,7 +683,7 @@ func (r *{{camel .ModuleName}}RepoSQL) Save(ctx context.Context, data *shareddom
if data.ID == 0 {
err = {{ if .IsMonorepo }}global{{end}}shared.SetSpanToGorm(ctx, db).Create(data).Error
} else {
err = {{ if .IsMonorepo }}global{{end}}shared.SetSpanToGorm(ctx, db).Save(data).Error
err = {{ if .IsMonorepo }}global{{end}}shared.SetSpanToGorm(ctx, db).Model(data).Updates(r.updateTools.ToMap(data, updateOptions...)).Error
}
{{else}}var query string
var args []interface{}
Expand All @@ -680,8 +696,15 @@ func (r *{{camel .ModuleName}}RepoSQL) Save(ctx context.Context, data *shareddom
query = "INSERT INTO {{snake .ModuleName}}s (field, created_at, updated_at) VALUES ({{if eq .SQLDriver "postgres"}}$1,$2,$3{{else}}?,?,?{{end}})"
args = []interface{}{data.Field, data.CreatedAt, data.UpdatedAt}
} else {
query = "UPDATE {{snake .ModuleName}}s SET field={{if eq .SQLDriver "postgres"}}$1{{else}}?{{end}}, created_at={{if eq .SQLDriver "postgres"}}$2{{else}}?{{end}}, updated_at={{if eq .SQLDriver "postgres"}}$3{{else}}?{{end}} WHERE id={{if eq .SQLDriver "postgres"}}$4{{else}}?{{end}}"
args = []interface{}{data.Field, data.CreatedAt, data.UpdatedAt, data.ID}
var updatedFields []string{{if eq .SQLDriver "postgres"}}
i := 1{{end}}
for field, val := range r.updateTools.ToMap(data, updateOptions...) {
args = append(args, val)
updatedFields = append(updatedFields, {{if eq .SQLDriver "postgres"}}fmt.Sprintf("%s=$%d", field, i))
i++{{else}}fmt.Sprintf("%s=?", field)){{end}}
}
query = fmt.Sprintf("UPDATE {{snake .ModuleName}}s SET %s WHERE id={{if eq .SQLDriver "postgres"}}$%d", strings.Join(updatedFields, ", "), i){{else}}?", strings.Join(updatedFields, ", ")){{end}}
args = append(args, data.ID)
}
trace.Log("query", query)
trace.Log("args", args)
Expand Down
9 changes: 7 additions & 2 deletions cmd/candi/template_usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,8 @@ import (
"context"
"{{$.PackagePrefix}}/internal/modules/{{cleanPathModule .ModuleName}}/domain"
{{if .SQLDeps}}
"{{.LibraryName}}/candishared"{{end}}
"{{.LibraryName}}/tracer"
)
Expand All @@ -351,8 +353,11 @@ func (uc *{{camel .ModuleName}}UsecaseImpl) Update{{upper (camel .ModuleName)}}(
return err
}
existing.Field = data.Field
err = uc.repo{{if .SQLDeps}}SQL{{else if .MongoDeps}}Mongo{{else if .ArangoDeps}}Arango{{end}}.{{upper (camel .ModuleName)}}Repo().Save(ctx, &existing){{end}}
return
{{if .SQLDeps}}err = uc.repoSQL.WithTransaction(ctx, func(ctx context.Context) error {
return uc.repoSQL.{{upper (camel .ModuleName)}}Repo().Save(ctx, &existing, candishared.DBUpdateSetUpdatedFields("Field"))
}){{else}}
err = uc.repo{{if .MongoDeps}}Mongo{{else if .ArangoDeps}}Arango{{end}}.{{upper (camel .ModuleName)}}Repo().Save(ctx, &existing){{end}}
return{{end}}
}
`

Expand Down
12 changes: 6 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ require (
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.3 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/klauspost/compress v1.16.0 // indirect
github.com/klauspost/compress v1.16.4 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/pierrec/lz4/v4 v4.1.17 // indirect
github.com/pkg/errors v0.9.1 // indirect
Expand All @@ -73,11 +73,11 @@ require (
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading

0 comments on commit 29b06ef

Please sign in to comment.