Skip to content
/ monger Public

Monger é um wrapper leve (e genérico) em Go para facilitar operações comuns com MongoDB usando o driver oficial (go.mongodb.org/mongo-driver).

License

Notifications You must be signed in to change notification settings

zdekdev/monger

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

📦 Monger

Monger é um wrapper leve (e genérico) em Go para facilitar operações comuns com MongoDB usando o driver oficial (go.mongodb.org/mongo-driver).

Ele fornece:

  • Um Repository[T] genérico com operações CRUD e paginação.
  • Um FilterBuilder para montar filtros BSON de forma fluente (sem a verbosidade de bson.M direto).
  • Um ProjectBuilder para projeção de campos (equivalente a SELECT/projection).

O Monger não substitui o driver oficial: ele organiza e reduz boilerplate para casos comuns.


Requisitos

  • Go 1.18+ (para generics). O go.mod do projeto está em Go 1.25.0.
  • MongoDB acessível e o driver oficial do MongoDB (vem como dependência transitiva).

Instalação

go get github.com/zdekdev/monger

Import:

import "github.com/zdekdev/monger"

Quickstart

Exemplo completo com conexão, criação de repositório e operações básicas.

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/zdekdev/monger"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

type User struct {
	ID        primitive.ObjectID `bson:"_id,omitempty" json:"id"`
	Name      string             `bson:"name" json:"name"`
	Age       int                `bson:"age" json:"age"`
	Active    bool               `bson:"active" json:"active"`
	CreatedAt time.Time          `bson:"createdAt" json:"createdAt"`
}

func main() {
	ctx := context.Background()

	client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = client.Disconnect(ctx) }()

	db := client.Database("app")
	users := monger.New[User](db, "users")

	// Insert
	u := User{Name: "Ana", Age: 29, Active: true, CreatedAt: time.Now()}
	id, err := users.InsertOne(ctx, &u)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("inserted id:", id)

	// Find by id (com projeção opcional)
	found, err := users.FindByID(ctx, id, monger.Select("name", "age"))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("found: %+v\n", found)

	// Find com filtro
	list, err := users.Find(ctx,
		monger.Filter().
			Eq("active", true).
			Gte("age", 18),
		nil,
	)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("total active adults:", len(list))
}

Conceitos principais

Tipos utilitários (M e D)

O pacote expõe aliases para facilitar a construção de BSON quando necessário:

  • type M = bson.M (map)
  • type D = bson.D (slice ordenado)

Isso é útil principalmente para sort e para casos onde você quer usar diretamente operadores BSON do driver.


FilterBuilder (montagem de filtros)

Crie filtros com monger.Filter() e encadeie comparadores.

Comparadores

  • Eq(field, val) / Equal(field, val){field: val}
  • Ne(field, val) / NotEqual(field, val){field: {$ne: val}}
  • Gt(field, val) / GreaterThan(field, val){field: {$gt: val}}
  • Gte(field, val) / GreaterThanOrEqual(field, val){field: {$gte: val}}
  • Lt(field, val) / LessThan(field, val){field: {$lt: val}}
  • Lte(field, val) / LessThanOrEqual(field, val){field: {$lte: val}}
  • In(field, vals) / InValues(field, vals){field: {$in: vals}}

Observação: vals deve ser algo que o driver aceite para $in (ex.: []string, []int, etc).

Operadores lógicos (And / Or)

Você pode compor filtros:

f := monger.Filter().And(
	monger.Filter().Eq("active", true),
	monger.Filter().Or(
		monger.Filter().Gte("age", 18),
		monger.Filter().Eq("role", "admin"),
	),
)

Build

Build() retorna um monger.M (alias de bson.M) pronto para uso no driver.


ProjectBuilder (projeção de campos)

Use para controlar quais campos voltam do banco:

  • monger.Select("a", "b"){a: 1, b: 1}
  • monger.Exclude("a", "b"){a: 0, b: 0}

Exemplo:

u, err := users.FindByID(ctx, id, monger.Select("name", "age"))

Repository[T]

Repository[T] encapsula uma *mongo.Collection e expõe métodos comuns.

Criando um repositório

users := monger.New[User](db, "users")

InsertOne

Insere um documento e retorna o _id em formato hex string (ObjectID):

id, err := users.InsertOne(ctx, &User{Name: "João"})

FindByID

Busca um documento pelo _id (hex). Aceita projeção opcional:

u, err := users.FindByID(ctx, id, nil)
u2, err := users.FindByID(ctx, id, monger.Select("name"))

Find

Busca múltiplos documentos com filtro/projeção:

list, err := users.Find(ctx,
	monger.Filter().Eq("active", true),
	monger.Exclude("passwordHash"),
)

FindAll

Atalho para retornar todos os documentos:

list, err := users.FindAll(ctx)

Count

Conta documentos que satisfazem um filtro:

total, err := users.Count(ctx, monger.Filter().Eq("active", true))

Exists

Retorna true se existir ao menos um documento que satisfaça o filtro:

ok, err := users.Exists(ctx, monger.Filter().Eq("email", "a@b.com"))

FindPaged (paginação + sort)

Retorna PagedResult[T] com Data e Total (total de documentos do filtro, sem paginação).

res, err := users.FindPaged(
	ctx,
	monger.Filter().Eq("active", true),
	monger.Select("name", "createdAt"),
	0,  // skip
	10, // limit
	monger.D{{Key: "createdAt", Value: -1}}, // sort desc
)
if err != nil {
	// handle
}

fmt.Println("total:", res.Total)
fmt.Println("page size:", len(res.Data))

UpdateByID (update parcial)

Atualiza parcialmente o documento.

1) Via struct (padrão): usa $set apenas com campos não-zerados.

err := users.UpdateByID(ctx, id, &User{Name: "Novo Nome"})

No exemplo acima, somente o campo Name será atualizado; os demais campos do documento permanecem como estão.

2) Valores “zerados” (0, "", false): em Go não dá para distinguir “campo não informado” de “campo informado com zero” usando apenas um struct comum. Para manter o código enxuto e ainda permitir atualizar valores zerados, use um patch struct com campos ponteiro.

Exemplo:

type UserPatch struct {
	Name   *string `bson:"name"`
	Age    *int    `bson:"age"`
	Active *bool   `bson:"active"`
}

// Você pode declarar só os campos que pretende atualizar.
// Ex.: type UserPatch struct { Active *bool `bson:"active"` }

// atualiza explicitamente para false
err := users.UpdateByID(ctx, id, &UserPatch{Active: monger.Value(false)})

// atualiza explicitamente para 0
err = users.UpdateByID(ctx, id, &UserPatch{Age: monger.Value(0)})

// atualiza explicitamente para string vazia
err = users.UpdateByID(ctx, id, &UserPatch{Name: monger.Value("")})

Observação: o campo _id é ignorado caso seja enviado no update.

DeleteByID

Remove um documento pelo _id:

err := users.DeleteByID(ctx, id)

Join (União de Coleções)

O Monger oferece funções para “juntar” dados de múltiplas coleções usando um valor em comum (por exemplo: cpf, email, userId).

Importante: Join/JoinAll fazem buscas coleção por coleção (várias consultas). Para volumes grandes, ou quando você quer que o Mongo faça a união no servidor, prefira JoinWithLookup.

Receita rápida (como pensar)

  1. Escolha o commonValue (o valor que vai servir de chave). Ex.: "12345678900".

  2. Para cada coleção, diga qual campo guarda esse valor.

  3. Use alias (recomendado) para evitar colisão de campos e deixar o resultado organizado.

JoinCollection

Representa uma coleção a ser unida. Use NewJoinCollection para criar a partir de um Repository:

jc := monger.NewJoinCollection(usersRepo, "cpf", "user")

Parâmetros:

  • repo: o Repository da coleção
  • field: campo a ser usado na junção (ex: "cpf", "_id", "email")
  • alias: nome do campo no resultado.
    • Se não vazio: o documento daquela coleção fica aninhado em result.Data[alias].
    • Se vazio: os campos são mesclados no nível raiz do resultado (se houver chaves iguais, a última coleção pode sobrescrever valores).

Join

Busca um documento em cada coleção que contenha o valor comum e retorna um único documento mesclado:

// Exemplo: buscar dados de um usuário em múltiplas coleções pelo CPF
result, err := monger.Join(ctx, "12345678900",
    monger.NewJoinCollection(usersRepo, "cpf", "user"),
    monger.NewJoinCollection(addressRepo, "ownerCpf", "address"),
    monger.NewJoinCollection(profileRepo, "documentCpf", "profile"),
)
if err != nil {
	log.Fatal(err)
}

// result.Data contém:
// {
//   "user": { "name": "Ana", "cpf": "12345678900", ... },
//   "address": { "street": "Rua X", "ownerCpf": "12345678900", ... },
//   "profile": { "bio": "...", "documentCpf": "12345678900", ... }
// }
fmt.Printf("%+v\n", result.Data)

Comportamento importante:

  • Se uma coleção não tiver documento com o valor, ela é ignorada.
  • Se nenhuma coleção retornar dados, o erro é mongo.ErrNoDocuments.
  • O retorno é *monger.JoinResult e os dados ficam em result.Data (um monger.M, alias de bson.M).

Se o alias for vazio, os campos são mesclados diretamente no resultado:

result, err := monger.Join(ctx, "12345678900",
    monger.NewJoinCollection(usersRepo, "cpf", ""),      // sem alias
    monger.NewJoinCollection(profileRepo, "cpf", ""),   // sem alias
)
// result.Data: { "name": "Ana", "cpf": "12345678900", "bio": "...", ... }

JoinAll

Similar ao Join, mas retorna todos os documentos encontrados em cada coleção (útil para relações 1:N):

// Um usuário pode ter múltiplos pedidos
result, err := monger.JoinAll(ctx, "12345678900",
    monger.NewJoinCollection(usersRepo, "cpf", "user"),
    monger.NewJoinCollection(ordersRepo, "customerCpf", "orders"),
)
if err != nil {
    log.Fatal(err)
}

// result.Data:
// {
//   "user": { "name": "Ana", "cpf": "12345678900" },
//   "orders": [
//     { "orderId": "001", "customerCpf": "12345678900", "total": 100 },
//     { "orderId": "002", "customerCpf": "12345678900", "total": 250 }
//   ]
// }

Notas:

  • Com alias definido: se a coleção retornar 1 documento, vira objeto; se retornar >1, vira array.
  • Sem alias: o Monger mescla somente o primeiro documento encontrado daquela coleção no resultado.

JoinWithLookup (Agregação no Servidor)

Usa o operador $lookup do MongoDB para fazer o join diretamente no servidor. Mais eficiente para grandes volumes de dados.

monger.LookupConfig

Cada monger.LookupConfig vira um estágio $lookup no pipeline. Campos:

  • From: nome da coleção que será consultada (coleção “externa”).
  • ForeignField: campo na coleção externa que será comparado com o localField da coleção base.
  • As: nome do campo onde o MongoDB colocará o resultado do $lookup.

O Monger remove automaticamente do resultado do $lookup o campo usado como chave de relacionamento (ForeignField) para evitar repetir dados (ex.: não retorna customerCpf se você já tem cpf no documento base).

Observação: no MongoDB, $lookup sempre retorna um array no campo As (mesmo quando a relação é 1:1).

result, err := monger.JoinWithLookup(ctx,
	usersRepo.Collection(),  // coleção base (onde começa a agregação)
	"cpf",                   // localField: campo na coleção base
	"12345678900",           // localValue: valor a buscar na coleção base
	monger.LookupConfig{
		From:         "orders",       // coleção externa
		ForeignField: "customerCpf",  // campo na externa que referencia o CPF
		As:           "orders",       // nome do campo no resultado
	},
	monger.LookupConfig{
		From:         "addresses",
		ForeignField: "ownerCpf",
		As:           "address",
	},
)
if err != nil {
    log.Fatal(err)
}

// result.Data contém o documento base + campos do lookup:
// {
//   "_id": "...",
//   "cpf": "12345678900",
//   "name": "Ana",
//   "orders": [
//     { "orderId": "001", "total": 100 },
//     { "orderId": "002", "total": 250 }
//   ],
//   "address": [
//     { "street": "Rua X" }
//   ]
// }

fmt.Printf("%+v\n", result.Data)

Detalhes úteis:

  • A agregação começa na baseCollection (o primeiro $match acontece nela).
  • Em LookupConfig, From, ForeignField e As são obrigatórios.
  • O Monger remove automaticamente o ForeignField do resultado do $lookup (evita repetir a chave).

Collection (acessar coleção subjacente)

Para usar JoinWithLookup, você pode precisar acessar a coleção MongoDB diretamente:

coll := usersRepo.Collection()

Quando usar cada função?

Função Uso recomendado
Join Buscar um documento por coleção (relação 1:1)
JoinAll Buscar múltiplos documentos por coleção (relação 1:N)
JoinWithLookup Grandes volumes de dados, join feito no servidor MongoDB

Dicas de uso

  • Use context.Context com timeout/cancelamento (principalmente em produção).
  • Para ordenação, prefira monger.D (alias de bson.D) pois preserva ordem dos campos.
  • Para filtros complexos com operadores que não estão no builder (ex.: $regex, $elemMatch), você pode misturar com monger.M diretamente via Build() ou criar um bson.M manual.

Licença

Veja LICENSE.

About

Monger é um wrapper leve (e genérico) em Go para facilitar operações comuns com MongoDB usando o driver oficial (go.mongodb.org/mongo-driver).

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages