From 890f35f0f855dad86adc0b7de32062a824b432a4 Mon Sep 17 00:00:00 2001 From: Duy Phung Ngoc Date: Mon, 19 Dec 2022 19:02:09 +0800 Subject: [PATCH] Add base gorm model with customized ID value type --- Makefile | 7 ++ README.md | 15 +++++ .../entity}/base_aggregate_root.go | 2 +- {entity => domain/entity}/base_entity.go | 0 {event => domain/event}/base_domain_event.go | 0 .../value_object}/string_value_object.go | 0 go.mod | 19 +++++- go.sum | 29 ++++++++ .../custom_gorm}/custom_type_uuidv1.go | 35 +++++++--- infrastructure/persistence/base_model.go | 15 +++++ infrastructure/tests/sqlite_db_connect.go | 59 ++++++++++++++++ samples/entity/account.go | 5 ++ samples/persistence/account/account_model.go | 11 +++ .../persistence/account/account_repository.go | 42 ++++++++++++ .../account/account_repository_test.go | 67 +++++++++++++++++++ 15 files changed, 296 insertions(+), 10 deletions(-) create mode 100644 Makefile create mode 100644 README.md rename {entity => domain/entity}/base_aggregate_root.go (73%) rename {entity => domain/entity}/base_entity.go (100%) rename {event => domain/event}/base_domain_event.go (100%) rename {value_object => domain/value_object}/string_value_object.go (100%) rename {gorm => infrastructure/custom_gorm}/custom_type_uuidv1.go (55%) create mode 100644 infrastructure/persistence/base_model.go create mode 100644 infrastructure/tests/sqlite_db_connect.go create mode 100644 samples/entity/account.go create mode 100644 samples/persistence/account/account_model.go create mode 100644 samples/persistence/account/account_repository.go create mode 100644 samples/persistence/account/account_repository_test.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cd327dd --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +GO_CMD=go +GO_TEST=$(GO_CMD) test -count=1 -v -cover + + +.PHONY:test +test: + @-$(GO_TEST) ./... ||: \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2b56e9 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Fundamental DDD elements in Golang +Fundamental elements of DDD implementation in Golang microservice + + + + +# Components +## Domain Layer +- Entity +- Value Object +- Domain Event + +## Persistence Layer +- Data Model +- Repository \ No newline at end of file diff --git a/entity/base_aggregate_root.go b/domain/entity/base_aggregate_root.go similarity index 73% rename from entity/base_aggregate_root.go rename to domain/entity/base_aggregate_root.go index 89a596e..e67a1dd 100644 --- a/entity/base_aggregate_root.go +++ b/domain/entity/base_aggregate_root.go @@ -1,6 +1,6 @@ package entity -import "github.com/tonybka/go-base-ddd/event" +import "github.com/tonybka/go-base-ddd/domain/event" type BaseAggregateRoot struct { BaseEntity diff --git a/entity/base_entity.go b/domain/entity/base_entity.go similarity index 100% rename from entity/base_entity.go rename to domain/entity/base_entity.go diff --git a/event/base_domain_event.go b/domain/event/base_domain_event.go similarity index 100% rename from event/base_domain_event.go rename to domain/event/base_domain_event.go diff --git a/value_object/string_value_object.go b/domain/value_object/string_value_object.go similarity index 100% rename from value_object/string_value_object.go rename to domain/value_object/string_value_object.go diff --git a/go.mod b/go.mod index 6f7df42..7d32bf3 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,21 @@ module github.com/tonybka/go-base-ddd go 1.18 -require github.com/google/uuid v1.3.0 +require ( + github.com/google/uuid v1.3.0 + gorm.io/gorm v1.24.2 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/mattn/go-sqlite3 v1.14.15 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +require ( + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/stretchr/testify v1.8.1 + gorm.io/driver/sqlite v1.4.3 +) diff --git a/go.sum b/go.sum index 3dfe1c9..138c813 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,31 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= +gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= +gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= +gorm.io/gorm v1.24.2 h1:9wR6CFD+G8nOusLdvkZelOEhpJVwwHzpQOUM+REd6U0= +gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= diff --git a/gorm/custom_type_uuidv1.go b/infrastructure/custom_gorm/custom_type_uuidv1.go similarity index 55% rename from gorm/custom_type_uuidv1.go rename to infrastructure/custom_gorm/custom_type_uuidv1.go index 3820ed0..05f36f1 100644 --- a/gorm/custom_type_uuidv1.go +++ b/infrastructure/custom_gorm/custom_type_uuidv1.go @@ -1,17 +1,20 @@ -package gorm +package customgorm import ( "database/sql/driver" + "errors" + "fmt" "github.com/google/uuid" + "gorm.io/gorm" + "gorm.io/gorm/schema" ) type CustomTypeUUIDv1 uuid.UUID -// NewCustomTypeUUIDv1FromString -> parse string to CustomTypeUUIDv1 -func NewCustomTypeUUIDv1FromString(s string) (CustomTypeUUIDv1, error) { - id, err := uuid.Parse(s) - return CustomTypeUUIDv1(id), err +// CustomTypeUUIDv1FromString -> parse string to CustomTypeUUIDv1 +func CustomTypeUUIDv1FromString(s string) CustomTypeUUIDv1 { + return CustomTypeUUIDv1(uuid.MustParse(s)) } //String -> String Representation of Binary16 @@ -24,6 +27,18 @@ func (my CustomTypeUUIDv1) GormDataType() string { return "binary(16)" } +// GormDBDataType returns gorm DB data type based on the current using database. +func (my CustomTypeUUIDv1) GormDBDataType(db *gorm.DB, field *schema.Field) string { + switch db.Dialector.Name() { + case "mysql": + return "BINARY(16)" + case "sqlite": + return "BLOB" + default: + return "" + } +} + func (my CustomTypeUUIDv1) MarshalJSON() ([]byte, error) { s := uuid.UUID(my) str := "\"" + s.String() + "\"" @@ -39,9 +54,13 @@ func (my *CustomTypeUUIDv1) UnmarshalJSON(by []byte) error { // Scan --> tells GORM how to receive from the database func (my *CustomTypeUUIDv1) Scan(value interface{}) error { - bytes, _ := value.([]byte) - parseByte, err := uuid.FromBytes(bytes) - *my = CustomTypeUUIDv1(parseByte) + bytes, ok := value.([]byte) + if !ok { + return errors.New(fmt.Sprint("Failed to decode value:", value)) + } + + parseBytes, err := uuid.FromBytes(bytes) + *my = CustomTypeUUIDv1(parseBytes) return err } diff --git a/infrastructure/persistence/base_model.go b/infrastructure/persistence/base_model.go new file mode 100644 index 0000000..ea8772b --- /dev/null +++ b/infrastructure/persistence/base_model.go @@ -0,0 +1,15 @@ +package persistence + +import ( + "time" + + customgorm "github.com/tonybka/go-base-ddd/infrastructure/custom_gorm" + "gorm.io/gorm" +) + +type BaseModel struct { + ID customgorm.CustomTypeUUIDv1 `gorm:"primarykey;default:(UUID_TO_BIN(UUID()));"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` +} diff --git a/infrastructure/tests/sqlite_db_connect.go b/infrastructure/tests/sqlite_db_connect.go new file mode 100644 index 0000000..85f10ad --- /dev/null +++ b/infrastructure/tests/sqlite_db_connect.go @@ -0,0 +1,59 @@ +package tests + +import ( + "io/ioutil" + "os" + "path" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +type SqliteDBConnect struct { + dataDir string + dbConnection *gorm.DB +} + +func NewSqliteDBConnect() (*SqliteDBConnect, error) { + var tempDir = "" + + // Temp file setup + tempDir, err := ioutil.TempDir("", "tests-") + if err != nil { + return nil, err + } + + // Database setup + tempDir = path.Join(tempDir, "test.sqlite3") + dialector := sqlite.Open(tempDir) + + dbConn, err := gorm.Open(dialector, &gorm.Config{ + SkipDefaultTransaction: true, + }) + if err != nil { + return nil, err + } + + sqliteDB, err := dbConn.DB() + if err != nil { + return nil, err + } + sqliteDB.SetMaxOpenConns(1) + dbConn.Debug() + + return &SqliteDBConnect{dataDir: tempDir, dbConnection: dbConn}, nil +} + +func (conn *SqliteDBConnect) Connection() *gorm.DB { + return conn.dbConnection +} + +func (conn *SqliteDBConnect) CleanUp() error { + var err error + + if len(conn.dataDir) > 0 { + err = os.RemoveAll(conn.dataDir) + } + + return err +} diff --git a/samples/entity/account.go b/samples/entity/account.go new file mode 100644 index 0000000..59ccaff --- /dev/null +++ b/samples/entity/account.go @@ -0,0 +1,5 @@ +package entity + +// Account is an user account as sample if entity +type Account struct { +} diff --git a/samples/persistence/account/account_model.go b/samples/persistence/account/account_model.go new file mode 100644 index 0000000..956262d --- /dev/null +++ b/samples/persistence/account/account_model.go @@ -0,0 +1,11 @@ +package account + +import ( + "github.com/tonybka/go-base-ddd/infrastructure/persistence" +) + +type AccountModel struct { + persistence.BaseModel + + AccountName string `gorm:"column:account_name;unique"` +} diff --git a/samples/persistence/account/account_repository.go b/samples/persistence/account/account_repository.go new file mode 100644 index 0000000..32c3076 --- /dev/null +++ b/samples/persistence/account/account_repository.go @@ -0,0 +1,42 @@ +package account + +import ( + "github.com/google/uuid" + customgorm "github.com/tonybka/go-base-ddd/infrastructure/custom_gorm" + "gorm.io/gorm" +) + +type AccountRepository struct { + db *gorm.DB +} + +func NewAccountRepository(db *gorm.DB) *AccountRepository { + return &AccountRepository{db} +} + +func (repo *AccountRepository) Create(dataModel AccountModel) error { + if result := repo.db.Create(&dataModel); result.Error != nil { + return result.Error + } + return nil +} + +func (repo *AccountRepository) FindById(id uuid.UUID) (AccountModel, error) { + var dataModel AccountModel + + if result := repo.db.Where("id = ?", customgorm.CustomTypeUUIDv1FromString(id.String())).First(&dataModel); result.Error != nil { + return AccountModel{}, result.Error + } + + return dataModel, nil +} + +func (repo *AccountRepository) GetAll() ([]AccountModel, error) { + var dataModels []AccountModel + + if result := repo.db.Find(&dataModels); result.Error != nil { + return nil, result.Error + } + + return dataModels, nil +} diff --git a/samples/persistence/account/account_repository_test.go b/samples/persistence/account/account_repository_test.go new file mode 100644 index 0000000..7002c24 --- /dev/null +++ b/samples/persistence/account/account_repository_test.go @@ -0,0 +1,67 @@ +package account + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + customgorm "github.com/tonybka/go-base-ddd/infrastructure/custom_gorm" + "github.com/tonybka/go-base-ddd/infrastructure/persistence" + "github.com/tonybka/go-base-ddd/infrastructure/tests" + "gorm.io/gorm" +) + +type AccountRepositoryTestSuite struct { + suite.Suite + + sqliteConnect *tests.SqliteDBConnect + dbConn *gorm.DB + + accountRepo *AccountRepository +} + +func (ts *AccountRepositoryTestSuite) SetupSuite() { + sqliteConn, err := tests.NewSqliteDBConnect() + require.NoError(ts.T(), err) + + ts.sqliteConnect = sqliteConn + ts.dbConn = sqliteConn.Connection() + + ts.dbConn.AutoMigrate(&AccountModel{}) + + ts.accountRepo = NewAccountRepository(ts.dbConn) +} + +func (ts *AccountRepositoryTestSuite) TestCreateAccount() { + entityId := customgorm.CustomTypeUUIDv1FromString(uuid.New().String()) + + account := AccountModel{ + BaseModel: persistence.BaseModel{ + ID: entityId, + }, + AccountName: "abc", + } + + err := ts.accountRepo.Create(account) + ts.NoError(err) + + all, err := ts.accountRepo.GetAll() + ts.NoError(err) + ts.Greater(len(all), 0) + + queriedAccount, err := ts.accountRepo.FindById(uuid.UUID(account.ID)) + ts.NoError(err) + ts.Equal(account.AccountName, queriedAccount.AccountName) + ts.Equal(account.ID, queriedAccount.ID) +} + +func (ts *AccountRepositoryTestSuite) TearDownSuite() { + err := ts.sqliteConnect.CleanUp() + ts.NoError(err) +} + +func TestSuiteRunnerAccountRepository(t *testing.T) { + ts := new(AccountRepositoryTestSuite) + suite.Run(t, ts) +}