Настройка GORM для Go веб-приложения

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Настройка GORM для Go веб-приложения
Средняя
~1 рабочий день
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Настройка GORM для Go веб-приложения

GORM — наиболее популярный ORM для Go. Не пытается скрыть SQL полностью, даёт прямой доступ к raw queries там, где это нужно. Версия v2 несовместима с v1 по API.

go get gorm.io/gorm
go get gorm.io/driver/postgres
# или MySQL:
go get gorm.io/driver/mysql

Инициализация подключения

// internal/db/db.go
package db

import (
    "fmt"
    "log"
    "os"
    "time"

    "gorm.io/driver/postgres"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

func New(dsn string) (*gorm.DB, error) {
    newLogger := logger.New(
        log.New(os.Stdout, "\r\n", log.LstdFlags),
        logger.Config{
            SlowThreshold:             200 * time.Millisecond,
            LogLevel:                  logger.Warn,
            IgnoreRecordNotFoundError: true,
            Colorful:                  false,
        },
    )

    db, err := gorm.Open(postgres.New(postgres.Config{
        DSN:                  dsn,
        PreferSimpleProtocol: true, // отключает prepared statements — нужно для pgBouncer
    }), &gorm.Config{
        Logger:                 newLogger,
        NowFunc:                func() time.Time { return time.Now().UTC() },
        PrepareStmt:            false, // согласовано с PreferSimpleProtocol
        DisableForeignKeyConstraintWhenMigrating: false,
    })
    if err != nil {
        return nil, fmt.Errorf("gorm.Open: %w", err)
    }

    sqlDB, err := db.DB()
    if err != nil {
        return nil, fmt.Errorf("db.DB(): %w", err)
    }

    sqlDB.SetMaxOpenConns(25)
    sqlDB.SetMaxIdleConns(10)
    sqlDB.SetConnMaxLifetime(5 * time.Minute)
    sqlDB.SetConnMaxIdleTime(2 * time.Minute)

    return db, nil
}

PreferSimpleProtocol: true и PrepareStmt: false — обязательны при работе через pgBouncer в transaction pooling mode. С prepared statements pgBouncer будет выдавать ошибки.

Модели

// internal/models/product.go
package models

import (
    "time"
    "gorm.io/gorm"
)

type ProductStatus string

const (
    StatusDraft     ProductStatus = "draft"
    StatusPublished ProductStatus = "published"
    StatusArchived  ProductStatus = "archived"
)

type Product struct {
    ID         uint           `gorm:"primarykey"`
    CreatedAt  time.Time
    UpdatedAt  time.Time
    DeletedAt  gorm.DeletedAt `gorm:"index"`   // soft delete

    Title      string        `gorm:"type:varchar(500);not null"`
    Slug       string        `gorm:"type:varchar(520);uniqueIndex;not null"`
    Price      float64       `gorm:"type:decimal(12,2);not null"`
    Status     ProductStatus `gorm:"type:varchar(20);default:draft;not null"`
    CategoryID uint          `gorm:"not null;index"`
    Category   Category      `gorm:"foreignKey:CategoryID;constraint:OnDelete:RESTRICT"`
    Tags       []Tag         `gorm:"many2many:product_tags;"`
    Images     []ProductImage `gorm:"foreignKey:ProductID;constraint:OnDelete:CASCADE"`
}

func (Product) TableName() string {
    return "products"
}

GORM по умолчанию ищет таблицу products для Product (pluralize + snake_case). Явный TableName() убирает неоднозначность.

AutoMigrate vs SQL-миграции

AutoMigrate подходит только для разработки:

err := db.AutoMigrate(&models.User{}, &models.Product{}, &models.Category{})

Для production используем golang-migrate:

go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
migrate create -ext sql -dir migrations -seq create_products
-- migrations/000002_create_products.up.sql
CREATE TABLE products (
    id          BIGSERIAL PRIMARY KEY,
    title       VARCHAR(500)   NOT NULL,
    slug        VARCHAR(520)   NOT NULL UNIQUE,
    price       DECIMAL(12, 2) NOT NULL,
    status      VARCHAR(20)    NOT NULL DEFAULT 'draft',
    category_id BIGINT         NOT NULL REFERENCES categories(id) ON DELETE RESTRICT,
    created_at  TIMESTAMPTZ    NOT NULL DEFAULT NOW(),
    updated_at  TIMESTAMPTZ    NOT NULL DEFAULT NOW(),
    deleted_at  TIMESTAMPTZ
);

CREATE INDEX idx_products_status_created ON products (status, created_at DESC);
CREATE INDEX idx_products_category       ON products (category_id, status);
CREATE INDEX idx_products_deleted_at     ON products (deleted_at);

Запросы

// internal/repository/product_repo.go
package repository

import (
    "context"
    "myapp/internal/models"
    "gorm.io/gorm"
)

type ProductRepository struct {
    db *gorm.DB
}

func NewProductRepository(db *gorm.DB) *ProductRepository {
    return &ProductRepository{db: db}
}

func (r *ProductRepository) GetPublished(
    ctx context.Context,
    categoryID uint,
    limit, offset int,
) ([]models.Product, error) {
    var products []models.Product

    err := r.db.WithContext(ctx).
        Preload("Category").
        Preload("Tags").
        Where("category_id = ? AND status = ?", categoryID, models.StatusPublished).
        Order("created_at DESC").
        Limit(limit).
        Offset(offset).
        Find(&products).Error

    return products, err
}

func (r *ProductRepository) GetBySlug(ctx context.Context, slug string) (*models.Product, error) {
    var product models.Product

    err := r.db.WithContext(ctx).
        Preload("Category").
        Preload("Tags").
        Preload("Images", func(db *gorm.DB) *gorm.DB {
            return db.Order("sort_order ASC")
        }).
        Where("slug = ?", slug).
        First(&product).Error

    if err != nil {
        return nil, err
    }
    return &product, nil
}

Preload выполняет отдельный SELECT с IN (...) — это не JOIN, но и не N+1. Для сложных join-запросов лучше использовать raw SQL через db.Raw().

Транзакции

func (r *ProductRepository) CreateWithTags(
    ctx context.Context,
    product *models.Product,
    tagIDs []uint,
) error {
    return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
        if err := tx.Create(product).Error; err != nil {
            return err
        }

        var tags []models.Tag
        if err := tx.Find(&tags, tagIDs).Error; err != nil {
            return err
        }

        return tx.Model(product).Association("Tags").Append(tags)
    })
}

Хуки

func (p *Product) BeforeCreate(tx *gorm.DB) error {
    if p.Slug == "" {
        p.Slug = slug.Make(p.Title)
    }
    return nil
}

func (p *Product) BeforeUpdate(tx *gorm.DB) error {
    tx.Statement.SetColumn("UpdatedAt", time.Now().UTC())
    return nil
}

Сроки

Начальная настройка GORM с golang-migrate для нового Go-проекта: 1 день. Включает подключение к БД, модели, репозитории, миграции и базовые тесты с testcontainers-go.