Files
tm_back/pkg/mongo

MongoDB ORM for Go

A production-grade, high-performance generic ORM for MongoDB in Go, using the official MongoDB driver. This ORM provides a clean, type-safe interface for MongoDB operations with support for pagination, transactions, and advanced querying.

Features

Generic Repository Pattern - Type-safe operations for any struct
Dual Pagination Support - Both offset-based and cursor-based pagination
Comprehensive CRUD Operations - Create, Read, Update, Delete with bulk operations
Advanced Querying - Fluent query builder with complex conditions
Aggregation Support - Full MongoDB aggregation pipeline support
Connection Management - Connection pooling and lifecycle management
Index Management - Easy index creation and management
Transaction Support - ACID transactions for multi-document operations
Structured Logging - Comprehensive logging with structured fields
Performance Optimized - Efficient cursor-based pagination and connection pooling
Type Safety - Full Go generics support for compile-time type safety

Quick Start

1. Define Your Model

type User struct {
    Model
    Email     string `bson:"email" json:"email"`
    FirstName string `bson:"first_name" json:"first_name"`
    LastName  string `bson:"last_name" json:"last_name"`
    Age       int    `bson:"age" json:"age"`
    IsActive  bool   `bson:"is_active" json:"is_active"`
}

2. Set Up Connection

// Create logger
logger := &YourLogger{}

// Configure connection
config := mongo.ConnectionConfig{
    URI:      "mongodb://localhost:27017",
    Database: "your_database",
}

// Create connection manager
connManager, err := mongo.NewConnectionManager(config, logger)
if err != nil {
    log.Fatal(err)
}
defer connManager.Close(context.Background())

// Create repository
userRepo := mongo.NewRepository[User](connManager.GetCollection("users"), logger)

3. Basic Operations

// Create
user := &User{
    Email:     "john@example.com",
    FirstName: "John",
    LastName:  "Doe",
    Age:       30,
    IsActive:  true,
}
err := userRepo.Create(context.Background(), user)

// Find by ID
foundUser, err := userRepo.FindByID(context.Background(), user.ID)

// Update
user.FirstName = "Jane"
err = userRepo.Update(context.Background(), user)

// Delete
err = userRepo.Delete(context.Background(), user.ID)

Pagination

Offset-based Pagination

pagination := mongo.NewPaginationBuilder().
    Limit(10).
    Skip(20).
    SortDesc("created_at").
    Build()

results, err := userRepo.FindAll(context.Background(), bson.M{}, pagination)
// First page
pagination := mongo.NewPaginationBuilder().
    Limit(10).
    SortDesc("created_at").
    Build()

firstPage, err := userRepo.FindAll(context.Background(), bson.M{}, pagination)

// Next page using cursor
if firstPage.NextCursor != "" {
    nextPagination := mongo.NewPaginationBuilder().
        Limit(10).
        SortDesc("created_at").
        Cursor(firstPage.NextCursor).
        Build()
    
    nextPage, err := userRepo.FindAll(context.Background(), bson.M{}, nextPagination)
}

Advanced Querying

Using QueryBuilder

query := mongo.NewQueryBuilder().
    Where("is_active", true).
    WhereGreaterThan("age", 18).
    WhereIn("category", []interface{}{"admin", "user"}).
    WhereRegex("email", ".*@example\\.com", "i").
    Build()

results, err := userRepo.FindAll(context.Background(), query, pagination)

Complex Queries

// AND/OR conditions
query := mongo.NewQueryBuilder().
    WhereAnd(
        bson.M{"is_active": true},
        bson.M{"age": bson.M{"$gte": 18}},
    ).
    WhereOr(
        bson.M{"role": "admin"},
        bson.M{"role": "moderator"},
    ).
    Build()

Aggregation

pipeline := mongo.Pipeline{
    {{Key: "$match", Value: bson.M{"is_active": true}}},
    {{Key: "$group", Value: bson.M{
        "_id": "$role",
        "count": bson.M{"$sum": 1},
        "avgAge": bson.M{"$avg": "$age"},
    }}},
    {{Key: "$sort", Value: bson.M{"count": -1}}},
}

results, err := userRepo.Aggregate(context.Background(), pipeline)

Bulk Operations

// Bulk create
users := []User{
    {Email: "user1@example.com", FirstName: "User1"},
    {Email: "user2@example.com", FirstName: "User2"},
}
err := userRepo.BulkCreate(context.Background(), users)

// Bulk update
filter := bson.M{"is_active": false}
update := bson.M{"$set": bson.M{"status": "inactive"}}
err = userRepo.BulkUpdate(context.Background(), filter, update)

// Bulk delete
filter := bson.M{"created_at": bson.M{"$lt": time.Now().AddDate(0, -1, 0).Unix()}}
err = userRepo.BulkDelete(context.Background(), filter)

Index Management

Create Indexes

// Create common indexes
indexes := mongo.UserIndexes()
err := connManager.CreateIndexes("users", indexes)

// Create custom indexes
customIndexes := []mongo.Index{
    *mongo.CreateUniqueIndex("email_idx", bson.D{{Key: "email", Value: 1}}),
    *mongo.CreateTTLIndex("expires_at_idx", "expires_at", 3600),
    *mongo.CreateTextIndex("search_idx", "title", "description"),
}

err = connManager.CreateIndexes("documents", customIndexes)

Index Types

// Unique index
uniqueIdx := mongo.CreateUniqueIndex("email_idx", bson.D{{Key: "email", Value: 1}})

// TTL index
ttlIdx := mongo.CreateTTLIndex("expires_at_idx", "expires_at", 3600)

// Text index
textIdx := mongo.CreateTextIndex("search_idx", "title", "description")

// Compound index
compoundIdx := mongo.CreateCompoundIndex("user_status_idx", map[string]int{
    "user_id": 1,
    "status":  1,
})

// Geospatial index
geoIdx := mongo.CreateGeospatialIndex("location_idx", "location")

// Hashed index
hashIdx := mongo.CreateHashedIndex("user_id_hash_idx", "user_id")

Transactions

// Start session
session, err := connManager.client.StartSession()
if err != nil {
    log.Fatal(err)
}
defer session.EndSession(context.Background())

// Execute transaction
_, err = session.WithTransaction(context.Background(), func(sessCtx mongo.SessionContext) (interface{}, error) {
    // Create user
    user := &User{Email: "transaction@example.com"}
    if err := userRepo.Create(sessCtx, user); err != nil {
        return nil, err
    }
    
    // Create related document
    profile := &Profile{UserID: user.ID}
    if err := profileRepo.Create(sessCtx, profile); err != nil {
        return nil, err
    }
    
    return nil, nil
})

Connection Configuration

config := mongo.ConnectionConfig{
    URI:                    "mongodb://localhost:27017",
    Database:               "your_database",
    MaxPoolSize:            100,
    MinPoolSize:            5,
    MaxConnIdleTime:        30 * time.Minute,
    ConnectTimeout:         10 * time.Second,
    ServerSelectionTimeout: 5 * time.Second,
}

Logger Interface

Implement the Logger interface for structured logging:

type YourLogger struct{}

func (l *YourLogger) Info(message string, fields map[string]interface{}) {
    log.Printf("[INFO] %s: %+v", message, fields)
}

func (l *YourLogger) Error(message string, fields map[string]interface{}) {
    log.Printf("[ERROR] %s: %+v", message, fields)
}

func (l *YourLogger) Debug(message string, fields map[string]interface{}) {
    log.Printf("[DEBUG] %s: %+v", message, fields)
}

func (l *YourLogger) Warn(message string, fields map[string]interface{}) {
    log.Printf("[WARN] %s: %+v", message, fields)
}

Model Interfaces

Timestampable Interface

Implement for automatic timestamp management:

type YourModel struct {
    Model
    // Your fields...
}

// Model already implements Timestampable
// Timestamps are automatically set on Create/Update operations

Custom ID Management

type CustomModel struct {
    Model
    // Your fields...
}

// Model already implements IDGetter and IDSetter
// ID is automatically set after Create operations

Performance Best Practices

1. Use Cursor-based Pagination

Cursor-based pagination is more efficient than offset-based pagination for large datasets:

// Good - Cursor-based
pagination := mongo.NewPaginationBuilder().
    Limit(10).
    SortDesc("created_at").
    Build()

// Avoid - Offset-based for large datasets
pagination := mongo.NewPaginationBuilder().
    Limit(10).
    Skip(10000). // Expensive for large datasets
    Build()

2. Create Appropriate Indexes

// Create indexes for frequently queried fields
indexes := []mongo.Index{
    *mongo.CreateUniqueIndex("email_idx", bson.D{{Key: "email", Value: 1}}),
    *mongo.NewIndex("status_created_idx", bson.D{
        {Key: "status", Value: 1},
        {Key: "created_at", Value: -1},
    }),
}

3. Use Projection for Large Documents

// Use FindMany with limit for large result sets
users, err := userRepo.FindMany(context.Background(), filter, 1000)

4. Connection Pooling

Configure appropriate connection pool settings:

config := mongo.ConnectionConfig{
    MaxPoolSize:    100,
    MinPoolSize:    5,
    MaxConnIdleTime: 30 * time.Minute,
}

Error Handling

The ORM provides specific error types:

var (
    ErrDocumentNotFound = errors.New("document not found")
    ErrInvalidCursor    = errors.New("invalid cursor")
    ErrInvalidPagination = errors.New("invalid pagination parameters")
)

// Handle specific errors
user, err := userRepo.FindByID(ctx, id)
if err != nil {
    if errors.Is(err, mongo.ErrDocumentNotFound) {
        // Handle not found
        return
    }
    // Handle other errors
}

Testing

Unit Testing with Mocks

// Create mock repository for testing
type MockUserRepository struct {
    users map[string]*User
}

func (m *MockUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
    if user, exists := m.users[id]; exists {
        return user, nil
    }
    return nil, mongo.ErrDocumentNotFound
}

// Use in tests
mockRepo := &MockUserRepository{users: make(map[string]*User)}

Integration Testing

func TestUserRepository(t *testing.T) {
    // Set up test database
    config := mongo.ConnectionConfig{
        URI:      "mongodb://localhost:27017",
        Database: "test_database",
    }
    
    connManager, err := mongo.NewConnectionManager(config, logger)
    require.NoError(t, err)
    defer connManager.Close(context.Background())
    
    userRepo := mongo.NewRepository[User](connManager.GetCollection("users"), logger)
    
    // Run tests...
}

Migration from Existing Code

From Manual MongoDB Operations

// Before
collection := client.Database("db").Collection("users")
filter := bson.M{"email": email}
var user User
err := collection.FindOne(ctx, filter).Decode(&user)

// After
userRepo := mongo.NewRepository[User](collection, logger)
user, err := userRepo.FindOne(ctx, bson.M{"email": email})

From Other ORMs

The generic repository pattern makes migration straightforward:

// Replace existing repository interface
type UserRepository interface {
    FindByID(ctx context.Context, id string) (*User, error)
    Create(ctx context.Context, user *User) error
    // ... other methods
}

// Implementation remains the same, just use mongo.NewRepository

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. Submit a pull request

License

This project is licensed under the MIT License - see the LICENSE file for details.