mobone

package module
v2.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 3, 2026 License: MIT Imports: 8 Imported by: 0

README

mobone — минималистичный слой для работы с PostgreSQL через pgx и Squirrel

mobone упрощает CRUD-операции и списочные выборки для моделей на Go, используя:

  • pgx/pgxpool как драйвер PostgreSQL
  • Masterminds/squirrel как билдер SQL с плейсхолдерами
  • Простые интерфейсы моделей для маппинга колонок и значений

Подходит для приложений, где нужно быстро собрать надежный слой доступа к данным с минимальным шаблонным кодом.

  • Мин. Требования: Go 1.24, pgx v5, squirrel.
  • Рекомендуется использовать формат плейсхолдеров Dollar для PostgreSQL: StatementBuilder.PlaceholderFormat(squirrel.Dollar)

Установка

go get github.com/rendau/mobone/v2

Быстрый старт

Инициализация хранилища
// Go
import (
  "context"

  "github.com/Masterminds/squirrel"
  "github.com/jackc/pgx/v5/pgxpool"
  "github.com/rendau/mobone/v2"
)

func NewStore(pool *pgxpool.Pool) mobone.ModelStore {
  qb := squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
  return mobone.ModelStore{
    Con:       pool,
    QB:        qb,
    TableName: "your_table",
  }
}
Интерфейсы моделей

mobone работает не с структурами напрямую, а с интерфейсами, которые возвращают:

  • карту колонок => адрес полей для Scan (ListModelI, GetModelI)
  • карту значений для INSERT/UPDATE (CreateModelI, UpdateModelI)
  • карту PK-условий (GetModelI, UpdateModelI, DeleteModelI)
  • опционально возвращаемые поля (CreateModelI.ReturningColumnMap)
  • опциональные перехватчики запросов (WithListInterceptorI, WithGetInterceptorI)

Ключи карт — имена колонок/выражений в SQL, значения — либо конкретные значения (для SetMap/Where), либо указатели на поля (для Scan).

Интерфейсы

  • TransactionManagerI

    • GetConnection(ctx) ConnectionI
    • TxFn(ctx, func(ctx) error) error — оборачивает функцию в транзакцию
  • ConnectionI

    • Exec(ctx, sql, args...) (pgconn.CommandTag, error)
    • Query(ctx, sql, args...) (pgx.Rows, error)
    • QueryRow(ctx, sql, args...) pgx.Row
  • ListModelI

    • ListColumnMap() map[string]any — колонки для Select/Scan
    • DefaultSortColumns() []string — сортировка по умолчанию
  • GetModelI

    • ListColumnMap()
    • PKColumnMap() map[string]any — условия по PK
  • CreateModelI

    • CreateColumnMap() map[string]any — значения для INSERT
    • ReturningColumnMap() map[string]any — поля для RETURNING
  • UpdateModelI

    • UpdateColumnMap() map[string]any — значения для UPDATE
    • PKColumnMap()
  • UpdateCreateModelI

    • объединяет UpdateModelI + CreateModelI (для upsert/insert-if-not-exists)
  • DeleteModelI

    • PKColumnMap()
  • WithListInterceptorI

    • ListInterceptor(qb, params) — тонкая настройка SELECT
  • WithGetInterceptorI

    • GetInterceptor(qb)

ModelStore: операции

  • Create(ctx, m CreateModelI) error
  • Update(ctx, m UpdateModelI) error
  • UpdateOrCreate(ctx, m UpdateCreateModelI) error — ON CONFLICT DO UPDATE
  • CreateIfNotExist(ctx, m UpdateCreateModelI) error — ON CONFLICT DO NOTHING
  • Delete(ctx, m DeleteModelI) error
  • Get(ctx, m GetModelI) (found bool, err error)
  • List(ctx, params ListParams, itemConstructor func(add bool) ListModelI) (totalCount int64, err error)

ListParams:

  • Conditions map[string]any — простые условия Where(map)
  • ConditionExpressions map[string][]any — выражения Where("a = ? and b > ?", args...)
  • Distinct bool
  • Columns []string — какие колонки вернуть (по умолчанию — все из ListColumnMap)
  • Page, PageSize int64 — пагинация (Offset = Page*PageSize)
  • WithTotalCount bool — вместе с данными вернуть count
  • OnlyCount bool — вернуть только count (без данных)
  • Sort []string — список ORDER BY (если пусто — берется DefaultSortColumns)
  • CustomConditions map[string]string — для ваших кастомизаций (используйте в перехватчиках)

Транзакции

TransactionManager прокидывает pgx.Tx через context, чтобы ModelStore автоматически использовал один и тот же ConnectionI (tx вместо пула) внутри TxFn.

// Go
txM := mobone.NewTransactionManager(pool)

store := mobone.ModelStore{
  Con:                pool,
  TransactionManager: txM,
  QB:                 squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar),
  TableName:          "your_table",
}

err := txM.TxFn(context.Background(), func(ctx context.Context) error {
  // все вызовы store внутри будут на одной транзакции
  // например: return store.Update(ctx, updateModel)
  return nil
})

Пример модели

// Go
type Contact struct {
  Phone string
  Email string
}

type ContactEdit struct {
  Phone *string
  Email *string
}

type Item struct {
  Id        int
  Name      string
  Flag      bool
  Contact   Contact
  CreatedAt time.Time
  UpdatedAt time.Time
}

// Для списков/получения
func (m *Item) ListColumnMap() map[string]any {
  return map[string]any{
    "id":         &m.Id,
    "name":       &m.Name,
    "flag":       &m.Flag,
    "contact":    &m.Contact,
    "created_at": &m.CreatedAt,
    "updated_at": &m.UpdatedAt,
  }
}

func (m *Item) PKColumnMap() map[string]any {
  return map[string]any{"id": m.Id}
}

func (m *Item) DefaultSortColumns() []string { return []string{"id"} }

// Для создания/обновления
type ItemUpsert struct {
  PKId      int
  Name      *string
  Flag      *bool
  Contact   *ContactEdit
  UpdatedAt *time.Time
}

func (u *ItemUpsert) CreateColumnMap() map[string]any {
  res := map[string]any{}
  if u.Name != nil { res["name"] = *u.Name }
  if u.Flag != nil { res["flag"] = *u.Flag }
  if u.Contact != nil { res["contact"] = u.Contact }
  if u.UpdatedAt != nil { res["updated_at"] = *u.UpdatedAt }
  return res
}

func (u *ItemUpsert) UpdateColumnMap() map[string]any {
  res := u.CreateColumnMap()
  // не обновляем PK-колонки
  delete(res, "id")
  // пример частичного merge jsonb (через squirrel.Expr)
  if v, ok := res["contact"]; ok {
    res["contact"] = squirrel.Expr("contact || ?", v)
  }
  return res
}

func (u *ItemUpsert) ReturningColumnMap() map[string]any {
  return map[string]any{"id": &u.PKId}
}

func (u *ItemUpsert) PKColumnMap() map[string]any {
  return map[string]any{"id": u.PKId}
}

Примеры операций

Create
// Go
store := mobone.ModelStore{Con: pool, QB: qb, TableName: "items"}

name := "Test"
flag := true
create := &ItemUpsert{
  Name: &name,
  Flag: &flag,
}
err := store.Create(ctx, create)
if err != nil { /* handle */ }
id := create.PKId
Get
// Go
item := &Item{Id: id}
found, err := store.Get(ctx, item)
if err != nil { /* handle */ }
if !found { /* not found */ }
// item заполнен из БД
Update
// Go
newName := "Updated"
now := time.Now()
upd := &ItemUpsert{
  PKId:      id,
  Name:      &newName,
  UpdatedAt: &now,
}
err := store.Update(ctx, upd)
Delete
// Go
err := store.Delete(ctx, &ItemUpsert{PKId: id})
List с пагинацией и сортировкой
// Go
var items []*Item
total, err := store.List(ctx, mobone.ListParams{
  Page:       0,
  PageSize:   20,
  Sort:       []string{"id desc"},
  WithTotalCount: true,
}, func(add bool) mobone.ListModelI {
  it := &Item{}
  if add { items = append(items, it) }
  return it
})
// items заполнен, total содержит количество (если WithTotalCount)
List с ограниченным набором колонок
// Go
var items []*Item
_, err := store.List(ctx, mobone.ListParams{
  Columns: []string{"id", "name"}, // берутся только разрешенные в ListColumnMap()
  PageSize: 50,
}, func(add bool) mobone.ListModelI {
  it := &Item{}
  if add { items = append(items, it) }
  return it
})
Только подсчет (без данных)
// Go
count, err := store.List(ctx, mobone.ListParams{
  OnlyCount: true,
}, func(add bool) mobone.ListModelI {
  return &Item{}
})
Условия
// Go
// Простой Where через карту (эквивалент field = value)
params := mobone.ListParams{
  Conditions: map[string]any{
    "flag": true,
  },
}

// Произвольные выражения с плейсхолдерами
params.ConditionExpressions = map[string][]any{
  "name ilike ?": {"%test%"},
}
Interceptor для списков
// Go
type ItemList struct{ Item }

func (m *ItemList) ListInterceptor(qb squirrel.SelectBuilder, params mobone.ListParams) squirrel.SelectBuilder {
  // например, форсируем join при определенных колонках
  if len(params.Columns) == 1 && params.Columns[0] == "id" {
    qb = qb.CrossJoin("(select 1) as s")
  }
  return qb
}

Upsert и Insert-if-not-exists

// Go
// ON CONFLICT (id) DO UPDATE SET ...
err := store.UpdateOrCreate(ctx, &ItemUpsert{
  PKId: id,
  Name: &newName,
})

// ON CONFLICT (id) DO NOTHING
err := store.CreateIfNotExist(ctx, &ItemUpsert{
  PKId: id,
  Name: &newName,
})

Утилиты сортировки

Пакет tools содержит функцию для безопасной сборки ORDER BY из whitelisted полей.

// Go
import "github.com/rendau/mobone/v2/tools"

allowed := map[string]string{
  "name": "user_name",
  "age":  "user_age",
}

order := tools.ConstructSortColumns(allowed, []string{"-name", "age"})
// -> []string{"user_name desc", "user_age"}

// Используйте в ListParams.Sort:
params.Sort = order

Особенности:

  • Игнорирует неразрешенные поля
  • Поддерживает префикс "-" для DESC
  • Возвращает nil, если итог пуст (удобно для проверки)

Рекомендации

  • Всегда используйте PlaceholderFormat(squirrel.Dollar) с PostgreSQL.
  • В ReturningColumnMap возвращайте указатели на поля вашей структуры.
  • В List используйте itemConstructor(add bool): добавляйте элемент в коллекцию только когда add == true (когда фактически прочитана строка).
  • Для JSONB-merge применяйте squirrel.Expr в UpdateColumnMap.

Обработка ошибок

mobone возвращает ошибки с оберткой контекста:

  • "fail to build query"
  • "fail to query"
  • "fail to exec"
  • "transaction function"
  • "transaction commit"

Используйте errors.Is для проверки pgx.ErrNoRows в Get.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ConnectionI added in v2.1.1

type ConnectionI interface {
	Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
	Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
	QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}

type CreateModelI

type CreateModelI interface {
	CreateColumnMap() map[string]any
	ReturningColumnMap() map[string]any
}

type DeleteModelI

type DeleteModelI interface {
	PKColumnMap() map[string]any
}

type GetModelI

type GetModelI interface {
	ListColumnMap() map[string]any
	PKColumnMap() map[string]any
}

type ListModelI

type ListModelI interface {
	ListColumnMap() map[string]any
	DefaultSortColumns() []string
}

type ListParams

type ListParams struct {
	Conditions           map[string]any
	ConditionExpressions map[string][]any
	Distinct             bool
	Columns              []string
	Page                 int64
	PageSize             int64
	WithTotalCount       bool
	OnlyCount            bool
	Sort                 []string
	CustomConditions     map[string]string
}

type ModelStore

type ModelStore struct {
	Con                *pgxpool.Pool
	TransactionManager connectionGetterI
	QB                 squirrel.StatementBuilderType
	TableName          string
}

func (*ModelStore) Create

func (s *ModelStore) Create(ctx context.Context, m CreateModelI) error

func (*ModelStore) CreateIfNotExist

func (s *ModelStore) CreateIfNotExist(ctx context.Context, m UpdateCreateModelI) error

func (*ModelStore) Delete

func (s *ModelStore) Delete(ctx context.Context, m DeleteModelI) error

func (*ModelStore) Get

func (s *ModelStore) Get(ctx context.Context, m GetModelI) (bool, error)

func (*ModelStore) GetConnection added in v2.1.1

func (s *ModelStore) GetConnection(ctx context.Context) ConnectionI

func (*ModelStore) List

func (s *ModelStore) List(ctx context.Context, params ListParams, itemConstructor func(add bool) ListModelI) (int64, error)

func (*ModelStore) Update

func (s *ModelStore) Update(ctx context.Context, m UpdateModelI) error

func (*ModelStore) UpdateOrCreate

func (s *ModelStore) UpdateOrCreate(ctx context.Context, m UpdateCreateModelI) error

type TransactionManager added in v2.1.1

type TransactionManager struct {
	// contains filtered or unexported fields
}

func NewTransactionManager added in v2.1.1

func NewTransactionManager(con *pgxpool.Pool) *TransactionManager

func (*TransactionManager) GetConnection added in v2.1.1

func (s *TransactionManager) GetConnection(ctx context.Context) ConnectionI

func (*TransactionManager) TxFn added in v2.1.1

type TransactionManagerI added in v2.1.1

type TransactionManagerI interface {
	GetConnection(ctx context.Context) ConnectionI
	TxFn(ctx context.Context, f func(context.Context) error) error
}

type UpdateCreateModelI

type UpdateCreateModelI interface {
	UpdateModelI
	CreateModelI
}

type UpdateModelI

type UpdateModelI interface {
	UpdateColumnMap() map[string]any
	PKColumnMap() map[string]any
}

type WithGetInterceptorI

type WithGetInterceptorI interface {
	GetInterceptor(qb squirrel.SelectBuilder) squirrel.SelectBuilder
}

type WithListInterceptorI

type WithListInterceptorI interface {
	ListInterceptor(qb squirrel.SelectBuilder, params ListParams) squirrel.SelectBuilder
}

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL