Update studio-backend/guidelines-repository.md
This commit is contained in:
@ -1,276 +1,52 @@
|
|||||||
# Repository Pattern Guidelines for New Developers
|
# Guidelines on Repository
|
||||||
|
|
||||||
## Overview
|
## Basic structure of repository for entity with workspace
|
||||||
|
|
||||||
This guide explains how to properly define repositories in our codebase using the MetaORM framework. The repository pattern provides a clean separation between data access logic and business logic.
|
|
||||||
|
|
||||||
## Repository Structure
|
|
||||||
|
|
||||||
### 1. Repository Interface
|
|
||||||
|
|
||||||
Define a repository interface that extends the base MetaORM repository:
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type EntityNameRepository interface {
|
import "git.metadiv.io/metadiv-studio/metaorm"
|
||||||
metaorm.Repository[EntityName]
|
|
||||||
|
func NewWorkspaceMemberLimitRepository(db metaorm.Database, workspaceId uint) WorkspaceMemberLimitRepository {
|
||||||
// Custom query methods with descriptive names
|
return &WorkspaceMemberLimitRepositoryImpl{
|
||||||
FindByEmail(email string) (*EntityName, error)
|
WorkspaceRepositoryImpl: metaorm.NewWorkspaceRepositoryImpl[WorkspaceMemberLimit](db, workspaceId),
|
||||||
FindByUserIdAndWorkspaceId(userId uint, workspaceId uint) (*EntityName, error)
|
}
|
||||||
FilteredPagedFindMany(
|
}
|
||||||
page metaorm.PaginationQuery,
|
|
||||||
sort metaorm.SortingQuery,
|
type WorkspaceMemberLimitRepository interface {
|
||||||
keywordFilter metaorm.Query,
|
metaorm.WorkspaceRepository[WorkspaceMemberLimit]
|
||||||
filterParam uint,
|
}
|
||||||
) ([]EntityName, *metaorm.Pagination, error)
|
|
||||||
|
type WorkspaceMemberLimitRepositoryImpl struct {
|
||||||
|
metaorm.WorkspaceRepositoryImpl[WorkspaceMemberLimit]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Repository Implementation
|
- Must having NewXXXXRepository method.
|
||||||
|
- Must having XXXXRepository interface.
|
||||||
|
- Must having XXXXRepositoryImpl struct.
|
||||||
|
- The XXXX is entity object.
|
||||||
|
|
||||||
Implement the repository with proper query building:
|
## Basic structure of repository for entity without workspace
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type EntityNameRepositoryImpl struct {
|
import "git.metadiv.io/metadiv-studio/metaorm"
|
||||||
metaorm.RepositoryImpl[EntityName]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Constructor Function
|
|
||||||
|
|
||||||
Always provide a constructor function:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func NewEntityNameRepository(db metaorm.Database) EntityNameRepository {
|
|
||||||
return &EntityNameRepositoryImpl{
|
|
||||||
RepositoryImpl: metaorm.NewRepositoryImpl[EntityName](db),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Query Building Patterns
|
|
||||||
|
|
||||||
### 1. Simple Field Queries
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (r *RepositoryImpl) FindByEmail(email string) (*Entity, error) {
|
|
||||||
qb := metaorm.NewQueryBuilder()
|
|
||||||
return r.Query(qb.Field("email").Eq(email)).FindOne()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RepositoryImpl) FindById(id uint) (*Entity, error) {
|
|
||||||
qb := metaorm.NewQueryBuilder()
|
|
||||||
return r.Query(qb.Field("id").Eq(id)).FindOne()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Multiple Field Queries
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (r *RepositoryImpl) FindByUserIdAndWorkspaceId(userId uint, workspaceId uint) (*Entity, error) {
|
|
||||||
qb := metaorm.NewQueryBuilder()
|
|
||||||
return r.
|
|
||||||
Query(qb.And(
|
|
||||||
qb.Field("user_id").Eq(userId),
|
|
||||||
qb.Field("workspace_id").Eq(workspaceId),
|
|
||||||
)).
|
|
||||||
Preload("Workspace").
|
|
||||||
Preload("User").
|
|
||||||
FindOne()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Advanced Filtered Queries (Updated Pattern)
|
|
||||||
|
|
||||||
Use generic interfaces for better flexibility and cleaner code:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (r *RepositoryImpl) FilteredPagedFindMany(
|
|
||||||
page metaorm.PaginationQuery,
|
|
||||||
sort metaorm.SortingQuery,
|
|
||||||
keywordFilter metaorm.Query,
|
|
||||||
workspaceId uint,
|
|
||||||
) ([]Entity, *metaorm.Pagination, error) {
|
|
||||||
qb := metaorm.NewQueryBuilder()
|
|
||||||
queries := make([]metaorm.Query, 0)
|
|
||||||
|
|
||||||
// Base filter
|
|
||||||
queries = append(queries, qb.Field("workspace_id").Eq(workspaceId))
|
|
||||||
|
|
||||||
// Add optional keyword filter
|
|
||||||
if keywordFilter != nil {
|
|
||||||
queries = append(queries, keywordFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.
|
|
||||||
Query(qb.And(queries...)).
|
|
||||||
Joins("User").
|
|
||||||
Preload("Workspace").
|
|
||||||
Pagination(page).
|
|
||||||
Sorting(sort).
|
|
||||||
PagedFindMany()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key improvements in this pattern:**
|
|
||||||
- Use `metaorm.PaginationQuery` and `metaorm.SortingQuery` interfaces instead of concrete types
|
|
||||||
- Accept `metaorm.Query` for keyword filters instead of string + manual query building
|
|
||||||
- Let the caller build the keyword filter query for better separation of concerns
|
|
||||||
- More flexible and reusable approach
|
|
||||||
|
|
||||||
### 4. Special Query Methods
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Unscoped queries (include soft-deleted records)
|
|
||||||
func (r *RepositoryImpl) UnscopedFindByUserEmail(email string, workspaceId uint) (*Entity, error) {
|
|
||||||
qb := metaorm.NewQueryBuilder()
|
|
||||||
return r.
|
|
||||||
Query(qb.And(
|
|
||||||
qb.Field("User.email").Eq(email),
|
|
||||||
qb.Field("workspace_members.workspace_id").Eq(workspaceId),
|
|
||||||
)).
|
|
||||||
Joins("User").
|
|
||||||
Preload("Workspace").
|
|
||||||
Unscoped().
|
|
||||||
FindOne()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find with exclusions
|
|
||||||
func (r *RepositoryImpl) FindAnOtherOwner(workspaceMember *Entity) (*Entity, error) {
|
|
||||||
qb := metaorm.NewQueryBuilder()
|
|
||||||
return r.
|
|
||||||
Query(qb.And(
|
|
||||||
qb.Field("role").Eq(RoleOwner),
|
|
||||||
qb.Field("workspace_id").Eq(workspaceMember.WorkspaceId.Get()),
|
|
||||||
qb.Field("id").Neq(workspaceMember.ID),
|
|
||||||
)).
|
|
||||||
FindOne()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bulk operations
|
|
||||||
func (r *RepositoryImpl) DeleteAllMembersOfWorkspace(workspaceId uint) error {
|
|
||||||
qb := metaorm.NewQueryBuilder()
|
|
||||||
return r.QueriedDelete(qb.Field("workspace_id").Eq(workspaceId))
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Query Building Best Practices
|
|
||||||
|
|
||||||
### 1. Query Builder Usage
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Always start with NewQueryBuilder
|
|
||||||
qb := metaorm.NewQueryBuilder()
|
|
||||||
|
|
||||||
// Simple conditions
|
|
||||||
qb.Field("field_name").Eq(value)
|
|
||||||
qb.Field("field_name").Neq(value)
|
|
||||||
qb.Field("field_name").Similar(keyword) // For LIKE queries
|
|
||||||
|
|
||||||
// Complex conditions
|
|
||||||
qb.And(
|
|
||||||
qb.Field("field1").Eq(value1),
|
|
||||||
qb.Field("field2").Eq(value2),
|
|
||||||
)
|
|
||||||
|
|
||||||
qb.Or(
|
|
||||||
qb.Field("field1").Eq(value1),
|
|
||||||
qb.Field("field2").Eq(value2),
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Relationship Handling
|
|
||||||
|
|
||||||
```go
|
|
||||||
// For joins and preloading
|
|
||||||
return r.
|
|
||||||
Query(qb.Field("id").Eq(id)).
|
|
||||||
Joins("RelatedEntity"). // Join for filtering
|
|
||||||
Preload("RelatedEntity"). // Preload for eager loading
|
|
||||||
FindOne()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Pagination and Sorting
|
|
||||||
|
|
||||||
```go
|
|
||||||
return r.
|
|
||||||
Query(qb.And(queries...)).
|
|
||||||
Joins("User").
|
|
||||||
Preload("Workspace").
|
|
||||||
Pagination(page).
|
|
||||||
Sorting(sort).
|
|
||||||
PagedFindMany()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Method Naming Conventions
|
|
||||||
|
|
||||||
- `FindBy[Field]` - Find single record by field
|
|
||||||
- `FindBy[Field1]And[Field2]` - Find by multiple fields
|
|
||||||
- `FindManyBy[Field]` - Find multiple records
|
|
||||||
- `FilteredPagedFindMany` - Find with pagination and filtering
|
|
||||||
- `UnscopedFindBy[Field]` - Find including soft-deleted records
|
|
||||||
- `DeleteBy[Condition]` - Delete records by condition
|
|
||||||
- `FindAn[Description]` - Find with specific business logic
|
|
||||||
|
|
||||||
## Key Implementation Rules
|
|
||||||
|
|
||||||
1. **Use generic interfaces** - Prefer `metaorm.PaginationQuery`, `metaorm.SortingQuery`, and `metaorm.Query` over concrete types
|
|
||||||
2. **Accept query objects** - Let callers build complex filters and pass them as `metaorm.Query`
|
|
||||||
3. **Always use QueryBuilder** - Never write raw SQL
|
|
||||||
4. **Use proper field names** - Match database column names exactly
|
|
||||||
5. **Handle relationships** - Use Joins for filtering, Preload for data
|
|
||||||
6. **Include comprehensive comments** - Document complex business logic
|
|
||||||
7. **Follow naming conventions** - Consistent method naming
|
|
||||||
8. **Return proper types** - Use entity types, not DTOs in repositories
|
|
||||||
9. **Handle errors properly** - Let MetaORM errors bubble up
|
|
||||||
|
|
||||||
## Example: Complete Repository
|
|
||||||
|
|
||||||
```go
|
|
||||||
func NewUserRepository(db metaorm.Database) UserRepository {
|
func NewUserRepository(db metaorm.Database) UserRepository {
|
||||||
return &UserRepositoryImpl{
|
return &UserRepositoryImpl{
|
||||||
RepositoryImpl: metaorm.NewRepositoryImpl[User](db),
|
RepositoryImpl: metaorm.NewRepositoryImpl[User](db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserRepository interface {
|
type UserRepository interface {
|
||||||
metaorm.Repository[User]
|
metaorm.Repository[User]
|
||||||
FindByEmail(email string) (*User, error)
|
|
||||||
FindByUsername(username string) (*User, error)
|
|
||||||
FilteredPagedFindMany(
|
|
||||||
page metaorm.PaginationQuery,
|
|
||||||
sort metaorm.SortingQuery,
|
|
||||||
keywordFilter metaorm.Query,
|
|
||||||
active bool,
|
|
||||||
) ([]User, *metaorm.Pagination, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserRepositoryImpl struct {
|
type UserRepositoryImpl struct {
|
||||||
metaorm.RepositoryImpl[User]
|
metaorm.RepositoryImpl[User]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UserRepositoryImpl) FindByEmail(email string) (*User, error) {
|
```
|
||||||
qb := metaorm.NewQueryBuilder()
|
|
||||||
return r.Query(qb.Field("email").Eq(email)).FindOne()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *UserRepositoryImpl) FilteredPagedFindMany(
|
- Must having NewXXXXRepository method.
|
||||||
page metaorm.PaginationQuery,
|
- Must having XXXXRepository interface.
|
||||||
sort metaorm.SortingQuery,
|
- Must having XXXXRepositoryImpl struct.
|
||||||
keywordFilter metaorm.Query,
|
- The XXXX is entity object.
|
||||||
active bool,
|
|
||||||
) ([]User, *metaorm.Pagination, error) {
|
|
||||||
qb := metaorm.NewQueryBuilder()
|
|
||||||
queries := make([]metaorm.Query, 0)
|
|
||||||
|
|
||||||
queries = append(queries, qb.Field("active").Eq(active))
|
|
||||||
if keywordFilter != nil {
|
|
||||||
queries = append(queries, keywordFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.
|
|
||||||
Query(qb.And(queries...)).
|
|
||||||
Pagination(page).
|
|
||||||
Sorting(sort).
|
|
||||||
PagedFindMany()
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user