Bascule sur l'ORM GORM
- On n'utilise plus la pattern CQRS trop lourde pour le système - Un système de models/repository "à la Symfony" est utilisé pour les requêtes
This commit is contained in:
84
internal/orm/migration.go
Normal file
84
internal/orm/migration.go
Normal file
@ -0,0 +1,84 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
)
|
||||
|
||||
type MigrationFunc func(ctx context.Context, tx *gorm.DB) error
|
||||
|
||||
type Migration interface {
|
||||
Version() string
|
||||
Up(context.Context) error
|
||||
Down(context.Context) error
|
||||
}
|
||||
|
||||
type DBMigration struct {
|
||||
version string
|
||||
up MigrationFunc
|
||||
down MigrationFunc
|
||||
}
|
||||
|
||||
func (m *DBMigration) Version() string {
|
||||
return m.version
|
||||
}
|
||||
|
||||
func (m *DBMigration) Up(ctx context.Context) error {
|
||||
db, err := m.getDatabase(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = WithTx(ctx, db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
return m.up(ctx, tx)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not apply up migration")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *DBMigration) Down(ctx context.Context) error {
|
||||
db, err := m.getDatabase(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = WithTx(ctx, db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
return m.down(ctx, tx)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not apply down migration")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *DBMigration) getDatabase(ctx context.Context) (*gorm.DB, error) {
|
||||
ctn, err := container.From(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve service container")
|
||||
}
|
||||
|
||||
orm, err := From(ctn)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve orm service")
|
||||
}
|
||||
|
||||
return orm.DB(), nil
|
||||
}
|
||||
|
||||
func NewDBMigration(version string, up, down MigrationFunc) *DBMigration {
|
||||
return &DBMigration{
|
||||
version: version,
|
||||
up: up,
|
||||
down: down,
|
||||
}
|
||||
}
|
146
internal/orm/migration_manager.go
Normal file
146
internal/orm/migration_manager.go
Normal file
@ -0,0 +1,146 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoAvailableMigration = errors.New("no available migration")
|
||||
ErrMigrationNotFound = errors.New("migration not found")
|
||||
)
|
||||
|
||||
type MigrationManager struct {
|
||||
migrations []Migration
|
||||
resolver VersionResolver
|
||||
}
|
||||
|
||||
func (m *MigrationManager) Up(ctx context.Context) error {
|
||||
currentVersion, err := m.resolver.Current(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve current version")
|
||||
}
|
||||
|
||||
migrate := func(up Migration) error {
|
||||
if err := up.Up(ctx); err != nil {
|
||||
return errors.Wrapf(err, "could not apply '%s' up migration", up.Version())
|
||||
}
|
||||
|
||||
if err := m.resolver.Set(ctx, up.Version()); err != nil {
|
||||
return errors.Wrapf(err, "could not update schema version to '%s'", up.Version())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if currentVersion == "" {
|
||||
up := m.migrations[0]
|
||||
|
||||
return migrate(up)
|
||||
}
|
||||
|
||||
for i, mi := range m.migrations {
|
||||
if mi.Version() != currentVersion && currentVersion != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Already at latest, do nothing
|
||||
if i >= len(m.migrations)-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
up := m.migrations[i+1]
|
||||
|
||||
return migrate(up)
|
||||
}
|
||||
|
||||
return errors.WithStack(ErrMigrationNotFound)
|
||||
}
|
||||
|
||||
func (m *MigrationManager) Down(ctx context.Context) error {
|
||||
currentVersion, err := m.resolver.Current(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve current version")
|
||||
}
|
||||
|
||||
for i, mi := range m.migrations {
|
||||
if mi.Version() != currentVersion {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := mi.Down(ctx); err != nil {
|
||||
return errors.Wrapf(err, "could not apply '%s' down migration", mi.Version())
|
||||
}
|
||||
|
||||
var version string
|
||||
|
||||
// Already at oldest, do nothing
|
||||
if i != 0 {
|
||||
down := m.migrations[i-1]
|
||||
version = down.Version()
|
||||
}
|
||||
|
||||
if err := m.resolver.Set(ctx, version); err != nil {
|
||||
return errors.Wrapf(err, "could not update schema version to '%s'", version)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.WithStack(ErrMigrationNotFound)
|
||||
}
|
||||
|
||||
func (m *MigrationManager) Latest(ctx context.Context) error {
|
||||
for {
|
||||
isLatest, err := m.IsLatest(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve schema state")
|
||||
}
|
||||
|
||||
if isLatest {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m.Up(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MigrationManager) Register(migrations ...Migration) {
|
||||
m.migrations = migrations
|
||||
}
|
||||
|
||||
func (m *MigrationManager) CurrentVersion(ctx context.Context) (string, error) {
|
||||
return m.resolver.Current(ctx)
|
||||
}
|
||||
|
||||
func (m *MigrationManager) LatestVersion() (string, error) {
|
||||
if len(m.migrations) == 0 {
|
||||
return "", errors.WithStack(ErrNoAvailableMigration)
|
||||
}
|
||||
|
||||
return m.migrations[len(m.migrations)-1].Version(), nil
|
||||
}
|
||||
|
||||
func (m *MigrationManager) IsLatest(ctx context.Context) (bool, error) {
|
||||
currentVersion, err := m.resolver.Current(ctx)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "could not retrieve current version")
|
||||
}
|
||||
|
||||
latestVersion, err := m.LatestVersion()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "could not retrieve latest version")
|
||||
}
|
||||
|
||||
return currentVersion == latestVersion, nil
|
||||
}
|
||||
|
||||
func NewMigrationManager(resolver VersionResolver) *MigrationManager {
|
||||
return &MigrationManager{
|
||||
resolver: resolver,
|
||||
migrations: make([]Migration, 0),
|
||||
}
|
||||
}
|
49
internal/orm/provider.go
Normal file
49
internal/orm/provider.go
Normal file
@ -0,0 +1,49 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
|
||||
// Import postgres dialect
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
)
|
||||
|
||||
func ServiceProvider(dialect, dsn string, debug bool) service.Provider {
|
||||
db, err := gorm.Open(dialect, dsn)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "could not connect to database")
|
||||
}
|
||||
|
||||
var srv *Service
|
||||
|
||||
if err == nil {
|
||||
db = db.LogMode(debug)
|
||||
|
||||
versionResolver := NewDBVersionResolver(db)
|
||||
ctx := context.Background()
|
||||
|
||||
err := versionResolver.Init(ctx)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "could not initialize version resolver")
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
srv = &Service{
|
||||
db: db,
|
||||
migration: NewMigrationManager(versionResolver),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return func(ctn *service.Container) (interface{}, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
}
|
47
internal/orm/service.go
Normal file
47
internal/orm/service.go
Normal file
@ -0,0 +1,47 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
)
|
||||
|
||||
const ServiceName service.Name = "orm"
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
migration *MigrationManager
|
||||
}
|
||||
|
||||
func (s *Service) DB() *gorm.DB {
|
||||
return s.db
|
||||
}
|
||||
|
||||
func (s *Service) Migration() *MigrationManager {
|
||||
return s.migration
|
||||
}
|
||||
|
||||
// From retrieves the orm service in the given container.
|
||||
func From(container *service.Container) (*Service, error) {
|
||||
service, err := container.Service(ServiceName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName)
|
||||
}
|
||||
|
||||
srv, ok := service.(*Service)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName)
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// Must retrieves the orm pool service in the given container or panic otherwise.
|
||||
func Must(container *service.Container) *Service {
|
||||
srv, err := From(container)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return srv
|
||||
}
|
47
internal/orm/tx.go
Normal file
47
internal/orm/tx.go
Normal file
@ -0,0 +1,47 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func WithTx(ctx context.Context, db *gorm.DB, fn func(context.Context, *gorm.DB) error) error {
|
||||
tx := db.BeginTx(ctx, &sql.TxOptions{})
|
||||
|
||||
defer func() {
|
||||
if err := tx.Rollback().Error; err != nil && !isGormError(err, gorm.ErrInvalidTransaction) {
|
||||
panic(errors.Wrap(err, "could not rollback transaction"))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := fn(ctx, tx); err != nil {
|
||||
err := errors.Wrap(err, "could not apply down migration")
|
||||
|
||||
if rollbackErr := tx.Rollback().Error; rollbackErr != nil {
|
||||
return errors.Wrap(err, rollbackErr.Error())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return errors.Wrap(err, "could not commit transaction")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isGormError(err error, compErr error) bool {
|
||||
if errs, ok := err.(gorm.Errors); ok {
|
||||
for _, err := range errs {
|
||||
if errors.Is(err, compErr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Is(err, compErr)
|
||||
}
|
112
internal/orm/version_resolver.go
Normal file
112
internal/orm/version_resolver.go
Normal file
@ -0,0 +1,112 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type VersionResolver interface {
|
||||
Current(context.Context) (string, error)
|
||||
Set(context.Context, string) error
|
||||
}
|
||||
|
||||
type DBVersionResolver struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type DatabaseVersion struct {
|
||||
ID uint `gorm:"primary_key"`
|
||||
Version string `gorm:"unique; not null"`
|
||||
MigratedAt time.Time
|
||||
IsCurrent bool
|
||||
}
|
||||
|
||||
func (r *DBVersionResolver) Current(ctx context.Context) (string, error) {
|
||||
var version string
|
||||
|
||||
err := WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
dbVersion := &DatabaseVersion{}
|
||||
err := tx.Where("is_current = ?", true).First(dbVersion).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
version = dbVersion.Version
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "could execute version resolver init transaction")
|
||||
}
|
||||
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func (r *DBVersionResolver) Set(ctx context.Context, version string) error {
|
||||
err := WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
dbVersion := &DatabaseVersion{
|
||||
Version: version,
|
||||
MigratedAt: time.Now(),
|
||||
}
|
||||
|
||||
if version != "" {
|
||||
if err := tx.FirstOrCreate(dbVersion).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := tx.Model(dbVersion).
|
||||
UpdateColumn("is_current", true).Error
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
err := tx.Model(&DatabaseVersion{}).
|
||||
Where("version <> ?", version).
|
||||
UpdateColumn("is_current", false).Error
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not update schema version")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DBVersionResolver) Init(ctx context.Context) error {
|
||||
err := WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&DatabaseVersion{}).Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := tx.Model(&DatabaseVersion{}).AddUniqueIndex("idx_unique_version", "version").Error; err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could execute version resolver init transaction")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewDBVersionResolver(db *gorm.DB) *DBVersionResolver {
|
||||
return &DBVersionResolver{
|
||||
db: db,
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user