Go rollback transactions in service level - go

I use an interface to interact with my entGO repository. But I have a problem: I'm using the CRUD model to design my repository, and I want to know if I need to use Transaction to create a user and then generate a token. I need to do that with Transaction to roll back in case of failure. My problem is I can only use my client at a repository level. What is the best manner to handle the transaction? Do I need to create a custom function to handle the Transaction or design my function to take transaction parameters? Here is an example of what I have:
// mariaUserRepository is data/repository implementation
// of service layer UserRepository
type mariaUserRepository struct {
Client *ent.Client
}
// NewUserRepository is a factory for initializing User Repositories
func NewUserRepository(client *ent.Client) models.UserRepository {
return &mariaUserRepository{
Client: client,
}
}
// Create reaches out to database entGO api
func (r *mariaUserRepository) Create(ctx context.Context, u *ent.User) error {
// check if a user already exist
check, err := r.Client.User.
Query().
Where(user.Email(u.Email)).Exist(ctx)
if err != nil {
utils.Logger.Info("Error when checking user",
zap.String("email", u.Email),
zap.Error(err),
)
return handle_errors.NewInternal()
}
if check {
err := handle_errors.NewConflict("email", u.Email)
utils.Logger.Info("Error when Register User",
zap.String("email", u.Email),
zap.Error(err),
)
return err
}
if u, err = r.Client.User.
Create().
SetFirstName(u.FirstName).
SetLastName(u.LastName).
SetEmail(u.Email).
SetGender(u.Gender).
SetUserType(u.UserType).
SetPasswordHash(u.PasswordHash).
Save(ctx); err != nil {
utils.Logger.Info("Error when create user",
zap.Any("User", u),
zap.Error(err),
)
return handle_errors.NewInternal()
}
return nil
}
My problem come from the comment that say to implement rollback in case of failure.
err := h.UserService.Signup(ctx, u)
if err != nil {
log.Printf("Failed to sign up user: %v\n", err.Error())
c.JSON(apperrors.Status(err), gin.H{
"error": err,
})
return
}
// create token pair as strings
tokens, err := h.TokenService.NewPairFromUser(ctx, u, "")
if err != nil {
log.Printf("Failed to create tokens for user: %v\n", err.Error())
// may eventually implement rollback logic here
// meaning, if we fail to create tokens after creating a user,
// we make sure to clear/delete the created user in the database
c.JSON(apperrors.Status(err), gin.H{
"error": err,
})
return
}
```

Related

GORM and PostgreSQL: search_path is used only when doing the first request

I'm currently working on an api gateway that forwards requests to an grpc client. The routes use a middleware. Within that middleware the Validate function from the dialed grpc client is used. The function is in the last code snippet at the end.
The gateway is based on a gin router, which, as far as I understand handles each request in a separat go routine. I can send as many requests simultaneously to either to endpoint /protected/1 or to endpoint /protected/2 and they get handled correctly.
The Validate function is having problems handling request sent simultaneously to /protected/1 and /protected/2. Sending two request to both endpoints simultaneously results in that either both, none or only one of the requests is handled correctly. The error message that I receive is actually from the gorm function trying to query the user in the Validate function (ERROR: relation "users" does not exist (SQLSTATE 42P01)). I use a gorm database object that is connected to a postgres database. This behaviour suggests that some how a resource is shared. Using run -race does not give any insights.
So my questions are, why does this setup not work to send requests simultaneously to both endpoints?
I tried to make a working minimal example, but somehow I could nor reproduce the error in a minimal setting, hence I would like to share my code snippets here.
Important snippets of the the Api Gateway implementation
func main() {
c, err := config.LoadConfig()
...
r := gin.Default()
auth.RegisterRoutes(r, &c)
r.Run(":" + c.Port)
}
whereas the routes are defined as
func RegisterRoutes(r *gin.Engine, c *config.Config){
svc := &ServiceClient{
Client: InitServiceClient(c),
}
a := InitAuthMiddleware(svc)
// routes
protected := r.Group("/protected")
protected.Use(a.AuthRequired)
protected.GET("/1", svc.DoStuff)
protected.GET("/2", svc.DoStuff)
}
// dummy handler
func (svc *ServiceClient) DoStuff(ctx *gin.Context) {
handler.DoStuff(ctx, svc.Client)
}
The grpc client is dialed with the following function
type ServiceClient struct {
Client pb.AuthServiceClient
}
func InitServiceClient(c *config.Config) pb.AuthServiceClient {
// using WithInsecure() because no SSL running
cc, err := grpc.Dial(c.AuthSvcUrl, grpc.WithInsecure())
if err != nil {
fmt.Println("Could not connect:", err)
}
return pb.NewAuthServiceClient(cc)
}
and the middleware is implemented in the following way:
type AuthMiddlewareConfig struct {
svc *ServiceClient
}
func InitAuthMiddleware(svc *ServiceClient) AuthMiddlewareConfig {
return AuthMiddlewareConfig{svc}
}
func (c *AuthMiddlewareConfig) AuthRequired(ctx *gin.Context) {
access_token, _ := ctx.Cookie("access_token")
res, err := c.svc.Client.Validate(context.Background(), &pb.ValidateRequest{
Token: access_token,
TokenType: "ACCESS_TOKEN",
Url: ctx.Request.URL.String(),
})
if err != nil || res.Status != http.StatusOK {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"status": http.StatusUnauthorized, "error": res.Error})
return
}
ctx.Set("userId", res.UserId)
ctx.Next()
}
Important snippets of the Auth Service:
func serve(c *conf.Configuration) {
/*
Dial returns:
type Repository struct {
DB *gorm.DB
}
*/
h := storage.Dial(&c.DB)
serviceUri := fmt.Sprintf(":%s", c.API.Port)
lis, err := net.Listen("tcp", serviceUri)
if err != nil {
logrus.Fatal("Failed to listen on: ", err)
}
s := api.Server{
R: h,
//
}
grpcServer := grpc.NewServer()
pb.RegisterAuthServiceServer(grpcServer, &s)
if err := grpcServer.Serve(lis); err != nil {
logrus.Fatalln("Failed to serve:", err)
}
}
and the validate function is implemented by
type Server struct {
R storage.Repository
//
// #https://github.com/grpc/grpc-go/issues/3794:
pb.UnimplementedAuthServiceServer
}
func (s *Server) Validate(ctx context.Context, req *pb.ValidateRequest) (*pb.ValidateResponse, error) {
// token validation stuff works
var user models.User
// causes error sometimes
if result := s.R.DB.Where(&models.User{Id: claims.Id}).First(&user); result.Error != nil {
return &pb.ValidateResponse{
Status: http.StatusNotFound,
Error: "User not found",
}, nil
}
// send response
}
EDIT
For completeness here the function that connects to the postgres client.
func openDb(c *conf.PostgresConfiguration, gormConfig *gorm.Config, database string) *gorm.DB {
connectionString := fmt.Sprintf("postgres://%s:%s#%s:%s/%s", c.User, c.Password, c.Host, c.Port, database)
db, err := gorm.Open(postgres.Open(connectionString), gormConfig)
if err != nil {
logrus.Fatal(fmt.Sprintf("Unable to connect to database '%s' given url: ", database), err)
}
logrus.Info(fmt.Sprintf("Connected to database %s", database))
return db
}
func Dial(c *conf.PostgresConfiguration) Repository {
gormDefaultConfig := &gorm.Config{}
gormAppConfig := &gorm.Config{}
// connect to default database
logrus.Info(fmt.Sprintf("Use default database %s for initialization", c.DefaultDatabase))
defaultDb := openDb(c, gormDefaultConfig, c.DefaultDatabase)
// check if database exists and create it if necessary
var dbexists bool
dbSQL := fmt.Sprintf("SELECT EXISTS (SELECT FROM pg_database WHERE datname = '%s') AS dbexists;", c.AppDatabase)
defaultDb.Raw(dbSQL).Row().Scan(&dbexists)
if !dbexists {
logrus.Info(fmt.Sprintf("Created database %s", c.AppDatabase))
db := defaultDb.Exec(fmt.Sprintf("CREATE DATABASE %s;", c.AppDatabase))
if db.Error != nil {
logrus.Fatal("Unable to create app database: ", db.Error)
}
}
// connect to app databse
appDb := openDb(c, gormAppConfig, c.AppDatabase)
// check if schema exists and create it if necessary
var schemaexists bool
schemaSQL := fmt.Sprintf("SELECT EXISTS(SELECT FROM pg_namespace WHERE nspname = '%s') AS schemaexisits;", c.AppDatabaseSchema)
appDb.Raw(schemaSQL).Row().Scan(&schemaexists)
// create app specfic database, if not already existing
if !schemaexists {
// create service specific schema
db := appDb.Exec(fmt.Sprintf("CREATE SCHEMA %s;", c.AppDatabaseSchema))
if db.Error != nil {
logrus.Fatal(fmt.Sprintf("Unable to create database schema %s", c.AppDatabaseSchema), db.Error)
}
logrus.Info(fmt.Sprintf("Created database schema %s", c.AppDatabaseSchema))
}
db := appDb.Exec(fmt.Sprintf(`set search_path='%s';`, c.AppDatabaseSchema))
logrus.Info(fmt.Sprintf("Use existing database schema %s", c.AppDatabaseSchema))
if db.Error != nil {
logrus.Fatal(fmt.Sprintf("Unable to set search_path for database to schema %s", c.AppDatabaseSchema), db.Error)
}
// migrate table
appDb.AutoMigrate(&models.User{})
return Repository{appDb}
}
Edit2
Changing the connection string in the openDb function to connectionString := fmt.Sprintf("postgres://%s:%s#%s:%s/%s?search_path=%s", c.User, c.Password, c.Host, c.Port, database, c.AppDatabaseSchema) fixes the issue (see how the search_path is part of the url).
Thank you!

Can I connect to Memgraph using Go?

I'd like to connect from Go to the running instance of the Memgraph database. I'm using Docker and I've installed the Memgraph Platform. What exactly do I need to do?
The procedure for connecting fro Go to Memgraph is rather simple. For this you need to use Bolt protocol. Here are the needed steps:
First, create a new directory for your app, /MyApp, and position yourself in it. Next, create a program.go file with the following code:
package main
import (
"fmt"
"github.com/neo4j/neo4j-go-driver/v4/neo4j"
)
func main() {
dbUri := "bolt://localhost:7687"
driver, err := neo4j.NewDriver(dbUri, neo4j.BasicAuth("username", "password", ""))
if err != nil {
panic(err)
}
// Handle driver lifetime based on your application lifetime requirements driver's lifetime is usually
// bound by the application lifetime, which usually implies one driver instance per application
defer driver.Close()
item, err := insertItem(driver)
if err != nil {
panic(err)
}
fmt.Printf("%v\n", item.Message)
}
func insertItem(driver neo4j.Driver) (*Item, error) {
// Sessions are short-lived, cheap to create and NOT thread safe. Typically create one or more sessions
// per request in your web application. Make sure to call Close on the session when done.
// For multi-database support, set sessionConfig.DatabaseName to requested database
// Session config will default to write mode, if only reads are to be used configure session for
// read mode.
session := driver.NewSession(neo4j.SessionConfig{})
defer session.Close()
result, err := session.WriteTransaction(createItemFn)
if err != nil {
return nil, err
}
return result.(*Item), nil
}
func createItemFn(tx neo4j.Transaction) (interface{}, error) {
records, err := tx.Run(
"CREATE (a:Greeting) SET a.message = $message RETURN 'Node ' + id(a) + ': ' + a.message",
map[string]interface{}{"message": "Hello, World!"})
// In face of driver native errors, make sure to return them directly.
// Depending on the error, the driver may try to execute the function again.
if err != nil {
return nil, err
}
record, err := records.Single()
if err != nil {
return nil, err
}
// You can also retrieve values by name, with e.g. `id, found := record.Get("n.id")`
return &Item{
Message: record.Values[0].(string),
}, nil
}
type Item struct {
Message string
}
Now, create a go.mod file using the go mod init example.com/hello command.
I've mentioned the Bolt driver earlier. You need to add it with go get github.com/neo4j/neo4j-go-driver/v4#v4.3.1. You can run your program with go run .\program.go.
The complete documentation is located at Memgraph site.

How to implement multitenancy with one Keycloak realm per tenant in go

Currently I am trying to implement multitenancy in an OAuth2 secured application by using one Keycloak realm for each tenant. I am creating a prototype in Go but am not really bound to the language and could switch to Node.js or Java if I needed to. I figure that my following question would hold true if I switched language though.
At first, implementing multitenancy seemed pretty straight forward to me:
For each tenant, create a realm with the needed client configuration for my backend application.
The backend receives a request with the URL tenant-1.my-app.com. Parse that URL to retrieve the tenant to be used for authentication.
Connect to the OAuth2 provider (Keycloak in this case) and verify the request token.
Following a guide, I use golang.org/x/oauth2 and github.com/coreos/go-oidc. This is how I setup the OAuth 2 connection for a single realm:
provider, err := oidc.NewProvider(context.Background(), "http://keycloak.docker.localhost/auth/realms/tenant-1")
if err != nil {
panic(err)
}
oauth2Config := oauth2.Config{
ClientID: "my-app",
ClientSecret: "my-app-secret",
RedirectURL: "http://tenant-1.my-app.com/auth-callback",
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
state := "somestate"
verifier := provider.Verifier(&oidc.Config{
ClientID: "my-app",
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tenant, err := getTenantFromRequest(r)
if err != nil {
log.Println(err)
w.WriteHeader(400)
return
}
log.Printf("Received request for tenant %s\n", tenant)
// Check if auth is present
rawAccessToken := r.Header.Get("Authorization")
if rawAccessToken == "" {
log.Println("No Auth present, redirecting to auth code url...")
http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound)
return
}
// Check if auth is valid
parts := strings.Split(rawAccessToken, " ")
if len(parts) != 2 {
w.WriteHeader(400)
return
}
_, err = verifier.Verify(context.Background(), parts[1])
if err != nil {
log.Printf("Error during auth verification (%s), redirecting to auth code url...\n", err)
http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound)
return
}
// Authentication okay
w.WriteHeader(200)
})
log.Printf("Starting server (%s)...\n", proxyConfig.Url)
log.Fatal(http.ListenAndServe(proxyConfig.Url, nil))
This works fine, but now comes the next step, adding multitenancy. IMO it seems like I would need to create one oidc.Provider for every tenant, because the realm endpoint (http://keycloak.docker.localhost/auth/realms/tenant-1) needs to be set on in the Provider struct.
I am unsure if this is the correct way to approach this situation. I guess I would add a cache for oidc.Provider instances to avoid creating instances on every request. But is creating one
Even though this hasn't been fully used in production, here is how I prototyped a solution that I will probably end up using:
I assume that there must be a single oidc.Provider for every keycloak realm.
Therefore there will also always be one oidc.IDTokenVerifier for every realm.
To manage these instances, I created this interface:
// A mechanism that manages oidc.Provider and IDTokenVerifierInterface instances
type OAuth2ManagerInterface interface {
GetOAuthProviderForKeycloakRealm(ctx context.Context, tenant string) (*oidc.Provider, error)
GetOpenIdConnectVerifierForProvider(provider *oidc.Provider) (IDTokenVerifierInterface, error)
}
// Interface created to describe the oidc.IDTokenVerifier struct.
// This was created because the oidc modules does not define its own interface
type IDTokenVerifierInterface interface {
Verify(ctx context.Context, rawIDToken string) (*oidc.IDToken, error)
}
Then this is the struct that will implement the manager interface:
type OAuth2Manager struct {
ProviderUrl string
ClientId string
// Maps keycloak provider urls onto oidc.Provider instances
// Used internally to avoid creating a new provider on each request
providers map[string]*oidc.Provider
// Lock to be used when accessing OAuth2Manager.providers
providerLock sync.Mutex
// Maps oidc.Provider instances onto oidc.IDTokenVerifier instances
// Used internally to avoid creating a new verifier on each request
verifiers map[*oidc.Provider]*oidc.IDTokenVerifier
// Lock to be used when accessing OAuth2Manager.verifiers
verifierLock sync.Mutex
}
Along with the functions that actually implement the interface:
func (manager *OAuth2Manager) GetProviderUrlForRealm(realm string) string {
return fmt.Sprintf("%s/%s", manager.ProviderUrl, realm)
}
func (manager *OAuth2Manager) GetOAuthProviderForKeycloakRealm(ctx context.Context, tenant string) (*oidc.Provider, error) {
providerUrl := manager.GetProviderUrlForRealm(tenant)
// Check if already exists ...
manager.providerLock.Lock()
if provider, alreadyExists := manager.providers[providerUrl]; alreadyExists {
manager.providerLock.Unlock()
return provider, nil
}
// ... create new instance if not
provider, err := oidc.NewProvider(ctx, providerUrl)
if err != nil {
manager.providerLock.Unlock()
return nil, err
}
manager.providers[providerUrl] = provider
manager.providerLock.Unlock()
log.Printf("Created new provider for provider url %s\n", providerUrl)
return provider, nil
}
func (manager *OAuth2Manager) GetOpenIdConnectVerifierForProvider(provider *oidc.Provider) (IDTokenVerifierInterface, error) {
// Check if already exists ...
manager.verifierLock.Lock()
if verifier, alreadyExists := manager.verifiers[provider]; alreadyExists {
manager.verifierLock.Unlock()
return verifier, nil
}
// ... create new instance if not
oidcConfig := &oidc.Config{
ClientID: manager.ClientId,
}
verifier := provider.Verifier(oidcConfig)
manager.verifiers[provider] = verifier
manager.verifierLock.Unlock()
log.Printf("Created new verifier for OAuth endpoint %v\n", provider.Endpoint())
return verifier, nil
}
It is important to use sync.Mutex locks if the providers are accessed concurrently.

How to retrieve the authenticated user in Golang

func Login(c echo.Context) error {
user := &users.User{}
if err := c.Bind(&user); err != nil {
return err
}
return token.SigIn(c, user.Email, user.Password)
}
This is my Login function that retrieve the token when the user send the requests.
the Signin func that handle the token
func SigIn(c echo.Context, email, password string) error {
user := users.User{}
db := database.SetUp()
if err := db.Where("email = ?", email).First(&user).Error; gorm.IsRecordNotFoundError(err) {
restErr := errors.NewBadRequestError("Invalid credentials")
return c.JSON(http.StatusBadRequest, restErr)
}
if user.VerifyPassword(password) != nil {
restErr := errors.NewUnauthorizedError("Couldn't log you in with these credentials")
return c.JSON(http.StatusUnauthorized, restErr)
}
//user is successfull
return CreateToken(c)
}
the CreateToken func is as follow
type TokenJWT struct {
Token string `json:"token"`
}
func CreateToken(c echo.Context) error {
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims["authorized"] = true
claims["name"] = "Pascal Gaetan"
claims["exp"] = time.Now().Add(time.Hour * 1).Unix()
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("my_secret_key"))
if err != nil {
return err
}
return c.JSON(http.StatusOK, TokenJWT{
Token: t,
})
}
when everyhting is succesfull, i would like to get the authenticated user through an URL /api/me that calls a Me function
Let me split your question into two parts: the first one is how to easily encode and decode user in or from JWT token and the second part is how to write a generic code which can retrieve user from everywhere.
From your example I mentioned that you created a MapClaims but to reduce parsing complexity it will be better to create a token using a custom claims type. If you are using dgrijalva/jwt-go, then according to documentation you can do something like that
type UserClaims struct {
Name string `json:"name"`
jwt.StandardClaims
}
// encode it as before, but with your created type
t := jwt.New(signer)
userClaims := &UserClaims{Name: "Burmese"}
t.Claims = userClaims
tokenString, err = t.SignedString(]byte("my_secret_key"))
then you can parse your user in your router/framework middleware with
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c"
token, err := jwt.ParseWithClaims(tokenString, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte("my_secret_key"), nil
})
if claims, ok := token.Claims.(*UserClaims); ok && token.Valid {
fmt.Printf("%v %v", claims.Name, claims.StandardClaims.ExpiresAt)
} else {
fmt.Println(err)
}
This example was adopted from an official documentation here
Now you know how to parse authenticated user struct with ease and the next logic move is to wrap it into your middleware. Whether there are a lot of implementation details like you can retrieve JWT from cookie, header or query, also defining some ordering on them, the gist the following: you should have wrapped abovementioned code into your middleware and after parsing the struct you can pass it via your request context. I don't use echo and other frameworks, but for pure net/http you can pass your parsed struct from middleware with
context.WithValue(ctx, UserCtxKey, claims)
Hope it helps!
This is a fairly common design pattern to create an authenticated client and then call various action methods on it. You could do something like the following:
type Client struct {
... // other members
token string // unexported unless there is a special reason to do otherwise
}
func NewClient(c echo.Context, email, password string) (*Client, error) {
user := users.User{}
cl := Client{}
... // your original method
cl.token = token
return &cl, nil
}
func (c *Client) DoSomething(...) ... { ... }

Golang how can I do a Dependency Injection to store some string values

I am using a mysql database and have many different Functions/Methods that interact with the database. For every Function I offcourse have to supply the Database Credentials such as
ReadAll.go
func ReadAll() {
db, err := sql.Open("mysql",
"user:password#tcp(127.0.0.1:3306)/hello")
if err != nil {
log.Fatal(err)
}
defer db.Close()
}
The part of "mysql",
"user:password#tcp(127.0.0.1:3306)/hello" never changes and I am supplying that to every Function that interacts with DB. I was wondering how can I for instance create a new File say DataBase.go put those credentials into some global variable and then reference when I need those strings ? That way if I have to change the credentials I only have to change them in 1 place.
I want to do something like
Database.go
const GlobalDB := "mysql","user:password#tcp(127.0.0.1:3306)/hello"
then
ReadAll.go
func ReadAll() {
db, err := sql.Open(GlobalDB)
if err != nil {
log.Fatal(err)
}
defer db.Close()
}
I am brand new to Golang but trying to figure this out.
I would probably do this by opening a session to the database once, then pass this session around to any function or method that may need it. This has a few potential problems:
You may need to lock access to it, so you don't co-mingle multiple queries on the same session (but it may be that your DB library ensures this, FWIW "database/sql" is concurrency-safe and recommends NOT opening short-lived database connections)
You can't safely close the session as it may still be in use elsewhere.
Another way would be to have a function that returns a DB sesssion, so instead of doing:
db, err := sql.Open("mysql", "user:password#tcp(127.0.0.1:3306)/hello")
You do the following:
func dbSession() (sql.DB, error) {
return sql.Open("mysql", "credentials")
}
func ReadAll() {
db, err := dbSession()
if err != nil {
log.Fatal(err)
}
defer db.Close()
}
And if you want even more flexibility, you can have a struct that contains the data you need, then build your DB connection parameters from that.
type dbData struct {
DBType, DBName, User, Host, Password string
}
var DBData dbData
func dbSession() (*sql.DB, error) {
return sql.Open(DBData.DBType, fmt.Sprintf("%s:%s#tcp(%s)/%s", DBData.User, DBData.Password, DBData.Host, DBData.DBName)
}
Also note the following in the documentation from sql.Open:
The returned DB is safe for concurrent use by multiple goroutines and
maintains its own pool of idle connections. Thus, the Open function
should be called just once. It is rarely necessary to close a DB.
you can easily create a new File with your credentials. Just have the file be in the main package main.
package main
var myDBConnectionString := "mysql://...."
This will be included when you compile your source.
The problem is, that you have to recompile your code everytime you have to connect to another database. Think about a development System vs. production System. The database credentials should differ in those systems, right? :)
To fix this, it is quit common to have a config file. So you can change the credentials with out re compiling your code.
I've got an other idea - just connect to the db once, and access this resource globally.
package main
import (
"fmt"
)
var myDb = "example"
func main() {
fmt.Println("Hello, playground")
doSomthingWithDatabase()
}
func doSomthingWithDatabase() {
fmt.Println("We can access a global variable here, see", myDb)
}
https://play.golang.org/p/npZ6Z49ink
For the configuration handling you can look here
https://blog.gopheracademy.com/advent-2014/reading-config-files-the-go-way/
hiboot-data provides out of the box starter that meet your requirement, the starter is github.com/hidevopsio/hiboot-data/starter/gorm, or you can implement your own starter by using hiboot framework, then you can inject then anywhere to decouple from the creation of the database configuration.
package service
import (
"errors"
"hidevops.io/hiboot-data/examples/gorm/entity"
"hidevops.io/hiboot-data/starter/gorm"
"hidevops.io/hiboot/pkg/app"
"hidevops.io/hiboot/pkg/utils/idgen"
)
type UserService interface {
AddUser(user *entity.User) (err error)
GetUser(id uint64) (user *entity.User, err error)
GetAll() (user *[]entity.User, err error)
DeleteUser(id uint64) (err error)
}
type UserServiceImpl struct {
// add UserService, it means that the instance of UserServiceImpl can be found by UserService
UserService
repository gorm.Repository
}
func init() {
// register UserServiceImpl
app.Component(newUserService)
}
// will inject BoltRepository that configured in github.com/hidevopsio/hiboot/pkg/starter/data/bolt
func newUserService(repository gorm.Repository) UserService {
repository.AutoMigrate(&entity.User{})
return &UserServiceImpl{
repository: repository,
}
}
func (s *UserServiceImpl) AddUser(user *entity.User) (err error) {
if user == nil {
return errors.New("user is not allowed nil")
}
if user.Id == 0 {
user.Id, _ = idgen.Next()
}
err = s.repository.Create(user).Error()
return
}
func (s *UserServiceImpl) GetUser(id uint64) (user *entity.User, err error) {
user = &entity.User{}
err = s.repository.Where("id = ?", id).First(user).Error()
return
}
func (s *UserServiceImpl) GetAll() (users *[]entity.User, err error) {
users = &[]entity.User{}
err = s.repository.Find(users).Error()
return
}
func (s *UserServiceImpl) DeleteUser(id uint64) (err error) {
err = s.repository.Where("id = ?", id).Delete(entity.User{}).Error()
return
}

Resources