Repository Pattern – Persiapan

Jika Anda menjalankan test dari modul sebelumnya dan docker service belum berjalan, maka akan terjadi error mengenai database connection, kurang lebih seperti berikut errornya.

2022/10/26 18:20:11 failed to connect to `host=localhost user=postgres database=users`: dial error (dial tcp 127.0.0.1:5432: connectex: No connection could be made because the target machine actively refused it.)
FAIL    webapp/cmd/web  4.490s
?       webapp/pkg/data [no test files]
?       webapp/pkg/db   [no test files]
FAIL

Seharusnya, proses testing tetap dapat dilakukan dengan atau tanpa database berjalan.

Untuk menyelesaikan masalah ini, kita dapat gunakan Repository pattern untuk database. Ide dari reposotory pattern adalah abstraksi implementasi database dengan menggunakan interface.

Mari kita mulai buat repository untuk database kita.

Pertama buat folder repository didalam folder pkg. Kemudian buat file repository.go didalamnya.

File repository.go berupa interface yang akan berisi fungsi-fungsi dari file pkg/db/users_postgres.go

package repository

import (
	"database/sql"
	"webapp/pkg/data"
)

type DatabaseRepo interface {
	Connection() *sql.DB
	AllUsers() ([]*data.User, error)
	GetUser(id int) (*data.User, error)
	GetUserByEmail(email string) (*data.User, error)
	UpdateUser(u data.User) error
	DeleteUser(id int) error
	InsertUser(user data.User) (int, error)
	ResetPassword(id int, password string) error
	InsertUserImage(i data.UserImage) (int, error)
}

Setelah interface dibuat, kita pindahkan fungsi database kedalam repository. Buat folder baru dbrepo didalam folder repository, kemudian buat file users_postgres.go didalamnya.

Berikut isi file repository/dbrepo/users_postgres.go, isinya mirip dengan file pkg/db/users_postgres.go, dengan merubah reciever *PostgresConn menjadi *PostgresDBRepo dan penambahan fungsi baru yaitu connection().

package dbrepo

import (
	"context"
	"database/sql"
	"log"
	"time"
	"webapp/pkg/data"

	"golang.org/x/crypto/bcrypt"
)

const dbTimeout = time.Second * 3

type PostgresDBRepo struct {
	DB *sql.DB
}

func (m *PostgresDBRepo) Connection() *sql.DB {
	return m.DB
}

// AllUsers returns all users as a slice of *data.User
func (m *PostgresDBRepo) AllUsers() ([]*data.User, error) {
	ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
	defer cancel()

	query := `select id, email, first_name, last_name, password, is_admin, created_at, updated_at
	from users order by last_name`

	rows, err := m.DB.QueryContext(ctx, query)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var users []*data.User

	for rows.Next() {
		var user data.User
		err := rows.Scan(
			&user.ID,
			&user.Email,
			&user.FirstName,
			&user.LastName,
			&user.Password,
			&user.IsAdmin,
			&user.CreatedAt,
			&user.UpdatedAt,
		)
		if err != nil {
			log.Println("Error scanning", err)
			return nil, err
		}

		users = append(users, &user)
	}

	return users, nil
}

// GetUser returns one user by id
func (m *PostgresDBRepo) GetUser(id int) (*data.User, error) {
	ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
	defer cancel()

	query := `
		select 
			id, email, first_name, last_name, password, is_admin, created_at, updated_at 
		from 
			users 
		where 
		    id = $1`

	var user data.User
	row := m.DB.QueryRowContext(ctx, query, id)

	err := row.Scan(
		&user.ID,
		&user.Email,
		&user.FirstName,
		&user.LastName,
		&user.Password,
		&user.IsAdmin,
		&user.CreatedAt,
		&user.UpdatedAt,
	)

	if err != nil {
		return nil, err
	}

	return &user, nil
}

// GetUserByEmail returns one user by email address
func (m *PostgresDBRepo) GetUserByEmail(email string) (*data.User, error) {
	ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
	defer cancel()

	query := `
		select 
			id, email, first_name, last_name, password, is_admin, created_at, updated_at 
		from 
			users 
		where 
		    email = $1`

	var user data.User
	row := m.DB.QueryRowContext(ctx, query, email)

	err := row.Scan(
		&user.ID,
		&user.Email,
		&user.FirstName,
		&user.LastName,
		&user.Password,
		&user.IsAdmin,
		&user.CreatedAt,
		&user.UpdatedAt,
	)

	if err != nil {
		return nil, err
	}

	return &user, nil
}

// UpdateUser updates one user in the database
func (m *PostgresDBRepo) UpdateUser(u data.User) error {
	ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
	defer cancel()

	stmt := `update users set
		email = $1,
		first_name = $2,
		last_name = $3,
		is_admin = $4,
		updated_at = $5
		where id = $6
	`

	_, err := m.DB.ExecContext(ctx, stmt,
		u.Email,
		u.FirstName,
		u.LastName,
		u.IsAdmin,
		time.Now(),
		u.ID,
	)

	if err != nil {
		return err
	}

	return nil
}

// DeleteUser deletes one user from the database, by id
func (m *PostgresDBRepo) DeleteUser(id int) error {
	ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
	defer cancel()

	stmt := `delete from users where id = $1`

	_, err := m.DB.ExecContext(ctx, stmt, id)
	if err != nil {
		return err
	}

	return nil
}

// InsertUser inserts a new user into the database, and returns the ID of the newly inserted row
func (m *PostgresDBRepo) InsertUser(user data.User) (int, error) {
	ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
	defer cancel()

	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), 12)
	if err != nil {
		return 0, err
	}

	var newID int
	stmt := `insert into users (email, first_name, last_name, password, is_admin, created_at, updated_at)
		values ($1, $2, $3, $4, $5, $6, $7) returning id`

	err = m.DB.QueryRowContext(ctx, stmt,
		user.Email,
		user.FirstName,
		user.LastName,
		hashedPassword,
		user.IsAdmin,
		time.Now(),
		time.Now(),
	).Scan(&newID)

	if err != nil {
		return 0, err
	}

	return newID, nil
}

// ResetPassword is the method we will use to change a user's password.
func (m *PostgresDBRepo) ResetPassword(id int, password string) error {
	ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
	defer cancel()

	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12)
	if err != nil {
		return err
	}

	stmt := `update users set password = $1 where id = $2`
	_, err = m.DB.ExecContext(ctx, stmt, hashedPassword, id)
	if err != nil {
		return err
	}

	return nil
}

// InsertUserImage inserts a user profile image into the database.
func (m *PostgresDBRepo) InsertUserImage(i data.UserImage) (int, error) {
	ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
	defer cancel()

	var newID int
	stmt := `insert into user_images (user_id, fileName, created_at, updated_at)
		values ($1, $2, $3, $4) returning id`

	err := m.DB.QueryRowContext(ctx, stmt,
		i.UserID,
		i.FileName,
		time.Now(),
		time.Now(),
	).Scan(&newID)

	if err != nil {
		return 0, err
	}

	return newID, nil
}

Sampai disini setup database repository sudah selesai, selanjutnya adalah menggunakannya kedalam aplikasi.

Buka file cmd/web/main.go, modifikasi interface application dan DB dengan menggunakan repository database yang telah kita buat diatas.

package main

import (
	"encoding/gob"
	"flag"
	"log"
	"net/http"
	"webapp/pkg/data"
	"webapp/pkg/repository"
	"webapp/pkg/repository/dbrepo"

	"github.com/alexedwards/scs/v2"
)

type application struct {
	DSN     string
	DB      repository.DatabaseRepo
	Session *scs.SessionManager
}

func main() {
	gob.Register(data.User{})
	//setup app config
	app := application{}

	//database connection string
	flag.StringVar(&app.DSN, "dsn", "host=localhost port=5432 user=postgres password=postgres dbname=users sslmode=disable timezone=UTC connect_timeout=5", "Postgres Connection")
	flag.Parse()

	//connection to database
	conn, err := app.connectToDB()
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	app.DB = &dbrepo.PostgresDBRepo{DB: conn}

	//get session manager
	app.Session = getSession()

	//start server
	log.Println("Starting server on port 8080...")
	err = http.ListenAndServe(":8080", app.routes())

	if err != nil {
		log.Fatal(err)
	}

}

Sampai disini Database Repository sudah digunakan dalam aplikasi kita. Selanjutnya kita akan membuat database repository untuk test.

Perhatian, karena kita sudah menggunakan pendekatan repository, file pkg/db/*.* dapat kita hapus.

Sharing is caring:

Leave a Comment