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.
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