programming

Get to know the Repository Pattern in Golang

In the book Domain-Driven Design, Eric Evans explains that

Repository is a mechanism for encapsulating storage, retrieval and search behaviour, which emulates a collection of objects

This Repository Pattern is usually used as a bridge between the business logic of our application and all the SQL commands in the Database. So we will write all the SQL in the repository, while our business logic code only needs to use the repository that we have created.

So that you can better imagine it, here is the repository pattern diagram below.

diagram repository pattern

Entity or Model

In object-oriented programming, usually a table in the database will always be represented as an Entity or Model class, but Golang does not recognize Class, so we can represent the data in the form of Struct. This struct will make it easier for us to create program code. When we query the Repository, when we return an array, it is better to convert it first to an Entity struct or model so that we just use the object.

An example of the struct that we will use later in the implementation can be seen below.

package model

type Comment struct {
    Id int32
    Email string
    Comment string
}

Implementation of the Repository Pattern

Before we understand the entity or model, we will try to learn from the start how to implement it.

First we need to create a project folder learn-golang-repository-pattern and initialize the Golang project with the command below.

go mod init github.com/santekno/learn-golang-repository-pattern

Next, we will create several folders as below.

├── model
│   ├── comment.go
├── repository
│   ├── new.go
│   ├── comment.go
├── database.go
├── main.go 
└── go.mod

The contents of the comment.go file are the same entity or model for comments as above, namely.

package model

type Comment struct {
    Id int32
    Email string
    Comment string
}

Then, we create an init.go file in the repository folder to store the methods that we will call when we need a function in the database.

package repository

import (
	"context"
	"database/sql"

	model "github.com/santekno/golang-belajar-repository-pattern/model"
)

type CommentRepo struct {
	DB *sql.DB
}

func NewCommentRepository(db *sql.DB) CommentRepository {
	return &CommentRepo{
		DB: db,
	}
}

type CommentRepository interface {
	Insert(ctx context.Context, comment model.Comment) (model.Comment, error)
	FindById(ctx context.Context, id int32) (model.Comment, error)
	FindAll(ctx context.Context) ([]model.Comment, error)
}

In order to be able to implement the methods that have been defined in the interface above, we need to create a file, namely comment.go with contents as below.

package repository

import (
	"context"
	"fmt"

	model "github.com/santekno/golang-belajar-repository-pattern/model"
)

func (repo *CommentRepo) Insert(ctx context.Context, comment model.Comment) (model.Comment, error) {
	result, err := repo.DB.ExecContext(ctx, "INSERT INTO comments(email,comment) VALUES(?,?)", comment.Email, comment.Email)
	if err != nil {
		return comment, err
	}

	insertId, err := result.LastInsertId()
	if err != nil {
		return comment, err
	}

	comment.Id = int32(insertId)
	return comment, nil
}

func (repo *CommentRepo) FindById(ctx context.Context, id int32) (model.Comment, error) {
	var comment model.Comment
	query := "SELECT id, email, comment FROM comments WHERE id=? LIMIT 1"
	rows, err := repo.DB.QueryContext(ctx, query, id)
	if err != nil {
		return comment, err
	}
	defer rows.Close()

	for rows.Next() {
		err := rows.Scan(&comment.Id, &comment.Email, &comment.Comment)
		if err != nil {
			return comment, err
		}
	}
	return comment, nil
}

func (repo *CommentRepo) FindAll(ctx context.Context) ([]model.Comment, error) {
	var comments []model.Comment
	query := "SELECT id, email, comment FROM comments"
	rows, err := repo.DB.QueryContext(ctx, query)
	if err != nil {
		return comments, err
	}
	defer rows.Close()

	for rows.Next() {
		var comment model.Comment
		err := rows.Scan(&comment.Id, &comment.Email, &comment.Comment)
		if err != nil {
			fmt.Printf("error scan rows %v", err)
			continue
		}
		comments = append(comments, comment)
	}

	return comments, nil
}

Now that we have created all the repositories, it’s time to call the main program function so that we can run all the repositories. Before playing, we need to add a database connection first so we can connect to the database.

func GetConnection() *sql.DB {
	db, err := sql.Open("mysql", "root:belajargolang@tcp(localhost:3306)/belajar-golang")
	if err != nil {
		panic(err)
	}

	db.SetMaxIdleConns(10)
	db.SetMaxOpenConns(100)
	db.SetConnMaxIdleTime(5 * time.Minute)
	db.SetConnMaxLifetime(60 * time.Minute)
	return db
}

And below are the contents of the main() function which calls the repository that we have created.

func main() {
	ctx := context.Background()
	db := GetConnection()
	defer db.Close()

	commentRepo := repository.NewCommentRepository(db)

	// find all data comments
	comments, err := commentRepo.FindAll(ctx)
	if err != nil {
		panic(err)
	}

	for _, cm := range comments {
		fmt.Printf("data %d: %v\n", cm.Id, cm)
	}

	// find all data by id
	comment, err := commentRepo.FindById(ctx, 2)
	if err != nil {
		panic(err)
	}
	fmt.Printf("data : %v", comment)

	// insert data
	id, err := commentRepo.Insert(ctx, model.Comment{Email: "test@gmail.com", Comment: "komentar yuk"})
	if err != nil {
		panic(err)
	}
	fmt.Printf("lastId: %v", id)
}

In the main() function we will call the function to connect to the database, then we initialize the comment repository and then we can use the repository to call source data from the database.

If we want to add a new method or new function which is related to retrieving data or saving data into the database. So, we simply add the Method to Interface and its implementation in the comment.go file. It’s easy, right? So our program code is encapsulated into an interface and will later be implemented into various methods that can be created.

comments powered by Disqus