Continuing looking for an ORM to database layer with Golang
Previously, to find a better way to build a database layer with Golang using an ORM, a library API was built using the REL lib on the database layer, as you can see below:
To continue this journey and not choose the first option that appears, a new lib will be explored, and now the chosen lib was the most famous GORM:
As was described before in the first article, the library API has the four CRUD operations and now I will show you how I built the database layer using GORM as lib.
To map the routes of the API was chosen the chi framework and the main function of the application will be:
Here I’m creating the repository that will have an instance of the database representation of GORM, and to create this instance it needs to pass the connection of the database and an instance of GORM configurations, here I’m only changing the log mode to be INFO
to see all generated SQL:
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
But you can pass only an empty instance of gorm.Config
if you want to use the default configurations of the GORM.
This instance of database it’s where I will use all the power that GORM provides to us, and I’m passing to the repository to be used inside of the database layer:
To start the database layer with GORM I had to map the entities as bellow:
The GORM has a few options to map the entity, for example, to easily mapping commons columns like ID, CreatedAt, UpdatedAt and DeletedAt, you can use the gorm.Model
struct as the base of your struct and so not need to write these fields to all your entities as a declarative way. But here I have chosen to not use this option because I can’t manipulate how these fields will be exposed to JSON, so I do the mapping directly as you can see:
package gormModel struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
As the column DeletedAt can be null, so is used the gorm.DeletedAt
struct that will manipulate correctly when this field is empty and will do a soft delete. This lib also fills these fields automatically and does not need to you fill in the creation. Here I only follow the same struct of the gorm.Model
but the lib allows you other options that you can see on the documentation. Using the tag gorm:"primarykey"
this mark what field has to map to the primary key of the table and using the tag gorm:"index"
will mark the field as an index of the table on the database.
To map the relationship between the book table and the shelf table I had to map with the tag gorm:"foreignkey:ShelfID"
on the field that contains the struct of the Shelf on the Book entity. And on the Shelf entity I only need to have the slice with the books that the lib will fill when using the Preload
function when do the query.
All the names will be the same at snake case than the name of columns of the tables on the database and the name of the tables by default is pluralized, but here to attends the initial migration, I implement the function TableName()
to customize the names of the tables:
func (Book) TableName() string {
return "book"
} func (Shelf) TableName() string {
return "shelf"
}
This lib has a lot of configurations that you can change to be the default of your application and not be needed to always customize something.
Well, since I already mapped the entities than can implement the functions needed to create the book and allow the API to insert new books.
To create a new book will be needed two functions, one to find the shelf that the book will belong and validate if the capacity supports and the function that will insert the new book on the database and increments the amount of the shelf, as you can see:
To fetch the shelf with your books I had to use the preload of the associations, and to do that I’m using the Preload
function of the GORM repository and next using the First
function to fetch the first registry that has the ID equals to the variable value shelfID:
err = r.db.Preload(clause.Associations).First(&shelf, shelfID).Error
All functions of GORM returns the struct of DB with the filled error when some wrong occurs and the count of rows affected, since I’m not worried about the rows affected, I’m only catching the value of Error
to verify if all occurred as expected.
On the console it’s possible to see the generated SQL by GORM:
[0.091ms] [rows:1] SELECT * FROM `book` WHERE `book`.`shelf_id` = 1 AND `book`.`deleted_at` IS NULL[0.281ms] [rows:1] SELECT * FROM `shelf` WHERE `shelf`.`id` = 1 AND `shelf`.`deleted_at` IS NULL ORDER BY `shelf`.`id` LIMIT 1
Now to create a book, the implementation of the InserBook
function had to have an implementation to insert the book and update the shelf amount on the same transaction:
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
book.ShelfID = book.BookShelf.ID
if err := tx.WithContext(ctx).Create(&book).Error; err != nil {
return err
}
if err := tx.WithContext(ctx).Model(&book.BookShelf).UpdateColumn("amount", len(book.BookShelf.Books)+1).Error; err != nil {
return err
}
return nil
})
Using the function Transaction
of db that expects a callback function that will be executed inside all on a single transaction, guarantees a rollback if something wrong occurs. So, I’m using this to insert the book and update the shelf safely.
But the GORM provides some interesting functionality, the hooks, that you can implements what will occur after the creation of the entity, and here I decided to change the insert of book removing the update of shelf and inserting this new function on the book entity:
func (book *Book) AfterCreate(tx *gorm.DB) error {
if book.BookShelf.ID == 0 {
return nil
}
if err := tx.Model(&book.BookShelf).UpdateColumn("amount", len(book.BookShelf.Books)+1).Error; err != nil {
return err
}
return nil
}
And the InsertBook
function becomes more cleaner:
func (r BooksRepository) InsertBook(ctx context.Context, book books.Book) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
book.ShelfID = book.BookShelf.ID
if err := tx.WithContext(ctx).Create(&book).Error; err != nil {
return err
}
return nil
})
}
Looking the console you can see the generated SQL:
Now I have the needed functions to insert the book following the business rules, let me talk about the tests with GORM.
As GORM has a feature to auto migrate by the mapped entities I decided to use the SQLITE database doing the auto migration to test all implementation of the database layer:
With this function it’s possible create the database of tests to each test of the repository as you can see on tests implemented to validate the search of the shelf and the creation of the book:
With all implemented to insert a new book, it’s time to show you how implements the function to fetch the book by ID and the test to validate the implementation:
To fetch the book by ID with GORM is too simple, just need to use the Preload
to fetch the shelf of the book and use the First
function passing the variable of the book to store the value and the ID:
err = r.db.WithContext(ctx).Preload("BookShelf").First(&book, bookID).Error
Following the CRUD operations, now you can see how implements the update of the book using GORM on your database layer with the test to validate if is what expects:
Here to allow update only a few fields of the book, I’m using the Updates
function passing a map with the fields that I want to update with the new values next to the function Model
that says to GORM what entity that will be updated.
Completing the CRUD operations, the function to delete a book was implemented in a simple way only using the function Delete
that will do a soft delete filling the DeletedAt
field that was mapped using the typegorm.DeletedAt
:
To finish the expected database layer, a function to find all books was implemented and next to a test validating the query:
With the GORM it’s just necessary use the Preload
function to fetch all the associations and use the Find
function passing the slice of the books to find all books.
Now the database layer was built one more time with no SQL code complexity, but the GORM does a lot of things in a magic way, for example, the hooks, looking on the database layer you haven’t to know that after the books are created the shelf will be updated.
All that was implemented you can see on my Github:
Please leave a comment or suggestion and let me know what you think about GORM. If you are familiar with another library, also leave a comment so we can compare them.
Until here two libs were explored, REL and GORM, but has one more lib yet to be explored, however on the first article I said I would explore the SQLX lib, and instead this, a new lib was suggested, the ent. lib a lib created by Facebook, so I decided to explore it since has a different approach and the SQLX still has a lot of SQL, which goes away the idea of this journey.
Thanks to Diego Henrique Oliveira and Alana Domit Bittar for the suggestions and the review.