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 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 , 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 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 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 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 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 , 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 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, and GORM, but has one more lib yet to be explored, however on the first article I said I would explore the lib, and instead this, a new lib was suggested, the 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.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store