CockroachDB as a database with Gorilla/Mux | Go

CockroachDB

According to its Github's page:

CockroachDB is a distributed SQL database built on a transactional and strongly-consistent key-value store. It scales horizontally; survives disk, machine, rack, and even data center failures with minimal latency disruption and no manual intervention; supports strongly-consistent ACID transactions; and provides a familiar SQL API for structuring, manipulating, and querying data.

In the case of Go, CockroachDB has support for pgx, pq, GORM, and upper/db. In case you want to look at its examples apps, here is the link

In this article, we will build an API using Gorilla/mux, CockroachDB combines with GORM.

This is our directory structure:

app/
     model/
            database.go
            model.go
     go.mod
     go.sum
     handlers.go
     main.go

We will start writing our model and our database connection. The database connection will be in the database.go file, the code there is the same code that the CockroachDB's page generates, so is valid no matter what router or web framework we use. In handlers.go we will write our handlers functions using Gorilla/Mux. In main.go we have our initializers.

model.go

package model

import (
    "gorm.io/gorm"
)

type Expenses struct {
    gorm.Model

    ID          string  `json:"id"`
    Amount      float64 `json:"amount"`
    Description string  `json:"description"`
    Category    string  `json:"category"`
}

In model.go we define our table and add its fields. gorm.Model adds the id, CreateAt, UpdateAt, and DeleteAt fields.

To connect to CockroachDb we need an account. We have to follow the instructions (Step 1. Start CockroachDB) on this page.

After we configure our account, we will be redirected to a dashboard, where there is a "connect" button. We select the language we will use, in this case, Go. And in the field "Select tool", we select "GORM". It will generate a code that we will paste into the database.go file.

cockroachdb_gorm.png

database.go

package model

import (
    "log"
    "time"

    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

func SetupDB() (*gorm.DB, error) {

    dsn := "DATABASE_URL"
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatal("failed to connect database", err)
    }

    var now time.Time
    db.Raw("SELECT NOW()").Scan(&now)

    log.Println(now)

    if err = db.AutoMigrate(&Expenses{}); err != nil {
        log.Println(err)
    }

    return db, err

}

Now, we will go to handlers.go, there we will code our handlers using Gorilla/Mux.

handlers.go

package main

import (
    "encoding/json"

    "net/http"

    "github.com/carlosm27/blog/cockroachdb-gorillamux/model"
    "github.com/gorilla/mux"
    "gorm.io/gorm"
)

type Server struct {
    db *gorm.DB
}

func NewServer(db *gorm.DB) *Server {
    return &Server{db: db}
}

func (s *Server) RegisterRouter(router *mux.Router) {
    router.HandleFunc("/expenses", s.getExpenses)
    router.HandleFunc("/expense/{id}", s.getExpense).Methods("GET")
    router.HandleFunc("/expense", s.createExpense).Methods("POST")
    router.HandleFunc("/expense/{id}", s.updateExpense).Methods("PUT")
    router.HandleFunc("/expense/{id}", s.deleteExpense).Methods("DELETE")

}

func (s *Server) getExpenses(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json; charset=UTF-8")
    var expenses []model.Expenses

    if err := s.db.Find(&expenses).Error; err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(expenses)

}

We define a Struct Server and its constructor. Then we define a method RegisterRouter with all the handlers we are going to use with their methods and paths. After, we define a variable expenses and use the method Find() and retrieve all the records from our database and send them as JSON.

func (s *Server) createExpense(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json; charset=UTF-8")
    var expense model.Expenses

    if err := json.NewDecoder(r.Body).Decode(&expense); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }


    newExpense := &model.Expenses{ Amount: expense.Amount, Description: expense.Description, Category: expense.Category}
    if err := s.db.Create(newExpense).Error; err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(newExpense)

}

We define a variable expense, decode the request body, and store it. Then we use the method Create() to add a new expense with the data that is passed to it and return it as JSON.

func (s *Server) getExpense(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json; charset=UTF-8")
    var expense model.Expenses
    vars := mux.Vars(r)
    id := vars["id"]

    if err := s.db.Where("id = ?", id).First(&expense).Error; err != nil {
        http.Error(w, err.Error(), http.StatusNotFound)
        return
    }
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(expense)
}

We extract the id parameters from the URI, and use it in the method Where, it searches the record that matches the id and returns it as JSON. If there is no record with the id it was passed, it will send a "Not found" code.

func (s *Server) updateExpense(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json; charset=UTF-8")
    var updateExpense UpdateExpense
    var expense model.Expenses

    vars := mux.Vars(r)
    id := vars["id"]

    if err := json.NewDecoder(r.Body).Decode(&updateExpense); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    if err := s.db.Where("id = ?", id).First(&expense).Error; err != nil {
        http.Error(w, err.Error(), http.StatusNotFound)
        return
    }

    if err := s.db.Model(&expense).Updates(&model.Expenses{Amount: updateExpense.Amount, Description: updateExpense.Description, Category: updateExpense.Category}).Error; err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(expense)

}

We define a variable updateExpense and store in it the decoded request body. Then, we extract the id from the path and pass it to the method Where, it searches for the record that matches the id, if there is no record with the id specified, it will return a "Not Found" response. If it finds the id, it uses the method Updates() to update the record with the data stored in updateExpense and returns a JSON response.

func (s *Server) deleteExpense(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json; charset=UTF-8")
    var expense model.Expenses
    vars := mux.Vars(r)
    id := vars["id"]

    if err := s.db.Where("id = ?", id).First(&expense).Error; err != nil {
        http.Error(w, err.Error(), http.StatusNotFound)
        return
    }
    if err := s.db.Delete(&expense).Error; err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode("Expense Deleted")

}

We extract the id parameters from the URI, and use it in the method Where, it searches for the record that matches the id passed and if it finds it, uses the method Delete() to delete the record and send the message " Expense Deleted". If there is no record with the id that was passed, it will send a Status Not Found code.

main.go

package main

import (
    "log"
    "net/http"

    "github.com/carlosm27/blog/cockroachdb-gorillamux/model"
    "github.com/gorilla/mux"
)


func main() {
    db, err := model.SetupDB()

    if err != nil {
        log.Println("Failed setting up database")
    }

    router := mux.NewRouter()

    server := NewServer(db)

    server.RegisterRouter(router)
    log.Fatal(http.ListenAndServe(":8000", router))

}

In main.go we initialize our database, create a router instance, pass the variable db as an argument to our server initializer and pass our router instance as an argument to the method RegisterRouter(). Then we use the ListenAndServer to run the server.

Conclusion

It is the first time I'm using CockroachDB and I really liked that it has examples for every language and drivers/orm frameworks it supports. I find really helpful that you can select a language, and driver in your dashboard and it generates a code snippet as a demonstration.

If you have any recommendations about other packages, how to improve my code, my English, or anything; please leave a comment or contact me through Twitter, LinkedIn.

The source code is here

Reference: