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.