Generics Guide
Generics Guide
Section titled “Generics Guide”Pie is built on Go generics, providing a type-safe MongoDB operation experience. This guide covers generics usage, benefits, and best practices in Pie.
What are Generics?
Section titled “What are Generics?”Generics are a feature introduced in Go 1.18 that allows writing reusable code while maintaining type safety. In Pie, generics ensure compile-time type checking, preventing runtime type errors.
Basic Usage
Section titled “Basic Usage”Session[T] - The Most Common Generic Type
Section titled “Session[T] - The Most Common Generic Type”Session[T] is the core generic type in Pie, providing type-safe database operations:
// Create type-safe sessionsession := pie.Table[User](engine)
// Query operations return []User, not []anyusers, err := session.Find(context.Background())
// Insert operations accept *User typeuser := &User{Name: "John Doe", Email: "john@example.com"}result, err := session.Insert(context.Background(), user)
// Type-safe update operationsupdateResult, err := session. Where("email", "john@example.com"). Update(context.Background(), bson.D{{"$set", bson.D{{"age", 25}}}})Aggregate[T] - Generic Support for Aggregation
Section titled “Aggregate[T] - Generic Support for Aggregation”// Create type-safe aggregation builderaggregate := pie.NewAggregate[User](engine)
// Aggregation results return []Uservar results []Usererr := aggregate. MatchStage().Where("status", "active").Done(). GroupStage().By("department", "$department").Count("total").Done(). Execute(context.Background(), &results)Cursor[T] - Generic Support for Cursors
Section titled “Cursor[T] - Generic Support for Cursors”// Create cursorcursor := pie.NewCursor[User](context.Background(), mongoCursor)
// Iteration provides type-safe User objectsfor cursor.Next(context.Background()) { var user User if err := cursor.Decode(&user); err != nil { // Handle error } // user is User type, not any}Generic Functions
Section titled “Generic Functions”Pie provides several generic functions for creating type-safe operations:
// Basic functionssession := pie.Table[User](engine) // Create sessionaggregate := pie.NewAggregate[User](engine) // Create aggregation
// Must versions (panic on failure)session := pie.MustTable[User](engine) // Must create sessionaggregate := pie.MustAggregate[User](engine) // Must create aggregation
// Using default enginesession, err := pie.TableWithDefault[User]() // Create session with default enginesession := pie.MustTableWithDefault[User]() // Must create session with default engineAdvanced Topics
Section titled “Advanced Topics”Benefits of Type Safety
Section titled “Benefits of Type Safety”Generics provide compile-time type checking, preventing runtime type errors:
// ❌ Traditional approach - runtime errors possiblevar users []anyerr := session.Find(context.Background(), &users)// Manual type assertion requiredfor _, u := range users { user := u.(User) // May panic}
// ✅ Generic approach - compile-time type safetyvar users []Usererr := session.Find(context.Background(), &users)// Direct usage, no type assertion neededfor _, user := range users { fmt.Println(user.Name) // Type safe}Type Inference and Explicit Type Declaration
Section titled “Type Inference and Explicit Type Declaration”Go’s type inference makes generics usage more concise:
// Type inferencesession := pie.Table[User](engine) // T is inferred as User
// Explicit type declaration (usually not needed)var session *pie.Session[User] = pie.Table[User](engine)Generics with Interfaces
Section titled “Generics with Interfaces”// Define common interfacetype Model interface { GetID() string GetCreatedAt() time.Time}
// Use constraintsfunc ProcessModel[T Model](session *pie.Session[T], ctx context.Context) error { models, err := session.Find(ctx) if err != nil { return err }
for _, model := range models { fmt.Printf("ID: %s, Created: %v\n", model.GetID(), model.GetCreatedAt()) } return nil}Generic Return Types
Section titled “Generic Return Types”Pie provides various generic return types:
// Result[T] - Generic result typefunc GetUser[T any](session *pie.Session[T], id string) *pie.Result[*T] { user, err := session.FindByID(context.Background(), id) return &pie.Result[*T]{ Data: user, Error: err, }}
// PaginationResult[T] - Pagination resultfunc GetUsersWithPagination[T any](session *pie.Session[T], page, size int) *pie.PaginationResult[T] { var users []T total, err := session.Paginate(context.Background(), page, size, &users) return &pie.PaginationResult[T]{ Data: users, Total: total, Page: int64(page), Size: int64(size), Error: err, }}
// AggregateResult[T] - Aggregation resultfunc GetUserStats[T any](aggregate *pie.Aggregate[T]) *pie.AggregateResult[T] { var results []T err := aggregate.Execute(context.Background(), &results) return &pie.AggregateResult[T]{ Data: results, Error: err, }}Generics in Transactions
Section titled “Generics in Transactions”// TransactionWithResult[T] - Transaction with resultfunc TransferMoney[T any](tx *pie.Transaction, fromID, toID string, amount float64) *pie.TransactionResult[T] { return pie.TransactionWithResult[T](tx, context.Background(), func(ctx context.Context) (T, error) { // Execute operations within transaction fromSession := pie.Table[Account](tx.engine) toSession := pie.Table[Account](tx.engine)
// Deduct from sender's balance err := fromSession.Where("id", fromID).Update(ctx, bson.D{{"$inc", bson.D{{"balance", -amount}}}}) if err != nil { return zero, err }
// Add to receiver's balance err = toSession.Where("id", toID).Update(ctx, bson.D{{"$inc", bson.D{{"balance", amount}}}}) if err != nil { return zero, err }
return zero, nil })}Generics in Change Streams
Section titled “Generics in Change Streams”// ChangeStreamWatcher[T] - Change stream watcherfunc WatchUserChanges[T any](engine *pie.Engine) { watcher := pie.NewWatcher[T](engine)
watcher.OnChange(func(event *pie.ChangeEvent[T]) { switch event.OperationType { case "insert": fmt.Printf("New user created: %+v\n", event.FullDocument) case "update": fmt.Printf("User updated: %+v\n", event.FullDocument) case "delete": fmt.Printf("User deleted: %+v\n", event.DocumentKey) } })
watcher.Start(context.Background())}Best Practices
Section titled “Best Practices”When to Use Generics vs any
Section titled “When to Use Generics vs any”// ✅ Recommended: Use generics for type safetyfunc ProcessUsers[T any](session *pie.Session[T], ctx context.Context) ([]T, error) { users, err := session.Find(ctx) return users, err}
// ❌ Not recommended: Use any, loses type safetyfunc ProcessUsersGeneric(session *pie.Session[any], ctx context.Context) ([]any, error) { users, err := session.Find(ctx) return users, err}Best Practices for Model Definition
Section titled “Best Practices for Model Definition”// ✅ Recommended: Define clear modelstype User struct { ID bson.ObjectID `bson:"_id,omitempty"` Name string `bson:"name"` Email string `bson:"email"` Age int `bson:"age"` CreatedAt time.Time `bson:"created_at"` UpdatedAt time.Time `bson:"updated_at"`}
// ✅ Recommended: Implement interface methodsfunc (u *User) GetID() string { return u.ID.Hex()}
func (u *User) GetCreatedAt() time.Time { return u.CreatedAt}Techniques to Avoid Type Conversion
Section titled “Techniques to Avoid Type Conversion”// ✅ Recommended: Use generics to avoid type conversionfunc GetUserByEmail[T any](session *pie.Session[T], email string) (*T, error) { var user T err := session.Where("email", email).First(context.Background(), &user) if err != nil { return nil, err } return &user, nil}
// ❌ Not recommended: Requires type conversionfunc GetUserByEmailGeneric(session *pie.Session[any], email string) (any, error) { var user any err := session.Where("email", email).First(context.Background(), &user) if err != nil { return nil, err } // Type assertion required return user.(User), nil}Performance Considerations
Section titled “Performance Considerations”Generics in Go provide zero-cost abstraction, with compiled code performing the same as hand-written code:
// Generic codefunc FindByID[T any](session *pie.Session[T], id string) (*T, error) { var result T err := session.FindByID(context.Background(), id, &result) return &result, err}
// Compiles to equivalent non-generic hand-written codefunc FindUserByID(session *pie.Session[User], id string) (*User, error) { var result User err := session.FindByID(context.Background(), id, &result) return &result, err}Common Issues
Section titled “Common Issues”Handling Generic Type Mismatch Errors
Section titled “Handling Generic Type Mismatch Errors”// Issue: Type mismatchvar users []Usererr := session.Find(context.Background(), &users) // ✅ Correct
// ❌ Error: Type mismatchvar users []stringerr := session.Find(context.Background(), &users) // Compile error
// Solution: Ensure type consistencyvar users []Usererr := session.Find(context.Background(), &users)Converting Between Different Generic Types
Section titled “Converting Between Different Generic Types”// If conversion is needed, use intermediate variablesfunc ConvertUsersToDTOs(users []User) []UserDTO { var dtos []UserDTO for _, user := range users { dtos = append(dtos, UserDTO{ ID: user.ID.Hex(), Name: user.Name, Email: user.Email, }) } return dtos}
// Or use generic conversion functionfunc ConvertSlice[T, U any](slice []T, converter func(T) U) []U { result := make([]U, len(slice)) for i, item := range slice { result[i] = converter(item) } return result}Generics with BSON Serialization
Section titled “Generics with BSON Serialization”// BSON tags work with genericstype User struct { ID bson.ObjectID `bson:"_id,omitempty"` Name string `bson:"name"` Email string `bson:"email"` Age int `bson:"age"` CreatedAt time.Time `bson:"created_at"` UpdatedAt time.Time `bson:"updated_at"`}
// Generics ensure type safety, BSON tags handle serializationsession := pie.Table[User](engine)user := &User{Name: "John Doe", Email: "john@example.com"}result, err := session.Insert(context.Background(), user)Common Compilation Errors and Solutions
Section titled “Common Compilation Errors and Solutions”// Error 1: Missing type parameter// ❌ Compilation errorsession := pie.Table(engine)
// ✅ Correctsession := pie.Table[User](engine)
// Error 2: Type mismatch// ❌ Compilation errorvar users []stringerr := session.Find(context.Background(), &users)
// ✅ Correctvar users []Usererr := session.Find(context.Background(), &users)
// Error 3: Generic constraint not satisfied// ❌ If User doesn't implement Model interfacefunc ProcessModel[T Model](session *pie.Session[T], ctx context.Context) error { // ...}
// ✅ Ensure User implements Model interfacetype User struct { // ...}
func (u *User) GetID() string { return u.ID.Hex() }func (u *User) GetCreatedAt() time.Time { return u.CreatedAt }Summary
Section titled “Summary”Generics are a core feature of Pie, providing:
- Type Safety: Compile-time checking, preventing runtime errors
- Code Reuse: One set of code handles multiple types
- Performance: Zero-cost abstraction, no runtime overhead
- Developer Experience: Better IDE support and code completion
By using generics properly, you can write safer, more efficient, and more maintainable MongoDB operation code.
Next Steps
Section titled “Next Steps”- Query Builder - Learn about query functionality
- Aggregation - Learn about data aggregation
- Transactions - Master transaction operations
- Cache Support - Improve query performance