- Updated the ListPaginationOptions struct to include SkipCount and IncludeCount fields, allowing for more flexible pagination behavior. - Modified the BuildListPagination function to handle cursor pagination with count options, improving performance by running count queries in parallel with data retrieval. - Enhanced the FindAll method in the repository to support concurrent counting of documents, reducing overall latency for list operations. - Added tests for pagination behavior, ensuring accurate handling of count scenarios in both offset and cursor pagination. This update improves the efficiency and flexibility of pagination in the MongoDB repository, enhancing the overall performance of list operations.
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)
Cursor-based Pagination (Recommended for Performance)
// 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
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
License
This project is licensed under the MIT License - see the LICENSE file for details.