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.
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.
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.
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
:
Go1package 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 totodo.db
usingGorm
andSQLite
. - 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.
To enable a correct integration with the database, we introduce an important update to the Todo
model:
Go1package 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.
To interact with the database, we'll separate the database access logic into repositories/todo_repository/todo_repository.go
:
Go1package 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 thetodos
slice. It queries theTodo
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 atodo
object and creates a corresponding entry in theTodo
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
:
Go1package 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.
Finally, let's integrate this new addition with our router. The routing logic, as usual, is defined in router/router.go
:
Go1package 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
:
Go1package 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
).
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!