Home > Software engineering >  How to handle DB connection in Go when using Repository pattern?
How to handle DB connection in Go when using Repository pattern?

Time:07-12

Let's assume I have a very simple repository interface that only reads from target database:

type UserRepository interface {
    read(ctx context.Context, id WHAT_TYPE_I_SHOULD_USE_HERE) models.User
}

NOTE: note that in id parameter I do not have any idea what to user as id type as id in MongoDB is ObjectId and in schema based DBs it might be a UUID field. My primary database is MongoDB if it helps, but I might switch to schema-based DBs.

Now I have a MongoDBRepository struct that has read() attached to it:

type MongoDBRepository struct {
}

func (mo MongoDBRepository) read(ctx context.Context, id primitive.ObjectID) {
    fmt.Printf("read user %s from MongoDB", id)
}

I have a method that connects to MongoDB:

func ConnectMongoDB() (*mongo.Client, context.CancelFunc) {
    client, err := mongo.NewClient(options.Client().ApplyURI(configs.MongoURI()))
    if err != nil {
        log.Fatal(err)
    }

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)

    err = client.Connect(ctx)

    err = client.Ping(ctx, nil)
    if err != nil {
        log.Fatal(err)
    }

    log.Print("Successfully connected to MongoDB!")

    return client, cancel
}

Now as interface is implemented we can have a similar repository for MySQL/PostgreSQL and also ConnectMySQL/ConnectPostgreSQL.

The issue I have is in my main function how should I handle connecting to my current database repository and how to use it in my controllers to read or update a document/record?

When I pass my connection to a controller method its type is set to *mongo.Client how can I abstract it away so my controller method that gets DB connection is not bound to target DB type?

CodePudding user response:

how should I handle connecting to my current database repository and how to use it in my controllers to read or update a document/record

I am always checking provider documentation to get a hint for that question.

After quick search, I found what I need in Github:

// Client is a handle representing a pool of connections to a MongoDB deployment. It is safe for concurrent use by
// multiple goroutines. 

Client is thread safe and represent not a single connection but a connection pool. That means that I can share a single Client instance among multiple instances of Controllers/Repositories, and the client can handle that.

Starting with Repositories. We need to inject Client:

type MongoDBRepository struct {
   Client mongo.Client
}

With current design, repository only contain thread safe members, so it is inherently thread safe.

This is the code to create a Repository in app start code:

repo := &MongoDBRepository{Client: client}

In controller, we define Repository as interface type, but we will inject MongoDBRepository struct:

// Controller Code
type UserController struct {
   Repo UserRepository
}

Controllers' initiation code also should happen on application star:

// App start 
controller := &UserController{Repo: repo}

To deal with db specific types (id WHAT_TYPE_I_SHOULD_USE_HERE), you need to implement them as Generics. It could make your Controller code a quite complicated. Consider hiding that complexity inside a Repository and exposing something simple like string or uuid.

With that approach, you easily can switch between different DBs. All you need to change is app initialization (Composition Root) without changing controllers code.

P.S. Multiple databases support is quite expensive. We already faced an issue with ID types. In the future, you should be ready to stop using any DB specific features and use only features available across all the databases. DB transactions is one of the great SQL features that is not available in Mongo. Weight pros and cons before you are fully committed to multiple DB types.

Update:

  1. Implement controller as struct with ServeHTTP(w http.ResponseWriter, r *http.Request) method.
  2. Create instance if that struct in main.go as described above.
  3. integrate that struct into router using router.Handle instead of router.HandleFunc

With struct, you should have better control for Controller dependencies lifecycle.

  • Related