Skip to Content
DocumentationFeaturesRepository Pattern

Repository Pattern

The Repository Pattern in Goofer ORM provides a type-safe, intuitive API for database operations. It abstracts away the complexities of SQL and database interactions, allowing you to work with your entities in a more object-oriented way.

Overview

The Repository Pattern offers the following capabilities:

  • Generic Repository[T] for each entity type
  • CRUD operations (Create, Read, Update, Delete)
  • Fluent query building
  • Filtering, sorting, and pagination
  • Transaction support

Repository Interface

Goofer ORM implements the Repository Pattern using Go’s generics:

// Repository provides type-safe database operations type Repository[T schema.Entity] struct { db *sql.DB dialect Dialect metadata *schema.EntityMetadata ctx context.Context }

The generic type parameter T ensures that the repository is type-safe and can only be used with the specified entity type.

Creating a Repository

To create a repository for an entity, use the NewRepository function:

// Create a repository for the User entity userRepo := repository.NewRepository[User](db, sqliteDialect)

This creates a repository that is specifically typed for the User entity, providing type-safe operations.

Basic CRUD Operations

Create

To create a new entity, use the Save method:

// Create a new user user := &User{ Name: "John Doe", Email: "john@example.com", } // Save the user if err := userRepo.Save(user); err != nil { log.Fatalf("Failed to save user: %v", err) } fmt.Printf("Created user with ID: %d\n", user.ID)

The Save method will insert a new record if the entity’s primary key is zero, or update an existing record if the primary key has a value.

Read

To read entities from the database, use the Find, FindByID, or query builder methods:

// Find by ID user, err := userRepo.FindByID(1) if err != nil { log.Fatalf("Failed to find user: %v", err) } // Find with conditions users, err := userRepo.Find(). Where("name LIKE ?", "%John%"). OrderBy("name ASC"). Limit(10). All() if err != nil { log.Fatalf("Failed to find users: %v", err) } // Find one with conditions user, err := userRepo.Find(). Where("email = ?", "john@example.com"). One() if err != nil { log.Fatalf("Failed to find user: %v", err) } // Count entities count, err := userRepo.Find(). Where("name LIKE ?", "%John%"). Count() if err != nil { log.Fatalf("Failed to count users: %v", err) }

Update

To update an entity, modify its properties and use the Save method:

// Find the user user, err := userRepo.FindByID(1) if err != nil { log.Fatalf("Failed to find user: %v", err) } // Update properties user.Name = "Jane Doe" user.Email = "jane@example.com" // Save the changes if err := userRepo.Save(user); err != nil { log.Fatalf("Failed to update user: %v", err) }

Delete

To delete an entity, use the Delete or DeleteByID method:

// Delete by entity if err := userRepo.Delete(user); err != nil { log.Fatalf("Failed to delete user: %v", err) } // Delete by ID if err := userRepo.DeleteByID(1); err != nil { log.Fatalf("Failed to delete user: %v", err) }

Query Builder

The Repository Pattern includes a fluent query builder that allows you to construct complex queries:

// Create a query builder query := userRepo.Find(). Where("name LIKE ?", "%John%"). Where("created_at > ?", time.Now().AddDate(0, -1, 0)). OrderBy("name ASC"). Limit(10). Offset(20) // Execute the query users, err := query.All() if err != nil { log.Fatalf("Failed to find users: %v", err) }

Where Conditions

You can add multiple Where conditions to filter your query:

query := userRepo.Find(). Where("name LIKE ?", "%John%"). Where("email LIKE ?", "%@example.com"). Where("created_at > ?", time.Now().AddDate(0, -1, 0))

Ordering

You can order the results using the OrderBy method:

query := userRepo.Find(). OrderBy("name ASC"). OrderBy("created_at DESC")

Pagination

You can paginate the results using the Limit and Offset methods:

// Get the first page (10 items per page) page1, err := userRepo.Find(). OrderBy("name ASC"). Limit(10). Offset(0). All() // Get the second page page2, err := userRepo.Find(). OrderBy("name ASC"). Limit(10). Offset(10). All()

Counting

You can count the number of entities that match your query:

count, err := userRepo.Find(). Where("name LIKE ?", "%John%"). Count()

Transactions

The Repository Pattern supports transactions to ensure data integrity:

// Start a transaction err := userRepo.Transaction(func(txRepo *repository.Repository[User]) error { // Create a new user user := &User{ Name: "John Doe", Email: "john@example.com", } // Save the user in the transaction if err := txRepo.Save(user); err != nil { return err } // Create a profile for the user profile := &Profile{ UserID: user.ID, Bio: "Software developer", } // Save the profile in the transaction profileRepo := repository.NewRepository[Profile](txRepo.DB(), txRepo.Dialect()) if err := profileRepo.Save(profile); err != nil { return err } // If we return nil, the transaction will be committed return nil }) if err != nil { log.Fatalf("Transaction failed: %v", err) }

If the function returns an error, the transaction will be automatically rolled back. If it returns nil, the transaction will be committed.

Context Support

The Repository Pattern supports context for cancellation and timeouts:

// Create a context with a timeout ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // Use the context with the repository userRepoWithCtx := userRepo.WithContext(ctx) // Execute a query with the context users, err := userRepoWithCtx.Find(). Where("name LIKE ?", "%John%"). All()

Hooks

The Repository Pattern integrates with the Hooks system to allow you to execute code at specific points in an entity’s lifecycle:

// User entity with hooks type User struct { ID uint `orm:"primaryKey;autoIncrement"` Name string `orm:"type:varchar(255);notnull"` Email string `orm:"unique;type:varchar(255);notnull"` CreatedAt time.Time `orm:"type:timestamp;default:CURRENT_TIMESTAMP"` UpdatedAt time.Time `orm:"type:timestamp"` } // BeforeSave is called before the entity is saved func (u *User) BeforeSave() error { u.UpdatedAt = time.Now() return nil } // AfterCreate is called after the entity is created func (u *User) AfterCreate() error { fmt.Printf("User created: %s\n", u.Name) return nil }

Best Practices

Repository Creation

Create repositories at application startup and reuse them:

// Create repositories at startup userRepo := repository.NewRepository[User](db, dialect) profileRepo := repository.NewRepository[Profile](db, dialect) postRepo := repository.NewRepository[Post](db, dialect) // Use them throughout your application

Error Handling

Always check errors returned by repository methods:

user, err := userRepo.FindByID(1) if err != nil { if err == sql.ErrNoRows { // Handle not found case return nil, fmt.Errorf("user not found: %w", err) } // Handle other errors return nil, fmt.Errorf("failed to find user: %w", err) }

Transactions

Use transactions for operations that need to be atomic:

err := userRepo.Transaction(func(txRepo *repository.Repository[User]) error { // Multiple operations that need to succeed or fail together return nil })

Query Optimization

Be mindful of the queries you’re generating:

  • Use appropriate indexes on your database tables
  • Limit the number of rows returned when possible
  • Use Count() instead of loading all entities when you only need the count
  • Consider the N+1 query problem when working with relationships

Next Steps

  • Learn about Hooks to understand how to add lifecycle events to your entities
  • Explore Validation to see how to validate your entities before saving them
  • Check out Transactions for more details on transaction support
  • See the Examples section for more examples of using the Repository Pattern
Last updated on