10 min read

[Jornada do DevOps] #5 - ORMs em Golang

[Jornada do DevOps] #5 - ORMs em Golang
Photo by Uriel SC / Unsplash

Na última postagem aprendemos sobre GraphQL no Golang. Nesse estudo, precisaremos entender sobre ORMs, especificamente gORM.

Recapitulando:

Golang é uma língua relativamente jovem. É a linguagem simples, mas oferece alto desempenho. Há muitos benefícios de usar Golang sobre linguagens como PHP, Java e RoR. É famosa entre os programadores por seus incríveis recursos de escalabilidade e simultaneidade.

Gin é um micro-framework de alto desempenho que oferece um framework muito minimalista que carrega consigo apenas os recursos, bibliotecas e funcionalidades mais essenciais necessários para criar aplicativos da Web e microsserviços.

O Gin simplifica a construção de um pipeline de manipulação de solicitações a partir de peças modulares e reutilizáveis, permitindo que você escreva middleware que pode ser conectado a um ou mais manipuladores de solicitações ou grupos de manipuladores de solicitações.

Vamos entender como usar ORM (Object Relational Mapping) em Golang com gin. Para isso o Go está disponibilizando um pacote chamado gorm.

MySQL e Golang

Para executar o gorm com o MySQL, precisamos dos pacotes abaixo, então adicione-os ao projeto.

  • github.com/gin-gonic/gin
  • gorm.io/gorm
  • gorm.io/driver/mysql

Terminando as informações básicas do pacote, vamos ao código.

Configure o ponto de entrada do aplicativo

main.go

package auth

import (
	"database/sql"
	"net/http"
	"context"
)

// A private key for context that only this package can access. This is important
// to prevent collisions between different context uses
var userCtxKey = &contextKey{"user"}
type contextKey struct {
	name string
}

// A stand-in for our database backed user object
type User struct {
	Name string
	IsAdmin bool
}

// Middleware decodes the share session cookie and packs the session into context
func Middleware(db *sql.DB) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			c, err := r.Cookie("auth-cookie")

			// Allow unauthenticated users in
			if err != nil || c == nil {
				next.ServeHTTP(w, r)
				return
			}

			userId, err := validateAndGetUserID(c)
			if err != nil {
				http.Error(w, "Invalid cookie", http.StatusForbidden)
				return
			}

			// get the user from the database
			user := getUserByID(db, userId)

			// put it in context
			ctx := context.WithValue(r.Context(), userCtxKey, user)

			// and call the next with our new context
			r = r.WithContext(ctx)
			next.ServeHTTP(w, r)
		})
	}
}

// ForContext finds the user from the context. REQUIRES Middleware to have run.
func ForContext(ctx context.Context) *User {
	raw, _ := ctx.Value(userCtxKey).(*User)
	return raw
}

Configuração do banco de dados

database/db.go

package database

import (
   "fmt"
   "gorm.io/driver/mysql"
   "gorm.io/gorm"
)

const DB_USERNAME = "root"
const DB_PASSWORD = "root"
const DB_NAME = "my_db"
const DB_HOST = "127.0.0.1"
const DB_PORT = "3306"

var Db *gorm.DB
func InitDb() *gorm.DB {
   Db = connectDB()
   return Db
}

func connectDB() (*gorm.DB) {
   var err error
   dsn := DB_USERNAME +":"+ DB_PASSWORD +"@tcp"+ "(" + DB_HOST + ":" + DB_PORT +")/" + DB_NAME + "?" + "parseTime=true&loc=Local"
   
   db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

   if err != nil {
      fmt.Println("Error connecting to database : error=%v", err)
      return nil
   }

   return db
}
parseTime=true verificará DATE e DATETIME automaticamente para loc=Local define o fuso horário do sistema, você pode definir o fuso horário necessário em vez de Local.time.Time

Agora, é hora das Tables and Schema, também como a migração funciona com gorm.

Crie models/user.go e cole o conteúdo abaixo nele, estamos usando um exemplo bem básico aqui, fazendo operações CRUD na tabela users.

Adicionando Models

models/user.go

package database

import (
   "fmt"
   "gorm.io/driver/mysql"
   "gorm.io/gorm"
)

const DB_USERNAME = "root"
const DB_PASSWORD = "root"
const DB_NAME = "my_db"
const DB_HOST = "127.0.0.1"
const DB_PORT = "3306"

var Db *gorm.DB
func InitDb() *gorm.DB {
   Db = connectDB()
   return Db
}

func connectDB() (*gorm.DB) {
   var err error
   dsn := DB_USERNAME +":"+ DB_PASSWORD +"@tcp"+ "(" + DB_HOST + ":" + DB_PORT +")/" + DB_NAME + "?" + "parseTime=true&loc=Local"
   
   db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

   if err != nil {
      fmt.Println("Error connecting to database : error=%v", err)
      return nil
   }

   return db
}

O modelo de usuário adicionará automaticamente os três campos a seguir na migração:

CreatedAt — usado para armazenar o tempo de criação dos registros

UpdatedAt — usado para armazenar registros de tempo atualizado

DeletedAt — usado para armazenar o tempo de exclusão de registros, não excluirá os registros, apenas defina o valor do campo DeletedAt para a hora atual e você não encontrará o registro ao consultar, ou seja, o que chamamos de exclusão suave.

Uma das maiores vantagens de usar ORM é que não precisamos escrever consultas brutas ou relações de esquema complexas. Ele pode ser tratado de forma eficiente usando ORM, para nós, gorm . Você pode encontrar mais sobre métodos gorm aqui.

Agora, vamos adicionar um controlador para lidar com o modelo.

Controllers

controllers/user.go

package controllers

import (
   "errors"
   "github.com/gin-gonic/gin"
   "gorm-test/database"
   "gorm-test/models"
   "gorm.io/gorm"
   "net/http"
)

type UserRepo struct {
   Db *gorm.DB
}

func New() *UserRepo {
   db := database.InitDb()
   db.AutoMigrate(&models.User{})
   return &UserRepo{Db: db}
}

//create user
func (repository *UserRepo) CreateUser(c *gin.Context) {
   var user models.User
   c.BindJSON(&user)
   err := models.CreateUser(repository.Db, &user)
   if err != nil {
      c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err})
      return
   }
   c.JSON(http.StatusOK, user)
}

//get users
func (repository *UserRepo) GetUsers(c *gin.Context) {
   var user []models.User
   err := models.GetUsers(repository.Db, &user)
   if err != nil {
      c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err})
      return
   }
   c.JSON(http.StatusOK, user)
}

//get user by id
func (repository *UserRepo) GetUser(c *gin.Context) {
   id, _ := c.Params.Get("id")
   var user models.User
   err := models.GetUser(repository.Db, &user, id)
   if err != nil {
      if errors.Is(err, gorm.ErrRecordNotFound) {
         c.AbortWithStatus(http.StatusNotFound)
         return
      }
      
      c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err})
      return
   }
   c.JSON(http.StatusOK, user)
}

// update user
func (repository *UserRepo) UpdateUser(c *gin.Context) {
   var user models.User
   id, _ := c.Params.Get("id")
   err := models.GetUser(repository.Db, &user, id)
   if err != nil {
      if errors.Is(err, gorm.ErrRecordNotFound) {
         c.AbortWithStatus(http.StatusNotFound)
         return
      }
      
      c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err})
      return
   }
   c.BindJSON(&user)
   err = models.UpdateUser(repository.Db, &user)
   if err != nil {
      c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err})
      return
   }
   c.JSON(http.StatusOK, user)
}

// delete user
func (repository *UserRepo) DeleteUser(c *gin.Context) {
   var user models.User
   id, _ := c.Params.Get("id")
   err := models.DeleteUser(repository.Db, &user, id)
   if err != nil {
      c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err})
      return
   }
   c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"})
}
db.AutoMigrate() migra automaticamente nosso esquema, para manter nosso esquema atualizado.

Lembre-se de que AutoMigrate() apenas criará tabelas, corrigirá colunas ausentes e índices ausentes e não manipulará dados ou tipo de coluna existente.

O controlador invocará os respectivos métodos de modelo. O que eu amo é que o gorm auto lida com exclusões suaves. Você pode aprender mais sobre os tipos de exclusão fornecidos pelo gorm.

Vamos adicionar rotas aos métodos do controlador de acesso. Em main.go adicione as seguintes linhas de código.

userRepo := controllers.New()
r.POST("/users", userRepo.CreateUser)
r.GET("/users", userRepo.GetUsers)
r.GET("/users/:id", userRepo.GetUser)
r.PUT("/users/:id", userRepo.UpdateUser)
r.DELETE("/users/:id", userRepo.DeleteUser)

O main.go vai ficar assim:

package main

import (
   "github.com/gin-gonic/gin"
   "gorm-test/controllers"
   "net/http"
)

func main() {
   r := setupRouter()
   _ = r.Run(":8080")
}

func setupRouter() *gin.Engine {
   r := gin.Default()

   r.GET("ping", func(c *gin.Context) {
      c.JSON(http.StatusOK, "pong")
   })

   userRepo := controllers.New()
   r.POST("/users", userRepo.CreateUser)
   r.GET("/users", userRepo.GetUsers)
   r.GET("/users/:id", userRepo.GetUser)
   r.PUT("/users/:id", userRepo.UpdateUser)
   r.DELETE("/users/:id", userRepo.DeleteUser)

   return r
}

Vamos construir o projeto e você está pronto para começar!

Run go build main.go

Run go run main.go

Você encontrará um servidor rodando na porta 8080. Você pode testar as operações CRUD com os endpoints de API definidos acima.

Repositório aqui.

PostgreSQL e Golang

package models

import (
	"github.com/google/uuid"
	"gorm.io/gorm"
)

type UserID struct {
	ID string `uri:"id" binding:"required"`
}

type User struct {
	ID        string         `gorm:"primaryKey" json:"id"`
	FirstName string         `json:"firstname" binding:"required"`
	LastName  string         `json:"lastname" binding:"required"`
	CreatedAt int64          `gorm:"autoCreateTime:milli" json:"created_at"`
	UpdatedAt int64          `gorm:"autoUpdateTime:milli" json:"updated_at"`
	DeletedAt gorm.DeletedAt `json:"deleted_at"`
}

func (x *User) FillDefaults() {
	if x.ID == "" {
		x.ID = uuid.New().String()
	}
}
var user string
var password string
var db string
var host string
var port string
var ssl string
var timezone string
var dbConn *gorm.DB

func init() {
	user = GetEnvVar("POSTGRES_USER")
	password = GetEnvVar("POSTGRES_PASSWORD")
	db = GetEnvVar("POSTGRES_DB")
	host = GetEnvVar("POSTGRES_HOST")
	port = GetEnvVar("POSTGRES_PORT")
	ssl = GetEnvVar("POSTGRES_SSL")
	timezone = GetEnvVar("POSTGRES_TIMEZONE")
}

func GetDSN() string {
	return fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=%s TimeZone=%s", host, user, password, db, port, ssl, timezone)
}

func CreateDBConnection() error {

	db, err := gorm.Open(postgres.New(postgres.Config{
		DSN:                  GetDSN(),
		PreferSimpleProtocol: true, // disables implicit prepared statement usage
	}), &gorm.Config{})

	if err != nil {
		log.Err(err).Msg("Error occurred while connecting with the database")
	}
	
	// Create the connection pool

	sqlDB, err := db.DB()

	sqlDB.SetConnMaxIdleTime(time.Minute * 5)

	// SetMaxIdleConns sets the maximum number of connections in the idle connection pool.
	sqlDB.SetMaxIdleConns(10)

	// SetMaxOpenConns sets the maximum number of open connections to the database.
	sqlDB.SetMaxOpenConns(100)

	// SetConnMaxLifetime sets the maximum amount of time a connection may be reused.
	sqlDB.SetConnMaxLifetime(time.Hour)
	dbConn = db
	return err
}

func GetDatabaseConnection() (*gorm.DB, error) {
	sqlDB, err := dbConn.DB()
	if err != nil {
		return dbConn, err
	}
	if err := sqlDB.Ping(); err != nil {
		return dbConn, err
	}
	return dbConn, nil
}
func AutoMigrateDB() error {
	// Auto migrate database
	db, connErr := GetDatabaseConnection()
	if connErr != nil {
		return connErr
	}
	// Add required models here
	err := db.AutoMigrate(&models.User{})
    	// Example for migrating multiple models
   	 // err:= db.AutoMigrate(&models.User{}, &models.Admin{}, &models.Guest{})
	return err
}

Create

func CreateUser(c *gin.Context) {
	var user models.User

	request_id := c.GetString("x-request-id")

	// Bind request payload with our model
	if binderr := c.ShouldBindJSON(&user); binderr != nil {

		log.Error().Err(binderr).Str("request_id", request_id).
			Msg("Error occurred while binding request data")

		c.JSON(http.StatusUnprocessableEntity, gin.H{
			"message": binderr.Error(),
		})
		return
	}
	user.FillDefaults()

	// Get a connection
	db, conErr := utils.GetDatabaseConnection()
	if conErr != nil {
		log.Err(conErr).Str("request_id", request_id).Msg("Error occurred while getting a DB connection from the connection pool")
		c.JSON(http.StatusServiceUnavailable, gin.H{
			"message": "Service is unavailable",
		})
		return
	}

	// Create a user
	result := db.Create(&user)
	if result.Error != nil && result.RowsAffected != 1 {
		log.Err(result.Error).Str("request_id", request_id).Msg("Error occurred while creating a new user")
		c.JSON(http.StatusInternalServerError, gin.H{
			"message": "Error occurred while creating a new user",
		})
		return
	}

	c.JSON(http.StatusCreated, gin.H{
		"message": "User created successfully",
		"id":      user.ID,
	})
}

Read

func GetUser(c *gin.Context) {
	var userId models.UserID

	request_id := c.GetString("x-request-id")

	// Bind request payload with our model
	if binderr := c.ShouldBindUri(&userId); binderr != nil {

		log.Error().Err(binderr).Str("request_id", request_id).
			Msg("Error occurred while binding request data")

		c.JSON(http.StatusUnprocessableEntity, gin.H{
			"message": binderr.Error(),
		})
		return
	}

	// Get a connection
	db, conErr := utils.GetDatabaseConnection()
	if conErr != nil {
		log.Err(conErr).Str("request_id", request_id).Msg("Error occurred while getting a DB connection from the connection pool")
		c.JSON(http.StatusServiceUnavailable, gin.H{
			"message": "Service is unavailable",
		})
		return
	}

	var user models.User
	result := db.First(&user, "id = ?", userId.ID)
	if result.Error != nil {
		log.Err(result.Error).Str("request_id", request_id).Msg("Error occurred while fetching the user")
		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
			c.JSON(http.StatusNotFound, gin.H{
				"message": "Record not found",
			})
		} else {
			c.JSON(http.StatusInternalServerError, gin.H{
				"message": "Error occurred while fetching user",
			})
		}
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"result": user,
	})
}

read m

func GetUsers(c *gin.Context) {
   	 // Create an array of users
	var users []models.User

    	// Read the query parameters
	request_id := c.GetString("x-request-id")

    	// Time filters for reading users
    	// earliest created unix timestamp of the user, default is 0
	earliest := c.DefaultQuery("earliest", "0")
    	// latest created unix timestamp of the user, default is the time of the request
	latest := c.DefaultQuery("latest", fmt.Sprint(time.Now().UnixMilli()))

	// Get a connection
	db, conErr := utils.GetDatabaseConnection()
	if conErr != nil {
		log.Err(conErr).Str("request_id", request_id).Msg("Error occurred while getting a DB connection from the connection pool")
		c.JSON(http.StatusServiceUnavailable, gin.H{
			"message": "Service is unavailable",
		})
		return
	}

    	// Read the users
	tx := db.Where(fmt.Sprintf("created_at >= '%s' and created_at <= '%s'", earliest, latest)).Order("created_at asc").Scopes(utils.Paginate(c)).Find(&users)
   	 
	if tx.RowsAffected == 0 {
		log.Info().Msg("Read users returned with empty results")
	}
	c.JSON(http.StatusOK, gin.H{
		"earliest": earliest,
		"latest":   latest,
		"results":  users,
	})
}

pagination

// Pagination helper for GORM
func Paginate(c *gin.Context) func(db *gorm.DB) *gorm.DB {
	return func(db *gorm.DB) *gorm.DB {
        	// Read the page no. query parameter
		page, _ := strconv.Atoi(c.Query("page"))
		if page == 0 {
			page = 1
		}
    
        	// Read the page_size query parameter
		pageSize, _ := strconv.Atoi(c.Query("page_size"))
		switch {
        	// Max size 100
		case pageSize > 100:
			pageSize = 100
       		// If -ve value, set to 10 (default)
		case pageSize <= 0:
			pageSize = 10
		}

		// calculate the offset
        	offset := (page - 1) * pageSize
        	// Return the database object with Offset and Limit
		return db.Offset(offset).Limit(pageSize)
	}
}

update

func UpdateUser(c *gin.Context) {
	var user models.User

	request_id := c.GetString("x-request-id")

	// Bind request payload with our model
	if binderr := c.ShouldBindJSON(&user); binderr != nil {

		log.Error().Err(binderr).Str("request_id", request_id).
			Msg("Error occurred while binding request data")

		c.JSON(http.StatusUnprocessableEntity, gin.H{
			"message": binderr.Error(),
		})
		return
	}
	user.FillDefaults()

	// Get a connection
	db, conErr := utils.GetDatabaseConnection()
	if conErr != nil {
		log.Err(conErr).Str("request_id", request_id).Msg("Error occurred while getting a DB connection from the connection pool")
		c.JSON(http.StatusServiceUnavailable, gin.H{
			"message": "Service is unavailable",
		})
		return
	}

	// Create a user object
	var value models.User

	// Read the user which is to be updated
	result := db.First(&value, "id = ?", user.ID)
	if result.Error != nil {
		log.Err(result.Error).Str("request_id", request_id).Msg("Error occurred while updating the user")
		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
			c.JSON(http.StatusNotFound, gin.H{
				"message": "Record not found",
			})
		} else {
			c.JSON(http.StatusInternalServerError, gin.H{
				"message": "Error occurred while updating user",
			})
		}
		return
	}

	// Update the desired values using the request payload
	value.FirstName = user.FirstName
	value.LastName = user.LastName

	// Save the updated user
	tx := db.Save(&value)
	if tx.RowsAffected != 1 {
		c.JSON(http.StatusInternalServerError, gin.H{
			"message": "Error occurred while updating user",
		})
		return
	}

	// Return the updated user with the response
	c.JSON(http.StatusOK, gin.H{
		"message": "User updated successfully",
		"result":  value,
	})

}

Delete

func DeleteUser(c *gin.Context) {
	var userId models.UserID

	request_id := c.GetString("x-request-id")

	// Bind request payload with our model
	if binderr := c.ShouldBindUri(&userId); binderr != nil {

		log.Error().Err(binderr).Str("request_id", request_id).
			Msg("Error occurred while binding request data")

		c.JSON(http.StatusUnprocessableEntity, gin.H{
			"message": binderr.Error(),
		})
		return
	}

	// Get a connection
	db, conErr := utils.GetDatabaseConnection()
	if conErr != nil {
		log.Err(conErr).Str("request_id", request_id).Msg("Error occurred while getting a DB connection from the connection pool")
		c.JSON(http.StatusServiceUnavailable, gin.H{
			"message": "Service is unavailable",
		})
		return
	}
    
    	// Read the desired user from the database
	var user models.User
	result := db.First(&user, "id = ?", userId.ID)
	if result.Error != nil {
		log.Err(result.Error).Str("request_id", request_id).Msg("Error occurred while deleting the user")
		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
			c.JSON(http.StatusNotFound, gin.H{
				"message": "Record not found",
			})
		} else {
			c.JSON(http.StatusInternalServerError, gin.H{
				"message": "Error occurred while deleting user",
			})
		}
		return
	}

    	// Delete the user
	tx := db.Delete(&user)
   
    	// Verify the status of the operation
	if tx.RowsAffected != 1 {
		c.JSON(http.StatusInternalServerError, gin.H{
			"message": "Error occurred while deleting user",
		})
		return
	}
    	// User is deleted successfully and there is no content to return
	c.JSON(http.StatusNoContent, gin.H{})
}