Egorm — A Simple and Powerful Wrapper of Gorm

Marten Gartner
5 min readApr 27, 2023
Foto von Caspar Camille Rubin auf Unsplash

Go is a lightweight and performant language which makes it an ideal choice for creating small, efficient services that can be easily deployed and maintained. The language’s straightforward syntax and small footprint mean that Go programs can run with minimal overhead. This makes Go particularly well-suited for environments where resources are limited. The Go ecosystem makes building and deploying microservices a breeze. Go’s built-in concurrency model, based on goroutines and channels, allows developers to create highly concurrent applications without the complexities often associated with multithreading. This makes it easier to build scalable microservices that can handle a large number of requests simultaneously. Finally, Go offers excellent performance, rivaling that of C and C++ in many cases. With its focus on simplicity and efficiency, Go provides a solid foundation for creating high-performance microservices that can meet the demands of modern applications.

Why We like Gorm

For microservices, database operations are often at the heart of their functionality. The ability to store, retrieve, and manipulate data efficiently and effectively is crucial to the success of any microservice-based architecture. Enter Gorm, a powerful Object-Relational Mapping (ORM) library for Go.

Gorm provides several benefits that make it an attractive choice for managing database operations in Go-based microservices:

  • Easy to use: Gorm’s intuitive API simplifies the process of working with databases, allowing developers to focus on implementing their microservices’ core functionality.
  • Feature-rich: Gorm offers a wide range of features such as associations, transactions, and query building, which makes it a versatile choice for various application requirements.
  • Extensible: Gorm’s plugin system enables developers to extend its functionality and adapt it to their specific needs.
  • Cross-database support: Gorm supports a variety of databases, including PostgreSQL, MySQL, SQLite, and SQL Server, making it suitable for projects with diverse database requirements.

Why EGorm?

Despite its many benefits, Gorm can still require a significant amount of boilerplate code to set up and use effectively. This article aims to introduce E(asy)Gorm, an abstraction of Gorm that leverages Go generics to further simplify its usage. Egorm’s goal is to minimize the amount of boilerplate code required to work with Gorm, making it even easier to integrate Gorm into your microservices.

In the following sections, we will walk through the implementation of Egorm, covering setting up Gorm, using Egorm to interact with your database, and automating migrations. By the end of this article, you should have a solid understanding of how EGorm can enhance your Gorm experience and streamline the development of your Go-based microservices.

Implementation and Usage

In this section we will have a look at how EGorm is implemented and how it can be used. Egorm targets to give you as a developer just the core methods that you need to perform database operations, e.g. the common CRUD operations. However, there is a small fraction of code required to setup EGorm.

Setting up EGorm

To setup database connections EGorm provides a list of setup methods, one for each supported database type, e.g. SQLite or Postgres. Let’s have a look at the SQLite setup code. To configure EGorm to use an sqlite connection, all we need to do is to call SetSQLiteConnectOpts before the first database operation, e.g. when our service starts, or to configure the respectire EGORM_DB=sqlite and EGORM_DB_SQLITE_PATH=egorm.sqlite. To connect to e.g. postgres, the code looks similar, including a configuration via environment variables. When the database is configured via environment, this allows to immediately perform database operations without calling any setup method before.


// SQLITE
// Via environment
// EGORM_DB_SQLITE=egorm_test.sqlite

// Via setup method
SetSQLiteConnectOpts(&egorm.SQLiteConnectOpts{
Path: path.Join(os.TempDir(), "egorm_test.sqlite"),
})

// POSTGRES
// Via environment
// EGORM_DB_POSTGRES_HOST=postgres
// EGORM_DB_POSTGRES_PORT=5432
// EGORM_DB_POSTGRES_DATABASE=postgres
// EGORM_DB_POSTGRES_USER=user
// EGORM_DB_POSTGRES_PASSWORD=user123

// Via setup method
SetPostgresConnectOpts(&egorm.PostgresConnectOpts{
Host "postgres",
Port 5432,
Database "postgres",
User "user",
Password "user123",
})

Thats all. We are now ready to perform any operation on the configured database.

Insert into Database

Let’s go through the common CRUD case and have a look how EGorm implements all these cases. We start with inserting entities into the database using EGorms DbCreatemethod:

type SampleStruct struct {
gorm.Model
Name string
}

func insertSample() error {
sample1 := SampleStruct{
Name: "Sample1",
}
err := egorm.DbCreate(&sample1)
if err != nil {
return err
}
return nil
}

It takes the reference to a struct as argument that it will insert into the database. After inserting, the struct will have a primary key in case the insert was successful, otherwise an error will be returned. Comparing this to gorm’s documentation may let you think that there is no real benefit of using egorm instead of gorm, both operations look quite similar. If you have done the proper database setup and migrations for gorm, then this is true. But egorm does all of this automatically. Let’s have a look at the internal implementation:

func DbCreate[T any](input *T) error {
if err := InitDB(); err != nil {
return err
}
err := autoMigrate(input)
if err != nil {
return err
}
result := Db.Create(input)
if result.Error != nil {
return result.Error
}
return nil
}

It performs 3 important steps:

  1. The InitDB() call ensures that the database setup is done correctly (it is ensured that it is done exactly once).
  2. The automigrate(input) call checks if the passed type was already migrated, if not it will perform gorms automigration .
  3. Now the actual insert operation using an obtained gorm.DB instance takes place.

Retrieving Entries from the Database

In the next code fragment we show how to fetch all entries from the database using egorms DbGetAll method.

type SampleStruct struct {
gorm.Model
Name string
}

func getAllSamples() ([]SampleStruct, error) {
var samples []SampleStruct
err := DbGetAll(&samples)
if err != nil {
return nil, err
}
return samples, nil
}

The DbGetAll method is also performing automigrations if required in the background to ensure consistency in the database.

Egorm also provides simple filter methods using a map, these can be applied for DbGet and DbFind to filter according to specific properties of the entries. The filter currently only supports to compare via equals, but we plan to extend it soon. In the following code sample we show how to filter entries using these two methods:

func firstSample() (*SampleStruct, error) {
var sample SampleStruct
err := DbFirst(&sample, Where{"Name": "Sample2"})
if err != nil {
return nil, err
}
return &sample, nil
}


func getSamples() ([]SampleStruct, error) {
var samples []SampleStruct
err := DbGet(&samples, Where{"Name": "Sample2"})
if err != nil {
return nil, err
}
return samples, nil
}

You can find examples for all provided methods in the tests we implemented for egorm.

Conclusions

In this article, we show a simple but powerful wrapper of gorm, called egorm. It reduces boilerplate code by allowing to call database operations anywhere, while only a setup call or configuration via environment variables is required. Although the available features of egorm are quite sparse at the moment, we observed great developer experience so far when using egorm, and we plan to extend the featureset soon. In any case, you can access the gorm.DB struct that is used under the hood via egorm.Db.

--

--

Marten Gartner

I’m a dad, software developer and researcher on network performance. Writing about high-speed frontend-dev, neworking, productivity and homeoffice with kids.