docs/Database
rico.siegrist edited this page 2025-09-11 10:14:18 +02:00

GORM Implementation Guide

Inhaltsverzeichnis

Installation

Grundlegende GORM-Installation

go get -u gorm.io/gorm

Datenbank-Driver installieren

SQLite:

go get -u gorm.io/driver/sqlite

PostgreSQL:

go get -u gorm.io/driver/postgres

MySQL:

go get -u gorm.io/driver/mysql

SQL Server:

go get -u gorm.io/driver/sqlserver

Datenbankverbindung

SQLite (Empfohlen für Entwicklung)

package main

import (
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

func main() {
    db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }
}

PostgreSQL (Empfohlen für Produktion)

import (
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

func connectPostgreSQL() {
    dsn := "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai"
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }
}

MySQL

import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

func connectMySQL() {
    dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }
}

Modelle und Beziehungen

GORM Struct Tags

type User struct {
    ID        uint      `gorm:"primaryKey"`                    // Primärschlüssel
    Username  string    `gorm:"unique;not null;size:255"`      // Eindeutig, nicht null, max 255 Zeichen
    Email     string    `gorm:"uniqueIndex"`                   // Eindeutiger Index
    Age       int       `gorm:"check:age > 13"`                // Check constraint
    CreatedAt time.Time `gorm:"autoCreateTime"`                // Automatisch bei Erstellung
    UpdatedAt time.Time `gorm:"autoUpdateTime"`                // Automatisch bei Update
    DeletedAt gorm.DeletedAt `gorm:"index"`                    // Soft delete
}

Beziehungen definieren

Has One (1:1):

type User struct {
    gorm.Model
    Profile Profile
}

type Profile struct {
    gorm.Model
    UserID uint
    User   User
}

Has Many (1:n):

type User struct {
    gorm.Model
    Articles []Article `gorm:"foreignKey:UserID"`
}

type Article struct {
    gorm.Model
    UserID uint
    User   User
}

Many to Many (n:m):

type User struct {
    gorm.Model
    Languages []Language `gorm:"many2many:user_languages;"`
}

type Language struct {
    gorm.Model
    Users []User `gorm:"many2many:user_languages;"`
}

Migrationen

AutoMigrate (Empfohlen für Entwicklung)

AutoMigrate erstellt Tabellen, fehlende Spalten und Indizes automatisch. Es löscht KEINE bestehenden Spalten oder Daten.

func autoMigrate() {
    db.AutoMigrate(&User{}, &Article{}, &Comment{})
}

Vorteile von AutoMigrate:

  • Einfach zu verwenden
  • Sicher (löscht keine Daten)
  • Gut für Entwicklung

Nachteile von AutoMigrate:

  • Begrenzte Kontrolle über Änderungen
  • Kann keine Spalten umbenennen oder löschen
  • Nicht ideal für komplexe Produktionsänderungen

Manuelle Migrationen

Für mehr Kontrolle kannst du manuelle Migrationen verwenden:

func manualMigration() {
    // Tabelle erstellen
    db.Migrator().CreateTable(&User{})

    // Spalte hinzufügen
    db.Migrator().AddColumn(&User{}, "NewColumn")

    // Index erstellen
    db.Migrator().CreateIndex(&User{}, "idx_user_email")

    // Spalte löschen (VORSICHT!)
    db.Migrator().DropColumn(&User{}, "OldColumn")

    // Tabelle umbenennen
    db.Migrator().RenameTable(&User{}, &NewUser{})
}

Migration-Strategien

Entwicklungsumgebung:

func initDevelopmentDB() {
    db.AutoMigrate(&User{}, &Article{}, &Comment{})

    // Entwicklungsdaten einfügen
    seedDevelopmentData()
}

Produktionsumgebung:

func initProductionDB() {
    // Versionierte Migrationen
    runMigrationV1()
    runMigrationV2()
    runMigrationV3()
}

func runMigrationV1() {
    if !db.Migrator().HasTable(&User{}) {
        db.AutoMigrate(&User{})
    }
}

Repository-Pattern

Basis Repository Interface

type Repository[T any] interface {
    Create(entity *T) error
    GetByID(id uint) (*T, error)
    GetAll() ([]T, error)
    Update(entity *T) error
    Delete(id uint) error
}

Spezifische Repository Implementierung

type UserRepository struct {
    db *gorm.DB
}

func NewUserRepository(db *gorm.DB) *UserRepository {
    return &UserRepository{db: db}
}

func (r *UserRepository) Create(user *User) error {
    return r.db.Create(user).Error
}

func (r *UserRepository) GetByUsername(username string) (*User, error) {
    var user User
    err := r.db.Where("username = ?", username).First(&user).Error
    return &user, err
}

Verschiedene Datenbanken

Konfiguration für verschiedene Umgebungen

package database

import (
    "os"
    "gorm.io/gorm"
    "gorm.io/driver/sqlite"
    "gorm.io/driver/postgres"
    "gorm.io/driver/mysql"
)

func InitDatabase() (*gorm.DB, error) {
    dbType := os.Getenv("DB_TYPE")

    switch dbType {
    case "postgres":
        return initPostgres()
    case "mysql":
        return initMySQL()
    case "sqlite", "":
        return initSQLite()
    default:
        return initSQLite() // Fallback
    }
}

func initPostgres() (*gorm.DB, error) {
    dsn := os.Getenv("DATABASE_URL")
    if dsn == "" {
        dsn = "host=localhost user=postgres password=postgres dbname=myapp port=5432 sslmode=disable"
    }
    return gorm.Open(postgres.Open(dsn), &gorm.Config{})
}

func initMySQL() (*gorm.DB, error) {
    dsn := os.Getenv("DATABASE_URL")
    if dsn == "" {
        dsn = "root:password@tcp(localhost:3306)/myapp?charset=utf8mb4&parseTime=True&loc=Local"
    }
    return gorm.Open(mysql.Open(dsn), &gorm.Config{})
}

func initSQLite() (*gorm.DB, error) {
    dbPath := os.Getenv("SQLITE_PATH")
    if dbPath == "" {
        dbPath = "./app.db"
    }
    return gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
}

Umgebungsvariablen (.env Datei)

Entwicklung (SQLite):

DB_TYPE=sqlite
SQLITE_PATH=./dev.db

Produktion (PostgreSQL):

DB_TYPE=postgres
DATABASE_URL=postgres://user:password@localhost/dbname?sslmode=disable

Testing (In-Memory SQLite):

DB_TYPE=sqlite
SQLITE_PATH=:memory:

Erweiterte Features

Hooks (Callbacks)

func (u *User) BeforeCreate(tx *gorm.DB) error {
    // Hash password before saving
    hashedPassword, err := hashPassword(u.Password)
    if err != nil {
        return err
    }
    u.Password = hashedPassword
    return nil
}

func (u *User) AfterFind(tx *gorm.DB) error {
    // Load additional data after finding
    return nil
}

Scopes

func ActiveUsers(db *gorm.DB) *gorm.DB {
    return db.Where("active = ?", true)
}

func AdminUsers(db *gorm.DB) *gorm.DB {
    return db.Where("is_admin = ?", true)
}

// Verwendung
var users []User
db.Scopes(ActiveUsers, AdminUsers).Find(&users)

Transaktionen

func TransferArticle(fromUserID, toUserID, articleID uint) error {
    return DB.Transaction(func(tx *gorm.DB) error {
        // Article ownership überprüfen
        var article Article
        if err := tx.First(&article, articleID).Error; err != nil {
            return err
        }

        if article.UserID != fromUserID {
            return errors.New("not authorized")
        }

        // Ownership ändern
        article.UserID = toUserID
        return tx.Save(&article).Error
    })
}

Soft Delete

type User struct {
    gorm.Model  // Enthält DeletedAt
    Username string
}

// Soft delete (setzt DeletedAt)
db.Delete(&user)

// Include soft deleted records
db.Unscoped().Find(&users)

// Permanently delete
db.Unscoped().Delete(&user)

Best Practices

1. Connection Pooling konfigurieren

func configureDB(db *gorm.DB) error {
    sqlDB, err := db.DB()
    if err != nil {
        return err
    }

    sqlDB.SetMaxIdleConns(10)
    sqlDB.SetMaxOpenConns(100)
    sqlDB.SetConnMaxLifetime(time.Hour)

    return nil
}

2. Logging konfigurieren

import "gorm.io/gorm/logger"

func newDB() *gorm.DB {
    db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info),
    })
    return db
}

3. Indizes für Performance

type User struct {
    ID       uint   `gorm:"primaryKey"`
    Username string `gorm:"uniqueIndex"`           // Eindeutiger Index
    Email    string `gorm:"index"`                 // Normaler Index
    Age      int    `gorm:"index:idx_age_name"`    // Composite Index
    Name     string `gorm:"index:idx_age_name"`    // Teil des Composite Index
}

4. Batch-Operationen

// Batch Insert
users := []User{{Username: "user1"}, {Username: "user2"}}
db.CreateInBatches(users, 100)

// Batch Update
db.Model(&User{}).Where("active = ?", false).Update("status", "inactive")

5. Prepared Statements verwenden

// Prepared statement für wiederholte Queries
stmt := db.Session(&gorm.Session{PrepareStmt: true})
for i := 0; i < 100; i++ {
    stmt.First(&user, i)
}

6. Fehlerbehandlung

func GetUserByID(id uint) (*User, error) {
    var user User
    err := db.First(&user, id).Error

    if errors.Is(err, gorm.ErrRecordNotFound) {
        return nil, fmt.Errorf("user with id %d not found", id)
    }

    if err != nil {
        return nil, fmt.Errorf("database error: %w", err)
    }

    return &user, nil
}

Migration von sqlx zu GORM

1. Dependencies aktualisieren

# Alte Dependencies entfernen
go mod tidy

# GORM hinzufügen
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite

2. Struct Tags ändern

// Vorher (sqlx)
type User struct {
    ID       int    `db:"id" json:"id"`
    Username string `db:"username" json:"username"`
}

// Nachher (GORM)
type User struct {
    ID       uint   `gorm:"primaryKey" json:"id"`
    Username string `gorm:"unique;not null" json:"username"`
}

3. Repository-Methoden anpassen

// Vorher (sqlx)
func GetUser(id int) (User, error) {
    var user User
    err := Db.Get(&user, "SELECT * FROM users WHERE id = ?", id)
    return user, err
}

// Nachher (GORM)
func GetUser(id uint) (User, error) {
    var user User
    err := DB.First(&user, id).Error
    return user, err
}

Diese Implementierung bietet dir eine solide Basis für die Verwendung von GORM mit verschiedenen Datenbankbackends und modernen Go-Praktiken.