Store Package Design
Overview
The Store package is the core of Bingo's data access layer. It implements a flexible and extensible data access framework through generics and composition pattern, reducing code duplication and improving code reusability.
All services (apiserver, admserver, etc.) use the unified generic Store[T] pattern in internal/pkg/store for data access.
Package Structure
pkg/store/
├── store.go # Universal Store[T] implementation
├── logger.go # Logger interface definition
└── where/
└── where.go # Query condition builder
internal/pkg/store/
├── store.go # IStore interface and application-level implementation (datastore)
├── logger.go # Business logger implementation
└── <model>.go # Business Store implementationsNote: All files in internal/pkg/store must be flat in the same directory to avoid circular imports. Use naming conventions rather than directory structure to organize modules.
Naming Convention
All files in internal/pkg/store must be flat (avoiding circular imports), using naming conventions to organize code.
Convention
File names:
<prefix>_<model>.go(e.g.,sys_admin.go,bot_channel.go,user.go)- System modules:
sys_prefix - Other modules: module name prefix (e.g.,
bot_,api_) - Can omit prefix when no conflict (e.g.,
user.go)
- System modules:
Store interface:
<Prefix><Model>Store(e.g.,BotChannelStore,AdminStore)- Add module prefix only when needed to distinguish from same-name models
Implementation struct: lowercase
<prefix><model>Store(e.g.,botChannelStore)Extension interface:
<Prefix><Model>Expansion(e.g.,BotChannelExpansion)Factory function:
New<Prefix><Model>Store()IStore methods: Plural or singular, keep it simple (e.g.,
Users(),Bot(),BotChannel())
Core Design
1. Generics + Composition Pattern
Universal Store[T] implements all CRUD operations. Business-specific Stores extend through composition:
// pkg/store - Universal implementation
type Store[T any] struct {
logger Logger
storage DBProvider
}
// internal/pkg/store - Business extension
type userStore struct {
*genericstore.Store[User]
}
type UserStore interface {
Create(ctx context.Context, obj *User) error
Update(ctx context.Context, obj *User, fields ...string) error
Delete(ctx context.Context, opts *where.Options) error
Get(ctx context.Context, opts *where.Options) (*User, error)
List(ctx context.Context, opts *where.Options) (int64, []*User, error)
UserExpansion // Business-specific extension interface
}2. Condition Builder
Use where package for fluent API to build query conditions:
// Pagination query
opts := where.F("status", "active").P(1, 10)
// Complex conditions
opts := where.NewWhere().
F("status", "active").
Q("created_at > ?", time.Now().AddDate(0, 0, -7)).
Load("User", "Tags").
P(1, 20)
// Convenience functions
opts := where.P(1, 10) // Pagination
opts := where.F("field", value) // Filter
opts := where.Load("Association") // PreloadSupported operations:
F(kvs...)- Filter conditionsQ(query, args...)- Custom SQLP(page, pageSize)- PaginationO(offset)/L(limit)- Offset and limitC(clauses...)- GORM clausesLoad(associations...)- Preload associations
3. Transaction Context
Automatically handle transactions through context. Store layer transparently supports transactions:
// internal/pkg/store/store.go
func (ds *datastore) DB(ctx context.Context, wheres ...where.Where) *gorm.DB {
db := ds.core
// Automatically extract transaction from context
if tx, ok := ctx.Value(transactionKey{}).(*gorm.DB); ok {
db = tx
}
// Apply query conditions
for _, whr := range wheres {
db = whr.Where(db)
}
return db
}
// Transaction API
func (ds *datastore) TX(ctx context.Context, fn func(ctx context.Context) error) error {
return ds.core.WithContext(ctx).Transaction(
func(tx *gorm.DB) error {
ctx = context.WithValue(ctx, transactionKey{}, tx)
return fn(ctx)
},
)
}API Reference
Store[T] Methods
// CRUD operations
Create(ctx context.Context, obj *T) error
Update(ctx context.Context, obj *T, fields ...string) error
Delete(ctx context.Context, opts *where.Options) error
Get(ctx context.Context, opts *where.Options) (*T, error)
// Query operations
List(ctx context.Context, opts *where.Options) (count int64, ret []*T, err error)
Find(ctx context.Context, opts *where.Options) (ret []*T, err error)
Last(ctx context.Context, opts *where.Options) (*T, error)
// Batch and conditional operations
CreateInBatch(ctx context.Context, objs []*T, batchSize int) error
CreateIfNotExist(ctx context.Context, obj *T) error
FirstOrCreate(ctx context.Context, where any, obj *T) error
UpdateOrCreate(ctx context.Context, where any, obj *T) error
Upsert(ctx context.Context, obj *T, fields ...string) error
DeleteInBatch(ctx context.Context, ids []uint) error
// Raw database access
DB(ctx context.Context, wheres ...where.Where) *gorm.DBMethod Description:
- CreateIfNotExist: Create object if not exists (using OnConflict DoNothing)
- FirstOrCreate: Find object by condition, create if not exists
- UpdateOrCreate: Update or create object in transaction, supports optimistic locking
- DeleteInBatch: Batch delete objects by IDs
IStore Interface
IStore is the application-level unified data access interface responsible for returning each Store implementation. The interface adopts a modular design, returning corresponding Stores through methods:
type IStore interface {
// Transaction and database
DB(ctx context.Context, wheres ...where.Where) *gorm.DB
TX(ctx context.Context, fn func(ctx context.Context) error) error
// Business Store methods (organized by module)
// Example: Users() UserStore, Admin() AdminStore, etc.
}Usage Examples
This chapter demonstrates basic usage of Store through examples.
Basic CRUD Operations
Assume we have a User model with basic operations through Store:
// Create
user := &User{Name: "John", Email: "[email protected]"}
err := store.Users().Create(ctx, user)
// Read
user, err := store.Users().Get(ctx, where.F("id", 1))
// Update (update only specified fields)
user.Email = "[email protected]"
err := store.Users().Update(ctx, user, "email")
// Delete
err := store.Users().Delete(ctx, where.F("id", 1))Query and Pagination
// Build query conditions
opts := where.F("status", "active").
P(1, 10) // Page 1, 10 items per page
// Execute query
count, users, err := store.Users().List(ctx, opts)Transaction Processing
Use TX() method when multiple operations need atomic guarantees:
err := store.TX(ctx, func(ctx context.Context) error {
// Store automatically uses transaction
if err := store.Users().Create(ctx, user1); err != nil {
return err // Auto rollback
}
if err := store.Users().Create(ctx, user2); err != nil {
return err // Auto rollback
}
return nil // Auto commit
})Extension Operations
Business-specific Stores can add custom operations through extension interfaces:
// internal/pkg/store/user.go
type UserExpansion interface {
FindByEmail(ctx context.Context, email string) (*User, error)
}
func (s *userStore) FindByEmail(ctx context.Context, email string) (*User, error) {
return s.Get(ctx, where.F("email", email))
}Adding New Business Store
Adding a new Store requires following these steps and naming conventions (see "Naming Convention" section):
1. Create Store Interface and Implementation
// internal/pkg/store/user.go
package store
// Store interface defines CRUD operations
type UserStore interface {
Create(ctx context.Context, obj *User) error
Update(ctx context.Context, obj *User, fields ...string) error
Delete(ctx context.Context, opts *where.Options) error
Get(ctx context.Context, opts *where.Options) (*User, error)
List(ctx context.Context, opts *where.Options) (int64, []*User, error)
UserExpansion // Extension interface
}
// Extension interface defines business-specific operations
type UserExpansion interface {
FindByEmail(ctx context.Context, email string) (*User, error)
}
// Implementation class
type userStore struct {
*genericstore.Store[User]
}
// Factory function
func NewUserStore(store *datastore) *userStore {
return &userStore{
Store: genericstore.NewStore[User](store, NewLogger()),
}
}
// Implement extension method
func (s *userStore) FindByEmail(ctx context.Context, email string) (*User, error) {
return s.Get(ctx, where.F("email", email))
}2. Register to IStore
Add method in internal/pkg/store/store.go:
type IStore interface {
Users() UserStore // New addition
// ...
}
func (ds *datastore) Users() UserStore {
return NewUserStore(ds)
}Next Step
- API Server - Learn about the core API service design and usage