Database Support
Database Support
Ginboot provides a powerful and flexible multi-database support system through a generic repository interface. This allows you to interact with different database systems (MongoDB, SQL, DynamoDB) using a consistent API, making your application more modular, testable, and adaptable to various data storage needs.
Generic Repository Interface
The core of Ginboot's database abstraction is the GenericRepository[T any] interface. This interface defines a comprehensive set of common data access operations, ensuring a uniform way to interact with different database types.
type GenericRepository[T any] interface {
FindById(id string) (T, error)
FindAllById(ids []string) ([]T, error)
Save(doc T) error
SaveOrUpdate(doc T) error
SaveAll(docs []T) error
Update(doc T) error
Delete(id string) error
FindOneBy(field string, value interface{}) (T, error)
FindOneByFilters(filters map[string]interface{}) (T, error)
FindBy(field string, value interface{}) ([]T, error)
FindByFilters(filters map[string]interface{}) ([]T, error)
FindAll(options ...interface{}) ([]T, error)
FindAllPaginated(pageRequest PageRequest) (PageResponse[T], error)
FindByPaginated(pageRequest PageRequest, filters map[string]interface{}) (PageResponse[T], error)
CountBy(field string, value interface{}) (int64, error)
CountByFilters(filters map[string]interface{}) (int64, error)
ExistsBy(field string, value interface{}) (bool, error)
ExistsByFilters(filters map[string]interface{}) (bool, error)
}Document Interface
For SQL and DynamoDB repositories, your data models must implement the Document interface, which provides the table/collection name.
type Document interface {
GetTableName() string
}Pagination Structures
Ginboot provides standardized structures for handling pagination requests and responses.
type SortField struct {
Field string `json:"field"`
Direction int `json:"direction"` // 1 for ascending, -1 for descending
}
type PageRequest struct {
Page int `json:"page"`
Size int `json:"size"`
Sort SortField `json:"sort"`
}
type PageResponse[T interface{}] struct {
Contents []T `json:"content"`
NumberOfElements int `json:"numberOfElements"`
Pageable PageRequest `json:"pageable"`
TotalPages int `json:"totalPages"`
TotalElements int `json:"totalElements"`
}MongoDB Database Support
Ginboot offers robust support for MongoDB through MongoConfig for connection management and MongoRepository for data operations.
MongoDB Configuration
Use ginboot.NewMongoConfig() to build your MongoDB connection string. You can specify host, port, credentials, database name, and additional options.
import (
"log"
"github.com/klass-lk/ginboot"
)
func connectMongo() *mongo.Database {
config := ginboot.NewMongoConfig().
WithHost("localhost", 27017).
WithDatabase("mydatabase").
WithCredentials("myuser", "mypassword").
WithOption("authSource", "admin")
db, err := config.Connect()
if err != nil {
log.Fatalf("Failed to connect to MongoDB: %v", err)
}
fmt.Println("Connected to MongoDB!")
return db
}MongoDB Repository Example
Define your document struct with bson tags for MongoDB field mapping and a ginboot:"_id" tag for the primary key if it's not named ID.
import (
"fmt"
"go.mongodb.org/mongo-driver/mongo"
"github.com/klass-lk/ginboot"
)
type User struct {
ID string `bson:"_id" ginboot:"_id"` // ginboot:_id helps the repository identify the ID field
Name string `bson:"name"`
Age int `bson:"age"`
}
// NewMongoRepository creates a new MongoDB repository instance.
// The collection name is typically the plural of your entity name.
func NewUserRepository(db *mongo.Database) *UserRepository {
return &UserRepository{
MongoRepository: ginboot.NewMongoRepository[User](db, "users"),
}
}
// Example usage of the MongoDB repository
func main() {
db := connectMongo() // Assume connectMongo() returns *mongo.Database
repo := ginboot.NewMongoRepository[User](db, "users")
// Save a new user
user := User{ID: "1", Name: "John Doe", Age: 30}
err := repo.Save(user)
if err != nil { log.Fatal(err) }
fmt.Println("User saved:", user.Name)
// Find user by ID
foundUser, err := repo.FindById("1")
if err != nil { log.Fatal(err) }
fmt.Println("Found user:", foundUser.Name)
// Update user
foundUser.Age = 31
err = repo.Update(foundUser)
if err != nil { log.Fatal(err) }
fmt.Println("User updated:", foundUser.Name)
// Find users by filter
filters := map[string]interface{}{"age": 31}
users, err := repo.FindByFilters(filters)
if err != nil { log.Fatal(err) }
fmt.Println("Users with age 31:", len(users))
// Paginated query
pageRequest := ginboot.PageRequest{Page: 1, Size: 10, Sort: ginboot.SortField{Field: "name", Direction: 1}}
pageResponse, err := repo.FindAllPaginated(pageRequest)
if err != nil { log.Fatal(err) }
fmt.Println("Paginated results:", len(pageResponse.Contents))
}SQL Database Support
Ginboot provides a generic repository interface for SQL databases, allowing you to interact with relational databases like PostgreSQL or MySQL using a consistent API.
SQL Configuration
Use ginboot.NewSQLConfig() to configure your SQL connection. You need to specify the Driver (e.g., "postgres", "mysql"), host, port, credentials, and database name.
import (
"log"
"database/sql"
"github.com/klass-lk/ginboot"
_ "github.com/lib/pq" // Import the PostgreSQL driver
)
func connectSQL() *sql.DB {
config := ginboot.NewSQLConfig().
WithDriver("postgres").
WithHost("localhost", 5432).
WithDatabase("testdb").
WithCredentials("postgres", "password").
WithOption("sslmode", "disable")
db, err := config.Connect()
if err != nil {
log.Fatalf("Failed to connect to PostgreSQL: %v", err)
}
fmt.Println("Connected to PostgreSQL!")
return db
}SQL Repository Example
Your SQL document struct must implement the Document interface and use db tags to map fields to database columns. The ID field is assumed to be the primary key.
import (
"fmt"
"log"
"database/sql"
"time"
"github.com/klass-lk/ginboot"
)
type Product struct {
ID string `db:"id"`
Name string `db:"name"`
Price float64 `db:"price"`
CreatedAt time.Time `db:"created_at"`
}
func (p Product) GetTableName() string {
return "products"
}
// Example usage of the SQL repository
func main() {
db := connectSQL() // Assume connectSQL() returns *sql.DB
repo := ginboot.NewSQLRepository[Product](db)
// Ensure the table exists (optional, can be done once at startup)
err := repo.CreateTable()
if err != nil { log.Fatal(err) }
// Save a new product
product := Product{ID: "p1", Name: "Laptop", Price: 1200.00, CreatedAt: time.Now()}
err = repo.Save(product)
if err != nil { log.Fatal(err) }
fmt.Println("Product saved:", product.Name)
// Find product by ID
foundProduct, err := repo.FindById("p1")
if err != nil { log.Fatal(err) }
fmt.Println("Found product:", foundProduct.Name)
// Update product
foundProduct.Price = 1150.00
err = repo.Update(foundProduct)
if err != nil { log.Fatal(err) }
fmt.Println("Product updated:", foundProduct.Name)
// Find products by filter
filters := map[string]interface{}{"name": "Laptop"}
products, err := repo.FindByFilters(filters)
if err != nil { log.Fatal(err) }
fmt.Println("Products named Laptop:", len(products))
// Delete product
err = repo.Delete("p1")
if err != nil { log.Fatal(err) }
fmt.Println("Product deleted.")
}DynamoDB Support
Ginboot provides robust support for AWS DynamoDB, offering a similar generic repository interface for interacting with NoSQL tables.
DynamoDB Configuration
Use ginboot.NewDynamoConfig() to configure your DynamoDB client. You can specify the AWS region, credentials (access key and secret key), a custom endpoint (useful for local DynamoDB), or an AWS profile.
import (
"log"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/klass-lk/ginboot"
)
func connectDynamoDB() *dynamodb.Client {
config := ginboot.NewDynamoConfig().
WithRegion("us-east-1").
WithEndpoint("http://localhost:8000") // For local DynamoDB
// .WithCredentials("your-access-key", "your-secret-key")
// .WithProfile("your-aws-profile")
client, err := config.Connect()
if err != nil {
log.Fatalf("Failed to connect to DynamoDB: %v", err)
}
fmt.Println("Connected to DynamoDB!")
return client
}DynamoDB Repository Example
Your DynamoDB document struct must implement the Document interface and use dynamodbav tags to map fields to DynamoDB attributes. The ID field is assumed to be the primary key.
import (
"fmt"
"log"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/klass-lk/ginboot"
)
type Order struct {
ID string `ginboot:"id"`
CustomerID string `dynamodbav:"customer_id"`
Amount float64 `dynamodbav:"amount"`
Status string `dynamodbav:"status"`
}
func (o Order) GetTableName() string {
return "orders"
}
// Example usage of the DynamoDB repository
func main() {
client := connectDynamoDB() // Assume connectDynamoDB() returns *dynamodb.Client
// The last parameter (skipTableCreation) can be set to true if you manage table creation externally
repo := ginboot.NewDynamoDBRepository[Order](client, "orders", false)
// Save a new order
order := Order{ID: "o1", CustomerID: "cust123", Amount: 99.99, Status: "PENDING"}
err := repo.Save(order)
if err != nil { log.Fatal(err) }
fmt.Println("Order saved:", order.ID)
// Find order by ID
foundOrder, err := repo.FindById("o1")
if err != nil { log.Fatal(err) }
fmt.Println("Found order:", foundOrder.ID, "Status:", foundOrder.Status)
// Update order
foundOrder.Status = "COMPLETED"
err = repo.Update(foundOrder)
if err != nil { log.Fatal(err) }
fmt.Println("Order updated:", foundOrder.ID, "Status:", foundOrder.Status)
// Find orders by filter (Note: DynamoDB Scan operations can be inefficient for large tables)
filters := map[string]interface{}{"customer_id": "cust123"}
orders, err := repo.FindByFilters(filters)
if err != nil { log.Fatal(err) }
fmt.Println("Orders for customer cust123:", len(orders))
// Delete order
err = repo.Delete("o1")
if err != nil { log.Fatal(err) }
fmt.Println("Order deleted.")
}Considerations for DynamoDB Performance
- Scan vs. Query: Methods like
FindOneBy,FindOneByFilters,FindBy,FindByFilters,CountBy,CountByFilters, and pagination methods (FindAllPaginated,FindByPaginated) internally use DynamoDB'sScanoperation when filtering on non-primary key attributes.Scanoperations read every item in the table and can be inefficient and costly for large tables. - Global Secondary Indexes (GSIs): For better performance on frequently queried non-primary key fields, consider defining Global Secondary Indexes (GSIs) on your DynamoDB tables. Ginboot's generic repository methods do not automatically leverage GSIs; you would typically use the underlying
*dynamodb.Clientdirectly for GSI-based queries or extend the repository to include GSI-aware methods. - Pagination: DynamoDB's native pagination uses
ExclusiveStartKeyrather than traditional offset/limit. Ginboot's pagination methods simulate offset/limit by performing multipleScanoperations and discarding items, which can be inefficient for deep pagination. For optimal performance with large datasets, consider implementing cursor-based pagination directly using DynamoDB'sExclusiveStartKey.
Customizing Repositories
You can easily extend Ginboot's generic repositories to add database-specific methods or custom business logic. This is done by embedding the generic repository within your own custom repository struct.
import (
"go.mongodb.org/mongo-driver/mongo"
"github.com/klass-lk/ginboot"
)
type UserRepository struct {
*ginboot.MongoRepository[User] // Embed the generic repository
}
func NewUserRepository(db *mongo.Database) *UserRepository {
return &UserRepository{
MongoRepository: ginboot.NewMongoRepository[User](db, "users"),
}
}
// Add a custom method specific to UserRepository
func (r *UserRepository) FindUsersByStatus(status string) ([]User, error) {
// You can use the embedded generic repository methods
return r.FindBy("status", status)
}
// Or implement a completely custom query
func (r *UserRepository) GetActiveUsersCount() (int64, error) {
// Access the underlying collection directly if needed
// return r.collection.CountDocuments(context.Background(), bson.M{"status": "active"})
return r.CountBy("status", "active")
}