479 lines
11 KiB
Markdown
479 lines
11 KiB
Markdown
# 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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
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)
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
type YourModel struct {
|
|
Model
|
|
// Your fields...
|
|
}
|
|
|
|
// Model already implements Timestampable
|
|
// Timestamps are automatically set on Create/Update operations
|
|
```
|
|
|
|
### Custom ID Management
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// Use FindMany with limit for large result sets
|
|
users, err := userRepo.FindMany(context.Background(), filter, 1000)
|
|
```
|
|
|
|
### 4. Connection Pooling
|
|
|
|
Configure appropriate connection pool settings:
|
|
|
|
```go
|
|
config := mongo.ConnectionConfig{
|
|
MaxPoolSize: 100,
|
|
MinPoolSize: 5,
|
|
MaxConnIdleTime: 30 * time.Minute,
|
|
}
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
The ORM provides specific error types:
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// 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:
|
|
|
|
```go
|
|
// 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. |