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
FilterBuilderpara montar filtros BSON de forma fluente (sem a verbosidade debson.Mdireto). - Um
ProjectBuilderpara projeção de campos (equivalente aSELECT/projection).
O Monger não substitui o driver oficial: ele organiza e reduz boilerplate para casos comuns.
- Go 1.18+ (para generics). O
go.moddo projeto está em Go1.25.0. - MongoDB acessível e o driver oficial do MongoDB (vem como dependência transitiva).
go get github.com/zdekdev/mongerImport:
import "github.com/zdekdev/monger"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))
}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.
Crie filtros com monger.Filter() e encadeie 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:
valsdeve ser algo que o driver aceite para$in(ex.:[]string,[]int, etc).
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() retorna um monger.M (alias de bson.M) pronto para uso no driver.
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] encapsula uma *mongo.Collection e expõe métodos comuns.
users := monger.New[User](db, "users")Insere um documento e retorna o _id em formato hex string (ObjectID):
id, err := users.InsertOne(ctx, &User{Name: "João"})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"))Busca múltiplos documentos com filtro/projeção:
list, err := users.Find(ctx,
monger.Filter().Eq("active", true),
monger.Exclude("passwordHash"),
)Atalho para retornar todos os documentos:
list, err := users.FindAll(ctx)Conta documentos que satisfazem um filtro:
total, err := users.Count(ctx, monger.Filter().Eq("active", true))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"))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))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.
Remove um documento pelo _id:
err := users.DeleteByID(ctx, id)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.
-
Escolha o
commonValue(o valor que vai servir de chave). Ex.:"12345678900". -
Para cada coleção, diga qual campo guarda esse valor.
-
Use
alias(recomendado) para evitar colisão de campos e deixar o resultado organizado.
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çãofield: 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).
- Se não vazio: o documento daquela coleção fica aninhado em
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.JoinResulte os dados ficam emresult.Data(ummonger.M, alias debson.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": "...", ... }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
aliasdefinido: 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.
Usa o operador $lookup do MongoDB para fazer o join diretamente no servidor. Mais eficiente para grandes volumes de dados.
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 olocalFieldda 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,
$lookupsempre retorna um array no campoAs(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$matchacontece nela). - Em
LookupConfig,From,ForeignFieldeAssão obrigatórios. - O Monger remove automaticamente o
ForeignFielddo resultado do$lookup(evita repetir a chave).
Para usar JoinWithLookup, você pode precisar acessar a coleção MongoDB diretamente:
coll := usersRepo.Collection()| 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 |
- Use
context.Contextcom timeout/cancelamento (principalmente em produção). - Para ordenação, prefira
monger.D(alias debson.D) pois preserva ordem dos campos. - Para filtros complexos com operadores que não estão no builder (ex.:
$regex,$elemMatch), você pode misturar commonger.Mdiretamente viaBuild()ou criar umbson.Mmanual.
Veja LICENSE.