Lesson 1
Persisting Items by Introducing a Database
Introduction

Welcome to the first lesson of our course on building an Enterprise-grade ToDo app using Go and Gin. In this lesson, we will explore how to persist ToDo items by integrating a database into our application. Persisting data means storing it in a manner that allows it to be accessed even after the application has been restarted. This is crucial for real-world applications where data retention is essential.

By the end of this lesson, you'll understand the importance of data persistence and how to implement it using a database. We'll introduce key concepts such as Object-Relational Mapping (ORM) and SQL databases, and particularly focus on implementing a SQLite database with the Gorm library. All of this will aid in managing our ToDo items efficiently.

Overview of SQL Databases and SQLite

SQL databases are systems designed to store and manage data in a structured format using SQL (Structured Query Language). They are essential for applications that require data persistence and easy accessibility over time. SQL databases are relational, meaning they organize data into tables. A table is a collection of related data entries and consists of rows and columns. Each column in a table represents a different attribute of the data, with a specific data type, and each row is a record that contains data for each column. This tabular structure allows for efficient data storage, retrieval, and manipulation by enabling SQL queries to filter, sort, and join tables to gather and analyze information across related datasets.

In this lesson, we will use SQLite as our database system. SQLite is a lightweight, serverless, and self-contained database engine. Its simplicity and lack of configuration requirements make it ideal for development environments and smaller applications. Despite its small footprint, SQLite is powerful and provides the SQL support needed for building robust applications. This makes it an excellent choice for our initial demonstration of database integration in a ToDo application.

Introduction to ORM (Object-Relational Mapping)

ORM, or Object-Relational Mapping, is a technique that facilitates interaction with SQL databases through an object-oriented programming paradigm. It bridges the gap between your application's data models and the underlying database tables. By using ORM, developers can execute database operations via code without writing SQL statements directly.

In this lesson, we will utilize Gorm, a powerful ORM library for Go. Gorm simplifies database interactions by mapping Go structures to SQL tables, making operations intuitive and significantly reducing the need for boilerplate code. This library will enable us to seamlessly connect our Go application to a SQLite database and perform CRUD (Create, Read, Update, Delete) operations efficiently.

Connecting to the SQLite Database

Let's move on to implementing the database connection in Go using Gorm and SQLite. We start by creating a new repositories/ package that will host all database access logic. We then start by implementing the connection logic in repositories/db/connection.go:

Go
1package db 2 3import ( 4 "gorm.io/driver/sqlite" 5 "gorm.io/gorm" 6 7 "codesignal.com/todoapp/models" 8) 9 10func ConnectDatabase() *gorm.DB { 11 db, err := gorm.Open(sqlite.Open("todo.db"), &gorm.Config{}) 12 if err != nil { 13 panic("Failed to connect to the database") 14 } 15 16 db.AutoMigrate(&models.Todo{}) 17 return db 18}

In this code, we:

  • Define the ConnectDatabase function to open a connection to todo.db using Gorm and SQLite.
  • Use gorm.Open() to initiate the connection and handle errors with a panic if it fails.

Gorm’s AutoMigrate function automates the creation and modification of database tables to align with model structures. It creates tables, adds missing columns, and updates column types to ensure the database matches the model. This automation helps maintain a consistent database structure with minimal effort. Please note that AutoMigrate doesn't handle operations like renaming columns or deleting tables, which need manual management.

Updating the ToDo Model

To enable a correct integration with the database, we introduce an important update to the Todo model:

Go
1package models 2 3type Todo struct { 4 ID uint `json:"id" gorm:"primaryKey"` // ID is now a primary key 5 Title string `json:"title"` 6 Completed bool `json:"completed"` 7}

This update designates the ID field as the primary key using the gorm:"primaryKey" annotation. A primary key is a unique identifier for each record in a SQL database table, essential for indexing and accessing data efficiently. The gorm annotation simplifies database operations by allowing Gorm to automatically manage the primary key, thereby optimizing data retrieval and management.

Interacting with the database

To interact with the database, we'll separate the database access logic into repositories/todo_repository/todo_repository.go:

Go
1package todo_repository 2 3import ( 4 "gorm.io/gorm" 5 6 "codesignal.com/todoapp/models" 7) 8 9func FindAllTodos(db *gorm.DB) []models.Todo { 10 var todos []models.Todo 11 db.Find(&todos) 12 return todos 13} 14 15func CreateTodo(db *gorm.DB, todo models.Todo) models.Todo { 16 db.Create(&todo) 17 return todo 18}

In this snippet, we are employing two main methods from the Gorm library:

  • db.Find(&todos): This method retrieves all records from the database and populates them into the todos slice. It queries the Todo table and returns a list of all existing ToDo items, allowing for easy access to the data stored in the database.
  • db.Create(&todo): This method inserts a new record into the database. It takes a todo object and creates a corresponding entry in the Todo table. This operation facilitates adding new ToDo items to the database, ensuring that they are persisted for future access.

On the other hand, the business logic is defined as usual in services/services.go:

Go
1package services 2 3import ( 4 "gorm.io/gorm" 5 6 "codesignal.com/todoapp/models" 7 "codesignal.com/todoapp/repositories/todo_repository" 8) 9 10func GetTodos(db *gorm.DB) []models.Todo { 11 return todo_repository.FindAllTodos(db) 12} 13 14func AddTodo(db *gorm.DB, newTodo models.Todo) models.Todo { 15 newTodo.Completed = false 16 return todo_repository.CreateTodo(db, newTodo) 17}

Here, we ensure the business logic is responsible for setting proper defaults (like setting Completed to false for new todos) and interacting with the repository layer.

Integrating the Database with Gin Router

Finally, let's integrate this new addition with our router. The routing logic, as usual, is defined in router/router.go:

Go
1package router 2 3import ( 4 "gorm.io/gorm" 5 "github.com/gin-gonic/gin" 6 7 "codesignal.com/todoapp/controllers" 8) 9 10func RegisterRoutes(router *gin.Engine, db *gorm.DB) { 11 router.GET("/api/todos", func(c *gin.Context) { 12 controllers.GetTodosHandler(c, db) 13 }) 14 15 router.POST("/api/todos", func(c *gin.Context) { 16 controllers.CreateTodoHandler(c, db) 17 }) 18}

Lastly, the handler functions in controllers/controllers.go:

Go
1package controllers 2 3import ( 4 "net/http" 5 6 "github.com/gin-gonic/gin" 7 "gorm.io/gorm" 8 9 "codesignal.com/todoapp/models" 10 "codesignal.com/todoapp/services" 11) 12 13func GetTodosHandler(c *gin.Context, db *gorm.DB) { 14 todos := services.GetTodos(db) 15 c.JSON(http.StatusOK, todos) 16} 17 18func CreateTodoHandler(c *gin.Context, db *gorm.DB) { 19 var newTodo models.Todo 20 if err := c.ShouldBindJSON(&newTodo); err != nil { 21 c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON"}) 22 return 23 } 24 createdTodo := services.AddTodo(db, newTodo) 25 c.JSON(http.StatusCreated, createdTodo) 26}

These handlers delegate business logic to the service layer, ensuring the application architecture aligns with good practices for separation of concerns. Please note that, in this context, db is a pointer to the database connection object (gorm.DB).

Summary and Preparation for Practice

In this lesson, we explored how to persist data in a ToDo application using a database. We learned about the significance of data persistence, the utility of ORMs with a focus on Gorm, and how to integrate SQLite as our database solution. Moreover, we created a Todo model, connected our application to a database, authored a clear separation of responsibilities across data access, business logic, and routing, and set up HTTP routes to handle ToDo item retrieval and creation.

This foundation equips you with the knowledge needed to handle persistent data in a Go application effectively. As you proceed to the practice exercises, focus on applying these concepts to enhance your understanding and proficiency. Enjoy the challenge and continue building on this solid foundation!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.