avatarIsrael Josué Parra Rosales

Summary

Chapter 12 of the series focuses on developing a database connection and integrating database interactions within a microservice, specifically using MySQL, within the context of a shopping cart service.

Abstract

In this chapter, the author delves into the technical aspects of establishing a database connection and implementing data access within a microservice architecture. The infrastructure package is introduced, containing sub-packages for persistence and database interactions. The persistence sub-package includes concrete implementations for shopping cart and product repositories, adhering to interfaces defined in the domain package. The database sub-package handles database configuration and connection establishment. The code examples provided demonstrate the creation and retrieval of shopping cart and product data from a MySQL database, with error handling and logging integrated throughout. The chapter emphasizes the importance of a well-structured data access layer for the microservice's functionality and sets the stage for the next chapter, which will focus on defining the REST API.

Opinions

  • The author recommends reading previous chapters for a comprehensive understanding of microservices architecture.
  • The use of UUIDs for unique identifiers is a design choice that promotes scalability and distributed system compatibility.
  • Employing a repository pattern separates the data access logic from the business logic, enhancing maintainability and testability.
  • The inclusion of custom error handling demonstrates a commitment to robust error management and clear communication of issues.
  • Utilizing environment variables for database configuration allows for flexible deployment environments and security best practices.
  • The use of the configor package for configuration management indicates a preference for convenience and readability in handling configuration data.
  • Logging important events, such as database connections and operations, is considered a best practice for monitoring and debugging purposes.

Chapter 12 — Coding our Microservice (Part 2)

Developing the Database Connection

The following list is the previous chapters of this series:

I recommend you take a look at the previous chapters if you have not read them yet. That will help you to get more knowledge in this wonderful world of “Microservices architecture”.

Structure

In this chapter, the following topics will be covered:

  • Developing the database connection
  • Adding database interaction to our microservices
  • Implementing MyQL
  • Consuming the database by repositories

Developing the database connection

The infrastructure package, as mentioned before this package, is defined to declare all the logic related to the persistence process. For this example, two sub-packages are defined as “persistence” and “database”.

Persistence will contain the concrete implementation of the repository, In both cases we are implementing a MySQL repository, and then the functionality related to products and the shopping carts will be defined there each one on its respective properties “shopping_cart_repository” and “product_repository” Those functions are the same defined by the interface declared on the domain package that was analyzed in the previous step.

On the other hand the subpackage “database” defines the needed code to load the DB configuration values “database_config.go” and defines the code used to establish a database connection “db_connection”.

Next is described and analyzed each one of the files.

│   ├── infrastructure/
│   │   ├── persistence/
│   │   │   ├── shopping_cart_repository.go
│   │   │   └── product_repository.go
│   │   └── database/
│   │       ├── db_connection.go
│   │       └── database_config.go

/infrastructure/persistence/shopping_cart_repository.go

package persistence

import (
    "database/sql"
    shoppingcart "github.com/go-microservices/shopping-cart-service/internal/domain/shopping_cart"
    uuid "github.com/satori/go.uuid"
    "github.com/sirupsen/logrus"
    template_errors "github.com/go-microservices/shopping-cart-service/internal/shared/error"
    _ "github.com/go-sql-driver/mysql"
)

type shoppingCartMySQLRepo struct {
    logger *logrus.Logger
    db     DataBaseConn
}

func NewshoppingCartMySQLRepo(db DataBaseConn, logger *logrus.Logger) shoppingCartMySQLRepo {
    return shoppingCartMySQLRepo{
        logger: logger,
        db:     db,
    }
}

func (sc shoppingCartMySQLRepo) Create(spc shoppingcart.ShoppingCart) error {
    sc.logger.Info("On ShoppingCart repository - creating shopping cart")
    query := "INSERT INTO shopping_cart (id, user_id) VALUES (?, ?)"
    randomUUID := uuid.NewV4()
    _, err := sc.db.Exec(query, randomUUID.String(), spc.UserID)
    if err != nil {
        sc.logger.Errorf("error saving shopping cart: %v", err)
        return err
    }
    return nil
}

func (sc shoppingCartMySQLRepo) GetByUserID(userId *uuid.UUID) (*shoppingcart.ShoppingCart, error) {
    sc.logger.Info("On ShoppingCart repository - get shopping cart info")
    query := "SELECT * FROM shopping_cart WHERE  user_id = ?"
    row := sc.db.QueryRow(query, userId.String())
    if row.Err() != nil {
        sc.logger.Errorf("error getting shopping cart: %v", row)
        return nil, row.Err()
    }
    var cart shoppingcart.ShoppingCart
    err := row.Scan(&cart.ID, &cart.UserID)
    if err == sql.ErrNoRows {
        return nil, template_errors.NewRecordNotFoundError("shopping cart not found", template_errors.RecordNotFound)
    }
    if err != nil {
        sc.logger.Errorf("error getting shopping cart values: %v", err)
        return nil, err
    }
    return &cart, nil
}

Explaining the code:

This Go code serves as the repository implementation for seamless interaction with a MySQL database within the scope of a shopping cart microservice. Essentially, it provides the concrete implementation of the repository required by the “shopping cart domain.” Within this code, we define and implement functions that fulfill the interface specified in “/domain/shopping_cart/shopping_cart_repository.go”.

Let’s break down the key components and functionalities of this code:

The code imports various packages, including:

  • “database/sql” for working with SQL databases
  • “github.com/sirupsen/logrus” for logging
  • “github.com/satori/go.uuid” for UUID generation
  • “github.com/go-sql-driver/mysql” for the MySQL driver
  • It also imports custom error handling and domain packages

The “shoppingCartMySQLRepo” struct holds a reference to a logger “logrus.Logger ‘’ and a database connection “DataBaseConn”. This struct represents the repository for interacting with the shopping cart data in the database.

The “NewshoppingCartMySQLRepo” function is a constructor for creating a new instance of the “shoppingCartMySQLRepo”. It takes a database connection and a logger as parameters and returns an instance of the repository.

The “Create” method is used to create a new shopping cart entry in the database. For this example, It generates a random UUID as the cart’s ID and executes an SQL “INSERT” query. If the insertion is successful, it returns `nil`. If an error occurs, it logs the error and returns it.

The “GetByUserID” method retrieves a shopping cart by user ID from the database. It executes a SQL “SELECT” query and maps the result to a “shoppingcart.ShoppingCart” struct. If the cart is not found, it returns a custom “RecordNotFoundError”. If any other error occurs, it logs the error and returns it.

This code represents the data access layer of a microservice responsible for creating and retrieving shopping cart data in a MySQL database. It abstracts the database operations and provides error handling for these operations.

/infrastructure/persistence/product_repository.go

package persistence

import (
    "database/sql"
    "log"
    "github.com/go-microservices/shopping-cart-service/internal/domain/product"
    template_errors "github.com/go-microservices/shopping-cart-service/internal/shared/error"
    uuid "github.com/satori/go.uuid"
    "github.com/sirupsen/logrus"
)

type ProductMySQLRepo struct {
    logger *logrus.Logger
    db     *sql.DB
}

func NewProductMySQLRepo(db *sql.DB, logger *logrus.Logger) ProductMySQLRepo {
    return ProductMySQLRepo{
        logger: logger,
        db:     db,
    }
}

func (pr ProductMySQLRepo) Create(product product.Product) error {
    log.Println("On Create product repository")
    query := "INSERT INTO product (id,name,quantity,product_description, shopping_cart_id) VALUES (?, ?,?,?,?)"
    randomUUID := uuid.NewV4()
    _, err := pr.db.Exec(query, randomUUID.String(),
        product.Name, product.Quantity, product.Description, product.ShoppingCartID)
    if err != nil {
        log.Println("error saving product: ", err.Error())
        return err
    }
    return nil
}

func (pr ProductMySQLRepo) Get(shoppingCartID uuid.UUID) ([]product.Product, error) {
    pr.logger.Info("On List products repository")
    products := []product.Product{}
    query := "SELECT * FROM product WHERE  shopping_cart_id = ?"
    rows, err := pr.db.Query(query, shoppingCartID.String())
    if err != nil {
        pr.logger.Errorf("error getting products cart: %v", err)
        return products, err
    }
    defer rows.Close()
    for rows.Next() {
        var product product.Product
        err := rows.Scan(&product.ID, &product.Name, &product.Quantity,
            &product.Description, &product.ShoppingCartID)
        if err != nil {
            pr.logger.Errorf("error scanning row: %v", err)
            continue
        }
        products = append(products, product)
    }
    return products, nil
}

func (pr ProductMySQLRepo) Delete(productID uuid.UUID) error {
    pr.logger.Info("On Delete product repository")
    if err := pr.GetByID(productID); err != nil {
        return err
    }
    query := "DELETE FROM product WHERE id = ?"
    _, err := pr.db.Exec(query, productID)
    if err != nil {
        pr.logger.Errorf("On Delete product repository - error deleting product: %v", err)
        return err
    }
    return nil
}

func (pr ProductMySQLRepo) UpdateQuantity(productID uuid.UUID, quantity int) error {
    pr.logger.Info("On Update product quantity repository")
    if err := pr.GetByID(productID); err != nil {
        return err
    }
    query := "UPDATE product SET quantity = ? WHERE id = ?"
    _, err := pr.db.Exec(query, quantity, productID)
    if err != nil {
        pr.logger.Errorf("error updating product: %v", err)
        return err
    }
    return nil
}

func (pr ProductMySQLRepo) GetByID(productID uuid.UUID) error {
    pr.logger.Info("On get product by ID quantity repository")
    querySelect := "SELECT id FROM product WHERE id = ?"
    var existingID uuid.UUID
    err := pr.db.QueryRow(querySelect, productID).Scan(&existingID)
    if err == sql.ErrNoRows {
        pr.logger.Infof("On Get product quantity repository - product not found %d", productID)
        return template_errors.NewRecordNotFoundError("product not found", template_errors.RecordNotFound)
    } else if err != nil {
        pr.logger.Errorf("error quering product: %v", err)
        return err
    }
    return nil
}

Explaining the code:

This Go code serves as the repository implementation for seamless interaction with a MySQL database within the scope of a shopping cart microservice. Essentially, it provides the concrete implementation of the repository required by the “product domain.” Within this code, we define and implement functions that fulfill the interface specified in /domain/product/product_repository.go.

Similar to the “shopping cart repository” studied previously, this code represents the data access layer of a microservice responsible for creating and retrieving shopping cart data in a MySQL database. It abstracts the database operations and provides error handling for these operations.

The logic is the same but implemented to handle product records, offering functions for creating, listing, deleting, and updating products. These functions adhere to the same specifications and standards applied in the shopping cart repository.

/infrastructure/database/database_config.go

package database

import (
"log"

"github.com/jinzhu/configor"
)

type DBConfig struct {
    Host string `env:"DB_HOST" default:"localhost"`
    Port string `env:"DB_PORT" default:"3306"`
    User string `env:"DB_USER" default:"user123"`
    Password string `env:"DB_PASSWORD" default:"pass123"`
    Database string `env:"DB_DATABASE" default:"shopping_cart"`
    Type string `env:"DB_TYPE" default:"mysql"`
}

func NewConfig() (conf DBConfig, err error) {
     log.Println("On loading database configuration")
     err = configor.New(&configor.Config{Environment: "development"}).Load(&conf)
     log.Println("configuration loaded: ")
     log.Println(conf)
     return
}

Explaining the code:

It’s essential to mention the required packages for this code. It includes the core “log” package for logging purposes and relies on a third-party library called github.com/jinzhu/configor” for effective configuration management.

An essential aspect to highlight is the DBConfig struct, responsible for storing configuration parameters for the database. These parameters encompass the host, port, user, password, database name, and database type. Each field in the struct is tagged with specific environment variables, facilitating value mapping. Default values are also provided for each field, serving as fallbacks in cases where the corresponding environment variable is not defined.

The NewConfig function is responsible for loading the database configuration. It uses the configor package to load the configuration. The configor.New function is called with a configuration object that specifies the environment as “development.” This means it’s loading the configuration for the development environment.

The configor.New function’s “Load” method is used to load the configuration values into the conf variable. This function returns any errors encountered during the loading process.

Finally, the function returns the loaded configuration conf and any error that occurred during the loading process.

In summary, this code sets up a structure DBConfig to hold database configuration values, loads these values using the configor package, and logs the loaded configuration.

/infrastructure/database/db_connection.go

package database

import (
    "database/sql"
    "log"
    _ "github.com/go-sql-driver/mysql"
)

func NewDatabseConnection(conf DBConfig) (*sql.DB, error) {
    dsn := conf.User + ":" + conf.Password + "@tcp(" + conf.Host + ":" + conf.Port + ")/" + conf.Database
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        log.Println("Error connecting to database")
        return nil, err
    }

    err = db.Ping()
    if err != nil {
        log.Println("Error connecting to database")
        return nil, err
    }
    log.Println("database connected")
    return db, nil
}

Explaining the code:

This Go function is responsible for establishing a connection to a MySQL database using the provided “DBConfig” configuration. Let’s break down its functionality:

The function takes a single input parameter, “conf”, which is an instance of the “DBConfig” struct. This struct holds the database configuration details, such as host, port, user, password, database name, and database type.

The function constructs a Data Source Name (var dns) string using the configuration details from “conf”. This DSN string contains information about how to connect to the MySQL database, including the user, password, host, and port.

It then uses the sql.Open() function from the github.com/go-sql-driver/mysql package to establish a connection to the MySQL database. If an error occurs during this process, it logs an error message and returns both `nil` and the encountered error.

After successfully opening the database connection, it immediately checks the connection’s validity using the “Ping()” method. If the ping fails, it logs an error and returns an error indicating the connection failure.

If both the database connection and ping are successful, it logs a confirmation message that the database is connected and returns a reference to the open database connection “*sql.DB” along with a “nil” error, indicating a successful connection.

This function encapsulates the process of connecting to a MySQL database using the provided configuration, handling potential errors along the way.

Next readings …

Wait for Chapter 13 “Building a Microservice With Go (Part 3)— Defining the REST API”.

Golang
Software Development
Software Engineering
Software Architecture
Computer Science
Recommended from ReadMedium