Skip to content

dentech-floss/orm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

37 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

orm

For Object Relational Mappings we use GORM which is wrapped here and configured to support accessing "Cloud SQL for MySQL" from services running on Cloud Run. Connecting to a locally running MySQL is of course also possible, as well as creating an in-memory SQLite database to use in tests.

For performance reasons, the underlying connection pool is preconfigured in accordance to this great read: Configuring sql.DB for Better Performance, so there should be no need for any custom configuration (all though it is possible).

Opentelemetry instrumentation is also configured via the otelgorm plugin which records database queries and reports metrics for the current span. In order for the Opentelemetry plugin to work, it is vital that "WithContext(ctx)" is used!!! See the example below.

Do also check out the dentech-floss/pagination lib which goes hand in hand with this lib.

Install

go get github.com/dentech-floss/orm@v0.1.5

Usage

Given this simple model we illustrate the usage of this lib with a quite extensive example, all this layering is obviously not required but this design/abstraction makes the code flexible/mockable and loosly coupled to the actual underlying datasource. So it's easy to swap the datasource, like injecting a SQLite instance while testing for example.

package model

type Clinic struct {
    ID          int32 `gorm:"primaryKey; autoIncrement"`
    Name        *string `gorm:"size:100"`
    Description *string `gorm:"size:255"`
    Created     time.Time
}

Create an ORM instance and inject it into an implementation of the repository tier, which then is injected into the service(s):

package example

import (
    "github.com/dentechse/some-service/internal/domain/repository"
    "github.com/dentechse/some-service/pkg/service"

    "github.com/dentech-floss/metadata/pkg/metadata"
    "github.com/dentech-floss/orm/pkg/orm"
)

func main() {

    metadata := metadata.NewMetadata()

    orm := orm.NewMySqlOrm(
        &orm.OrmConfig{
            OnGCP:      metadata.OnGCP,
            DbName:     "clinic",
            DbUser:     "some_user",
            DbPassword: "some_pwd",
            DbHost:     "some_host",
            // DbPort:     3306, // not mandatory, will default to 3306 if not provided
        },
    )

    repo := repository.NewSqlRepository(orm)
    patientGatewayServiceV1 := service.NewPatientGatewayServiceV1(repo)
}

For the sake of completeness, here is the mentioned repository interface:

package repository

import (
    "context"

    "github.com/dentechse/some-service/internal/domain/model"
    "github.com/dentech-floss/orm/pkg/orm"
)

type Repository interface {
    // returns nil if the clinic was not found
    FindClinicById(ctx context.Context, clinicId int32) (*model.Clinic, error)
}

...and its sql implementation based on GORM (notice that "WithContext(ctx)" must be used for the tracing to work):

package repository

import (
    "github.com/dentechse/some-service/internal/domain/model"
    "github.com/dentech-floss/orm/pkg/orm"

    "gorm.io/gorm"
)

type sqlRepository struct {
    orm *orm.Orm
}

func NewSqlRepository(orm *orm.Orm) Repository {
    return &sqlRepository{orm: orm}
}

func (r *sqlRepository) FindClinicById(ctx context.Context, clinicId int32) (*model.Clinic, error) {

	var clinic *model.Clinic = nil
	if err := r.orm.
		WithContext(ctx). // to propagate the active span for tracing!!!
		First(&clinic, clinicId).Error; err != nil {
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, err
		}
	}
	return clinic, nil
}

Migration

This lib includes Gormigrate which is a minimalistic migration helper for GORM that provide support for schema versioning and migration rollback. Gormigrate is in other words more advanced and robust/reliable to use instead of the standard GORM Migrator Interface so this is the recommeded approach, but both options are supported as shown in the below examples.

Gormigrate

package example

import (
    "github.com/dentech-floss/metadata/pkg/metadata"
    "github.com/dentech-floss/orm/pkg/migration"
    "github.com/dentech-floss/orm/pkg/orm"

    "gorm.io/gorm"
    "github.com/go-gormigrate/gormigrate/v2"
)

func main() {

    metadata := metadata.NewMetadata()

    orm := orm.NewMySqlOrm(
        &orm.OrmConfig{
            OnGCP:      metadata.OnGCP,
            DbName:     "clinic",
            DbUser:     "some_user",
            DbPassword: "some_pwd",
            DbHost:     "some_host",
        },
    )

    if err := runMigrations(orm); err != nil {
        panic(err)
    }
}

func runMigrations(orm *orm.Orm) error {
    // Or use 'migration.NewMigration(orm, migration.WithUseTransaction)' to just enable the 'UseTransaction' options flag
    return migration.
        NewMigration(orm).
        RunMigrations([]*gormigrate.Migration{
        // create persons table
        {
            ID: "201608301400",
            Migrate: func(tx *gorm.DB) error {
                // it's a good pratice to copy the struct inside the function,
                // so side effects are prevented if the original struct changes during the time
                type Person struct {
                    gorm.Model
                    Name string
                }
                return tx.AutoMigrate(&Person{})
            },
            Rollback: func(tx *gorm.DB) error {
                return tx.Migrator().DropTable("persons")
            },
        },
        // add age column to persons
        {
            ID: "201608301415",
            Migrate: func(tx *gorm.DB) error {
                // when table already exists, it just adds fields as columns
                type Person struct {
                    Age int
                }
                return tx.AutoMigrate(&Person{})
            },
            Rollback: func(tx *gorm.DB) error {
                return tx.Migrator().DropColumn("persons", "age")
            },
        },
        // add pets table
        {
            ID: "201608301430",
            Migrate: func(tx *gorm.DB) error {
                type Pet struct {
                    gorm.Model
                    Name     string
                    PersonID int
                }
                return tx.AutoMigrate(&Pet{})
            },
            Rollback: func(tx *gorm.DB) error {
                return tx.Migrator().DropTable("pets")
            },
        },
    })
}

You can rollback last applied migration (could be run more times):

package example

import (
    "github.com/dentech-floss/metadata/pkg/metadata"
    "github.com/dentech-floss/orm/pkg/orm"
    "github.com/dentech-floss/orm/pkg/migration"

    "gorm.io/gorm"
    "github.com/go-gormigrate/gormigrate/v2"
)

func main() {

    metadata := metadata.NewMetadata()

    orm := orm.NewMySqlOrm(
        &orm.OrmConfig{
            OnGCP:      metadata.OnGCP,
            DbName:     "clinic",
            DbUser:     "some_user",
            DbPassword: "some_pwd",
            DbHost:     "some_host",
        },
    )

    if err := runRollback(orm); err != nil {
        panic(err)
    }
}

func runRollback(orm *orm.Orm) error {
    // Or use 'migration.NewMigration(orm, migration.WithUseTransaction)' to just enable the 'UseTransaction' options flag
    return migration.
        NewMigration(orm).
        RollbackLastMigration([]*gormigrate.Migration{
        // create persons table
        {
            ID: "201608301400",
            Migrate: func(tx *gorm.DB) error {
                // it's a good pratice to copy the struct inside the function,
                // so side effects are prevented if the original struct changes during the time
                type Person struct {
                    gorm.Model
                    Name string
                }
                return tx.AutoMigrate(&Person{})
            },
            Rollback: func(tx *gorm.DB) error {
                return tx.Migrator().DropTable("persons")
            },
        },
        // add age column to persons
        {
            ID: "201608301415",
            Migrate: func(tx *gorm.DB) error {
                // when table already exists, it just adds fields as columns
                type Person struct {
                    Age int
                }
                return tx.AutoMigrate(&Person{})
            },
            Rollback: func(tx *gorm.DB) error {
                return tx.Migrator().DropColumn("persons", "age")
            },
        },
        // add pets table
        {
            ID: "201608301430",
            Migrate: func(tx *gorm.DB) error {
                type Pet struct {
                    gorm.Model
                    Name     string
                    PersonID int
                }
                return tx.AutoMigrate(&Pet{})
            },
            Rollback: func(tx *gorm.DB) error {
                return tx.Migrator().DropTable("pets")
            },
        },
    })
}

GORM Migrator Interface

If you for some reason do not want to use Gormigrate, then you can get hold of the standard GORM Migrator Interface and for example it's Auto Migration like this:

package example

import (
    "github.com/dentech-floss/metadata/pkg/metadata"
    "github.com/dentech-floss/orm/pkg/orm"
)

func main() {

    metadata := metadata.NewMetadata()

    orm := orm.NewMySqlOrm(
        &orm.OrmConfig{
            OnGCP:      metadata.OnGCP,
            DbName:     "clinic",
            DbUser:     "some_user",
            DbPassword: "some_pwd",
            DbHost:     "some_host",
        },
    )

    if err := orm.Migrator().AutoMigrate(&model.Clinic{}); err != nil {
        panic(err)
    }
}

Testing

To create and inject an in-memory SQLite database for testing:

package example

import (
    "testing"
    "github.com/dentech-floss/orm/pkg/orm"
)

func Test_FindClinicById(t *testing.T) {

    // in-memory database for testing (and create the table)
    orm := orm.NewSQLiteOrm(&orm.OrmConfig{})

    // ...or use the Gormigrate support as described above...
    if err := orm.GetMigrator().AutoMigrate(&model.Clinic{}); err != nil {
        panic(err)
    }

    repo := repository.NewSqlRepository(orm)
    patientGatewayServiceV1 := service.NewPatientGatewayServiceV1(repo) // we could inject a mock here otherwise

    patientGatewayServiceV1.FindClinicById(...)
}