db.Repository¶
Repository pattern implementation with blueprint and goqu. The Repository provides a comprehensive interface-driven abstraction for database operations, composed of multiple specialized interfaces for different operation types.
Overview¶
The Repository interface is composed of multiple smaller interfaces, following the Interface Segregation Principle:
type Repository interface {
Identifier
Builder
Reader
Executor
Writer
Deleter
Updater
Counter
GridOps
SqlBuilder
NewTransaction(opts *sql.TxOptions) (Transaction, error)
}
Usage¶
package main
import (
"context"
"fmt"
"github.com/oddbit-project/blueprint/db"
"github.com/oddbit-project/blueprint/provider/pgsql"
"log"
"time"
)
type UserRecord struct {
Id int `db:"id_user" goqu:"skipinsert"` // field is autogenerated
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
Name string `db:"name"`
Email string `db:"email"`
}
func main() {
pgConfig := pgsql.NewClientConfig() // use defaults
pgConfig.DSN = "postgres://username:password@localhost:5432/database?sslmode=allow"
client, err := pgsql.NewClient(pgConfig)
if err != nil {
log.Fatal(err)
}
// create a repository for the table users
// Note: context is internally stored and then propagated to the appropriate sqlx methods; this is
// not the advised way of using contexts, but the rationale is to allow clean thread or application shutdown
// via context, without the overhead of adding an extra parameter to every function
repo := db.NewRepository(context.Background(), client, "users")
user1 := &UserRecord{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Name: "John Connor",
Email: "jconnor@skynet.com",
}
// Add user
if err = repo.Insert(user1); err != nil {
log.Fatal(err)
}
// Read all users
users := make([]*UserRecord, 0)
if err = repo.Fetch(repo.SqlSelect(), &users); err != nil {
log.Fatal(err)
}
// search for sarah by email
sarah := &UserRecord{}
if err = repo.FetchRecord(map[string]any{"email": "sconnor@skynet.com"}, sarah); err != nil {
if db.EmptyResult(err) {
fmt.Println("Sarah Connor not found")
} else {
log.Fatal(err)
}
}
// Count all users
count, err := repo.Count()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Total users: %d\n", count)
// Count users with specific criteria
activeCount, err := repo.CountWhere(map[string]any{"active": true})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Active users: %d\n", activeCount)
}
Interface Documentation¶
Reader Interface¶
The Reader interface provides methods for fetching data from the database:
type Reader interface {
FetchOne(qry *goqu.SelectDataset, target any) error
FetchRecord(fieldValues map[string]any, target any) error
Fetch(qry *goqu.SelectDataset, target any) error
FetchWhere(fieldValues map[string]any, target any) error
FetchByKey(keyField string, value any, target any) error
Exists(fieldName string, fieldValue any, skip ...any) (bool, error)
}
FetchOne¶
Fetches a single record using a goqu SelectDataset. The target must be a struct pointer. Automatically adds LIMIT 1 to the query.
Example:
user := &UserRecord{}
err := repo.FetchOne(repo.SqlSelect().Where(goqu.C("id").Eq(123)), user)
if err != nil {
if db.EmptyResult(err) {
log.Println("User not found")
} else {
log.Fatal(err)
}
}
FetchRecord¶
Fetches a single record with WHERE clauses built from field values. All conditions are combined with AND.
Example:
user := &UserRecord{}
err := repo.FetchRecord(map[string]any{
"email": "john@example.com",
"active": true,
}, user)
Fetch¶
Fetches multiple records. The target must be a slice pointer.
Example:
var users []*UserRecord
err := repo.Fetch(
repo.SqlSelect().
Where(goqu.C("active").IsTrue()).
OrderBy(goqu.C("created_at").Desc()),
&users,
)
FetchWhere¶
Fetches multiple records with WHERE clauses from field values map.
Example:
var users []*UserRecord
err := repo.FetchWhere(map[string]any{
"department": "engineering",
"active": true,
}, &users)
FetchByKey¶
Fetches a single record by a specific key field value.
Example:
Exists¶
Checks if records exist matching the given criteria. The optional skip parameter allows excluding specific records (useful for unique constraint validation).
Example:
// Check if email exists
exists, err := repo.Exists("email", "john@example.com")
// Check if email exists, excluding user with ID 123
exists, err = repo.Exists("email", "john@example.com", "id", 123)
Counter Interface¶
The Counter interface provides methods for counting records:
type Counter interface {
Count() (int64, error)
CountWhere(fieldValues map[string]any) (int64, error)
}
Count¶
Returns the total number of rows in the table.
Example:
CountWhere¶
Returns the number of rows matching the field values map. All conditions are combined with AND.
Example:
Executor Interface¶
The Executor interface provides methods for executing queries and raw SQL:
type Executor interface {
Exec(qry *goqu.SelectDataset) error
RawExec(sql string, args ...any) error
Select(sql string, target any, args ...any) error
}
Exec¶
Executes a query without returning results (useful for complex operations).
Example:
RawExec¶
Executes raw SQL that doesn't return rows (DDL, complex updates, stored procedures).
Example:
err := repo.RawExec(`
CREATE INDEX CONCURRENTLY idx_users_email_active
ON users(email) WHERE active = true
`)
// With parameters
err = repo.RawExec(
"CALL process_user_batch($1, $2)",
startDate, endDate,
)
Select¶
Executes raw SQL SELECT queries and scans results into target.
Example:
var stats []struct {
Department string `db:"department"`
Count int `db:"count"`
}
err := repo.Select(`
SELECT department, COUNT(*) as count
FROM users
WHERE active = $1
GROUP BY department
`, &stats, true)
Writer Interface¶
The Writer interface provides methods for inserting records:
type Writer interface {
Insert(records ...any) error
InsertReturning(record any, returnFields []string, target ...any) error
}
Insert¶
Inserts one or more records. Supports efficient batch insert operations that generate optimized SQL for multiple records.
Single Record Example:
Batch Insert Examples:
Method 1: Direct []any slice
users := []any{
&UserRecord{Name: "Alice", Email: "alice@example.com"},
&UserRecord{Name: "Bob", Email: "bob@example.com"},
&UserRecord{Name: "Charlie", Email: "charlie@example.com"},
}
err := repo.Insert(users...)
Method 2: Using ToAnySlice helper
// With typed slice
typedUsers := []*UserRecord{
{Name: "Alice", Email: "alice@example.com"},
{Name: "Bob", Email: "bob@example.com"},
}
// Convert and insert
err := repo.Insert(db.ToAnySlice(typedUsers)...)
Method 3: Large datasets with chunking
func batchInsertLarge(repo db.Repository, users []*UserRecord) error {
const chunkSize = 1000
for i := 0; i < len(users); i += chunkSize {
end := i + chunkSize
if end > len(users) {
end = len(users)
}
chunk := users[i:end]
records := make([]any, len(chunk))
for j, user := range chunk {
records[j] = user
}
if err := repo.Insert(records...); err != nil {
return fmt.Errorf("chunk %d-%d failed: %w", i, end-1, err)
}
}
return nil
}
Method 4: Batch insert with transaction
func batchInsertWithTransaction(repo db.Repository, users []*UserRecord) error {
tx, err := repo.NewTransaction(nil)
if err != nil {
return err
}
defer tx.Rollback()
records := db.ToAnySlice(users)
if err := tx.Insert(records...); err != nil {
return err
}
return tx.Commit()
}
Performance Notes: - Batch inserts generate a single optimized SQL statement - Use chunking for very large datasets (>1000 records) - Consider transactions for atomicity - Monitor memory usage with large batches
InsertReturning¶
Inserts a record and returns specified fields. Supports three scanning modes:
- Struct scanning - Maps returned fields to struct by name/tag
- Positional scanning - Maps to multiple variables by position
- Single value - Returns a single field value
Examples:
// Struct scanning - returns all fields into struct
user := &UserRecord{Name: "John", Email: "john@example.com"}
result := &UserRecord{}
err := repo.InsertReturning(user, []string{"id", "name", "email", "created_at"}, result)
// Positional scanning - returns to individual variables
var id int64
var createdAt time.Time
err := repo.InsertReturning(user, []string{"id", "created_at"}, &id, &createdAt)
// Single value - returns just the ID
var newID int64
err := repo.InsertReturning(user, []string{"id"}, &newID)
Updater Interface¶
The Updater interface provides comprehensive update operations:
type Updater interface {
Update(qry *goqu.UpdateDataset) error
UpdateReturning(record any, whereFieldsValues map[string]any, returnFields []string, target ...any) error
UpdateRecord(record any, whereFieldsValues map[string]any) error
UpdateFields(record any, fieldsValues map[string]any, whereFieldsValues map[string]any) error
UpdateFieldsReturning(record any, fieldsValues map[string]any, whereFieldsValues map[string]any, returnFields []string, target ...any) error
UpdateByKey(record any, keyField string, value any) error
}
Update (Deprecated)¶
Executes an update using goqu UpdateDataset. This method is deprecated due to serialization issues with some data types. Use UpdateRecord instead.
UpdateRecord¶
Updates a record using the modern query builder. WHERE conditions are combined with AND.
Example:
user := &UserRecord{
Name: "John Updated",
Email: "john.updated@example.com",
UpdatedAt: time.Now(),
}
err := repo.UpdateRecord(user, map[string]any{"id": 123})
UpdateFields¶
func (r *repository) UpdateFields(record any, fieldsValues map[string]any, whereFieldsValues map[string]any) error
Updates specific fields only, useful for partial updates.
Example:
// Update only specific fields
err := repo.UpdateFields(
&UserRecord{}, // empty struct for type info
map[string]any{
"last_login": time.Now(),
"login_count": goqu.L("login_count + 1"),
},
map[string]any{"id": 123},
)
UpdateReturning¶
func (r *repository) UpdateReturning(record any, whereFieldsValues map[string]any, returnFields []string, target ...any) error
Updates a record and returns specified fields. Supports the same three scanning modes as InsertReturning.
Example:
user := &UserRecord{Name: "John Updated"}
result := &UserRecord{}
err := repo.UpdateReturning(
user,
map[string]any{"id": 123},
[]string{"id", "name", "updated_at"},
result,
)
UpdateFieldsReturning¶
func (r *repository) UpdateFieldsReturning(record any, fieldsValues map[string]any, whereFieldsValues map[string]any, returnFields []string, target ...any) error
Updates specific fields and returns values. Combines UpdateFields with RETURNING support.
Example:
var updatedAt time.Time
err := repo.UpdateFieldsReturning(
&UserRecord{},
map[string]any{"status": "verified"},
map[string]any{"id": 123},
[]string{"updated_at"},
&updatedAt,
)
UpdateByKey¶
Updates a record using a single key field condition.
Example:
user := &UserRecord{
Name: "Updated Name",
UpdatedAt: time.Now(),
}
err := repo.UpdateByKey(user, "id", 123)
Deleter Interface¶
The Deleter interface provides methods for deleting records:
type Deleter interface {
Delete(qry *goqu.DeleteDataset) error
DeleteWhere(fieldNameValue map[string]any) error
DeleteByKey(keyField string, value any) error
}
Delete¶
Executes a delete query using goqu DeleteDataset.
Example:
err := repo.Delete(
repo.SqlDelete().Where(
goqu.C("created_at").Lt(time.Now().AddDate(-1, 0, 0)),
),
)
DeleteWhere¶
Deletes records matching field values. All conditions are combined with AND.
Example:
DeleteByKey¶
Deletes a record by a single key field.
Example:
GridOps Interface¶
The Repository also implements a GridOps interface that provides methods for working with data grids:
type GridOps interface {
Grid(record any) (*Grid, error)
QueryGrid(record any, args GridQuery, dest any) error
}
Grid(record any) (*Grid, error)
- Creates a Grid object based on the provided record type, using field tags to determine which fields can be sorted, filtered, or searchedQueryGrid(record any, args GridQuery, dest any) error
- Creates a Grid object and executes a query using the provided GridQuery parameters
Example Usage¶
type UserRecord struct {
ID int `db:"id" json:"id" grid:"sort,filter"`
Username string `db:"username" json:"username" grid:"sort,search,filter"`
Email string `db:"email" json:"email" grid:"sort,search,filter"`
Active bool `db:"active" json:"active" grid:"filter"`
}
func main() {
// ... setup connection and repository as shown earlier ...
repo := db.NewRepository(context.Background(), client, "users")
// Create a GridQuery for searching and filtering
query, err := db.NewGridQuery(db.SearchAny, 10, 0)
if err != nil {
log.Fatal(err)
}
// Set search parameters
query.SearchText = "john"
query.FilterFields = map[string]any{
"active": true,
}
query.SortFields = map[string]string{
"username": db.SortAscending,
}
// Execute the query
var users []*UserRecord
if err := repo.QueryGrid(&UserRecord{}, query, &users); err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d active users matching 'john'\n", len(users))
// Alternatively, create and configure a Grid manually
grid, err := repo.Grid(&UserRecord{})
if err != nil {
log.Fatal(err)
}
// Add custom filter function
grid.AddFilterFunc("active", func(value any) (any, error) {
switch v := value.(type) {
case string:
switch v {
case "yes", "true", "1":
return true, nil
case "no", "false", "0":
return false, nil
default:
return nil, fmt.Errorf("invalid boolean value: %v", v)
}
case bool:
return v, nil
default:
return nil, fmt.Errorf("unsupported type: %T", value)
}
})
// Validate and build the query
if err := grid.ValidQuery(query); err != nil {
log.Fatal(err)
}
statement, err := grid.Build(repo.SqlSelect(), query)
if err != nil {
log.Fatal(err)
}
// Execute the query
var filteredUsers []*UserRecord
if err := repo.Fetch(statement, &filteredUsers); err != nil {
log.Fatal(err)
}
}
For more detailed information about grid functionality, see the Data Grid documentation.
Builder Interface¶
The Builder interface provides methods for creating SQL query builders:
type Builder interface {
Sql() goqu.DialectWrapper
SqlSelect() *goqu.SelectDataset
SqlInsert() *goqu.InsertDataset
SqlUpdate() *goqu.UpdateDataset
SqlDelete() *goqu.DeleteDataset
}
Sql¶
Returns the goqu dialect wrapper for building custom queries.
Example:
dialect := repo.Sql()
customQuery := dialect.From("users").
InnerJoin(goqu.T("departments"), goqu.On(goqu.C("users.dept_id").Eq(goqu.C("departments.id")))).
Select("users.*", "departments.name")
SqlSelect¶
Returns a SELECT query builder for the repository's table.
Example:
query := repo.SqlSelect().
Where(goqu.C("active").IsTrue()).
OrderBy(goqu.C("created_at").Desc()).
Limit(10)
var users []*UserRecord
err := repo.Fetch(query, &users)
SqlInsert¶
Returns an INSERT query builder. Note: goqu prepared statements are not compatible with PostgreSQL extended types; use Insert() method or SqlBuilder() instead.
Example:
// Not recommended for PostgreSQL extended types
insertQuery := repo.SqlInsert().Rows(
goqu.Record{"name": "John", "email": "john@example.com"},
)
SqlUpdate¶
Returns an UPDATE query builder. Note: goqu prepared statements are not compatible with PostgreSQL extended types; use UpdateRecord() or SqlBuilder() instead.
SqlDelete¶
Returns a DELETE query builder for the repository's table.
Example:
deleteQuery := repo.SqlDelete().
Where(goqu.C("status").Eq("deleted")).
Where(goqu.C("deleted_at").Lt(time.Now().AddDate(0, -1, 0)))
err := repo.Delete(deleteQuery)
SqlBuilder Interface¶
The SqlBuilder interface provides access to the modern query builder:
type SqlBuilder interface {
SqlDialect() qb.SqlDialect
SqlBuilder() *qb.SqlBuilder
SqlUpdateX(record any) *qb.UpdateBuilder
Do(qry any, target ...any) error
}
SqlDialect¶
Returns the SQL dialect being used by the repository.
Example:
SqlBuilder¶
Returns the query builder instance for advanced SQL construction.
Example:
SqlUpdateX¶
Creates an UpdateBuilder for the given record with advanced options.
Example:
user := &UserRecord{Name: "Updated Name"}
updateBuilder := repo.SqlUpdateX(user).
WithOptions(&qb.UpdateOptions{
IncludeFields: []string{"name", "updated_at"},
UpdateAutoFields: true,
}).
Where(qb.Eq("id", 123))
err := repo.Do(updateBuilder)
Do¶
Executes various query types (SELECT, UPDATE, INSERT, DELETE) with optional target for results.
Example:
// Execute UpdateBuilder
updateBuilder := repo.SqlUpdateX(user).Where(qb.Eq("id", 123))
err := repo.Do(updateBuilder)
// Execute with RETURNING
updateBuilder = repo.SqlUpdateX(user).
WithOptions(&qb.UpdateOptions{
ReturningFields: []string{"id", "updated_at"},
}).
Where(qb.Eq("id", 123))
result := &UserRecord{}
err := repo.Do(updateBuilder, result)
Transaction Support¶
The Repository supports database transactions through the Transaction interface:
type Transaction interface {
Builder
Reader
Executor
Writer
Deleter
Updater
Counter
SqlBuilder
Db() *sqlx.Tx
Name() string
Commit() error
Rollback() error
}
Creating Transactions¶
Creates a new transaction with optional transaction options.
Example:
// Basic transaction
tx, err := repo.NewTransaction(nil)
if err != nil {
return err
}
defer tx.Rollback() // Always defer rollback
// Transaction with options
opts := &sql.TxOptions{
Isolation: sql.LevelSerializable,
ReadOnly: false,
}
tx, err := repo.NewTransaction(opts)
Using Transactions¶
Transactions implement all the same interfaces as Repository, allowing seamless operation:
func transferFunds(repo db.Repository, fromID, toID int, amount decimal.Decimal) error {
tx, err := repo.NewTransaction(nil)
if err != nil {
return err
}
defer tx.Rollback()
// Debit source account
err = tx.UpdateFields(
&AccountRecord{},
map[string]any{"balance": goqu.L("balance - ?", amount)},
map[string]any{"id": fromID},
)
if err != nil {
return fmt.Errorf("failed to debit account: %w", err)
}
// Credit destination account
err = tx.UpdateFields(
&AccountRecord{},
map[string]any{"balance": goqu.L("balance + ?", amount)},
map[string]any{"id": toID},
)
if err != nil {
return fmt.Errorf("failed to credit account: %w", err)
}
// Insert transaction record
txRecord := &TransactionRecord{
FromAccountID: fromID,
ToAccountID: toID,
Amount: amount,
CreatedAt: time.Now(),
}
if err := tx.Insert(txRecord); err != nil {
return fmt.Errorf("failed to record transaction: %w", err)
}
// Commit the transaction
return tx.Commit()
}
Transaction Best Practices¶
- Always defer Rollback(): Even if you plan to commit, deferred rollback is a safety net
- Keep transactions short: Long-running transactions can cause lock contention
- Handle errors properly: Any error should trigger a rollback
- Use appropriate isolation levels: Choose based on your consistency requirements
Example of complex transaction:
func processOrder(repo db.Repository, orderID int) error {
tx, err := repo.NewTransaction(&sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
if err != nil {
return err
}
defer tx.Rollback()
// Lock order for update
var order OrderRecord
err = tx.FetchRecord(
map[string]any{"id": orderID, "status": "pending"},
&order,
)
if err != nil {
if db.EmptyResult(err) {
return errors.New("order not found or already processed")
}
return err
}
// Process order items
var items []*OrderItemRecord
err = tx.FetchWhere(map[string]any{"order_id": orderID}, &items)
if err != nil {
return err
}
for _, item := range items {
// Update inventory
err = tx.UpdateFields(
&ProductRecord{},
map[string]any{"stock": goqu.L("stock - ?", item.Quantity)},
map[string]any{"id": item.ProductID},
)
if err != nil {
return fmt.Errorf("failed to update inventory: %w", err)
}
}
// Update order status
order.Status = "completed"
order.CompletedAt = time.Now()
err = tx.UpdateRecord(&order, map[string]any{"id": orderID})
if err != nil {
return err
}
return tx.Commit()
}
Advanced Usage Patterns¶
Custom Query Execution¶
func customAggregation(repo db.Repository) error {
// Complex aggregation query
query := repo.SqlSelect().
Select(
goqu.C("department"),
goqu.COUNT("*").As("count"),
goqu.AVG("salary").As("avg_salary"),
goqu.MAX("salary").As("max_salary"),
).
GroupBy("department").
Having(goqu.COUNT("*").Gt(5)).
OrderBy(goqu.C("avg_salary").Desc())
var results []struct {
Department string `db:"department"`
Count int `db:"count"`
AvgSalary float64 `db:"avg_salary"`
MaxSalary float64 `db:"max_salary"`
}
return repo.Fetch(query, &results)
}
Batch Insert Operations¶
func batchInsertWithProgress(repo db.Repository, users []*UserRecord) error {
const batchSize = 500
total := len(users)
for i := 0; i < total; i += batchSize {
end := i + batchSize
if end > total {
end = total
}
batch := users[i:end]
records := db.ToAnySlice(batch)
err := repo.Insert(records...)
if err != nil {
return fmt.Errorf("batch %d-%d failed: %w", i, end-1, err)
}
progress := float64(end) / float64(total) * 100
log.Printf("Inserted %d-%d: %.1f%% complete", i, end-1, progress)
}
return nil
}
func batchInsertWithErrorRecovery(repo db.Repository, users []*UserRecord) error {
var successful []string
var failed []struct {
User *UserRecord
Error error
}
// Try batch insert first
records := db.ToAnySlice(users)
err := repo.Insert(records...)
if err == nil {
log.Printf("Successfully batch inserted %d users", len(users))
return nil
}
log.Printf("Batch insert failed, trying individual inserts: %v", err)
// Fall back to individual inserts
for _, user := range users {
err := repo.Insert(user)
if err != nil {
failed = append(failed, struct {
User *UserRecord
Error error
}{user, err})
} else {
successful = append(successful, user.Name)
}
}
log.Printf("Individual inserts: %d successful, %d failed", len(successful), len(failed))
if len(failed) > 0 {
return fmt.Errorf("failed to insert %d records", len(failed))
}
return nil
}
Batch Update Operations¶
func batchUpdateWithProgress(repo db.Repository, updates []UserUpdate) error {
total := len(updates)
completed := 0
for i := 0; i < total; i += 100 {
end := i + 100
if end > total {
end = total
}
tx, err := repo.NewTransaction(nil)
if err != nil {
return err
}
for _, update := range updates[i:end] {
err := tx.UpdateByKey(&update, "id", update.ID)
if err != nil {
tx.Rollback()
return fmt.Errorf("failed at record %d: %w", update.ID, err)
}
}
if err := tx.Commit(); err != nil {
return err
}
completed = end
log.Printf("Progress: %d/%d (%.1f%%)", completed, total, float64(completed)/float64(total)*100)
}
return nil
}
Repository Factory Pattern¶
type RepositoryFactory struct {
client db.Client
ctx context.Context
}
func NewRepositoryFactory(client db.Client) *RepositoryFactory {
return &RepositoryFactory{
client: client,
ctx: context.Background(),
}
}
func (f *RepositoryFactory) Users() db.Repository {
return db.NewRepository(f.ctx, f.client, "users")
}
func (f *RepositoryFactory) Orders() db.Repository {
return db.NewRepository(f.ctx, f.client, "orders")
}
func (f *RepositoryFactory) Products() db.Repository {
return db.NewRepository(f.ctx, f.client, "products")
}
// Usage
factory := NewRepositoryFactory(client)
userRepo := factory.Users()
orderRepo := factory.Orders()
Performance Considerations¶
Connection Pooling¶
- Repositories share the client's connection pool
- Configure pool size based on concurrent operations
- Monitor pool usage to avoid exhaustion
Query Optimization¶
- Use appropriate indexes for WHERE clauses
- Batch operations when possible
- Use RETURNING clauses to avoid extra queries
- Consider pagination for large result sets
Caching¶
- Repository caches field specifications per struct type
- Grid specifications are cached after first use
- Consider application-level caching for frequently accessed data
Error Handling¶
The Repository provides consistent error handling:
// Check for empty results
user := &UserRecord{}
err := repo.FetchByKey("id", 123, user)
if err != nil {
if db.EmptyResult(err) {
// Handle not found case
return nil, ErrUserNotFound
}
// Handle actual error
return nil, fmt.Errorf("database error: %w", err)
}
// Handle constraint violations
err = repo.Insert(user)
if err != nil {
if isUniqueViolation(err) {
return ErrDuplicateEmail
}
return fmt.Errorf("insert failed: %w", err)
}
Best Practices¶
- Use appropriate interfaces: Don't pass full Repository when only Reader is needed
- Leverage transactions: Group related operations in transactions
- Handle errors properly: Always check for EmptyResult on single record fetches
- Use struct tags: Properly tag structs for field mapping and grid functionality
- Batch operations: Use batch methods for bulk operations
- Monitor performance: Log slow queries and optimize as needed