4.8 KiB
PostgreSQL Repository Refactoring Summary
Overview
The PostgreSQL repository implementation has been refactored to use generics and traits for commonality, similar to the approach used in the memory crate. This eliminates code duplication while maintaining static dispatch and type safety.
What Changed
Before: Duplicated Repository Implementations
Previously, there were two separate repository structs:
PostgresUserRepository
- ~150 lines of codePostgresProductRepository
- ~150 lines of code
Both implemented the same CRUD operations with nearly identical patterns, differing only in:
- Table names (
users
vsproducts
) - Column names (
username, email
vsname, description
) - SQL queries
- Entity construction
After: Generic Repository with Traits
Now there's a single generic repository:
PostgresRepository<T>
- Generic implementationPostgresEntity
trait - Abstracts entity-specific behavior- Type aliases for backward compatibility
Key Components
1. PostgresEntity Trait
pub trait PostgresEntity: Entity + Send + Sync {
fn id(&self) -> Uuid;
fn table_name() -> &'static str;
fn entity_name() -> &'static str;
fn from_db_record(record: Self::DbRecord) -> Self;
fn select_columns() -> &'static str;
fn insert_columns() -> &'static str;
fn insert_placeholders() -> &'static str;
fn update_set_clause() -> &'static str;
fn extract_create_values(data: &Self::Create) -> Vec<String>;
fn extract_update_values(data: &Self::Update) -> Vec<Option<String>>;
type DbRecord: Send + Sync + Unpin + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>;
}
2. Generic PostgresRepository
pub struct PostgresRepository<T> {
pool: PgPool,
_phantom: std::marker::PhantomData<T>,
}
impl<T: PostgresEntity> Repository<T> for PostgresRepository<T> {
// Generic CRUD implementations
}
3. Database Record Types
#[derive(sqlx::FromRow)]
pub struct UserRecord {
pub id: Uuid,
pub username: String,
pub email: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(sqlx::FromRow)]
pub struct ProductRecord {
pub id: Uuid,
pub name: String,
pub description: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
Benefits
1. Code Reuse
- Eliminated ~150 lines of duplicated code
- Single implementation handles all entity types
- Consistent behavior across all entities
2. Maintainability
- Bug fixes and improvements apply to all entities
- Single place to update SQL patterns
- Consistent error handling
3. Extensibility
- Adding new entities requires minimal code
- Just implement
PostgresEntity
trait and define record type - No need to duplicate CRUD logic
4. Type Safety
- Maintains static dispatch
- Compile-time guarantees
- No runtime overhead
5. Backward Compatibility
- Type aliases preserve existing API
- No breaking changes to consumers
- Gradual migration possible
Adding New Entities
To add a new entity (e.g., Order
):
- Define the database record type:
#[derive(sqlx::FromRow)]
pub struct OrderRecord {
pub id: Uuid,
pub customer_id: Uuid,
pub total_amount: Decimal,
pub status: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
- Implement PostgresEntity:
impl PostgresEntity for Order {
fn table_name() -> &'static str { "orders" }
fn select_columns() -> &'static str { "customer_id, total_amount, status, created_at, updated_at" }
// ... other required methods
type DbRecord = OrderRecord;
}
- Create type aliases:
pub type PostgresOrderRepository = PostgresRepository<Order>;
pub type PostgresOrderService = Service<Order, PostgresOrderRepository>;
That's it! The generic repository handles all CRUD operations automatically.
Performance Impact
- Zero runtime overhead - All generics are resolved at compile time
- Static dispatch - No virtual function calls
- Same SQL performance - Queries are identical to before
- Connection pooling - Unchanged
Testing
All existing tests continue to pass:
- 27 PostgreSQL repository tests
- 64 integration tests
- Full workspace compilation
Migration Path
The refactoring is fully backward compatible:
- Existing code continues to work unchanged
- Type aliases preserve the old names
- No migration required for consumers
Future Considerations
This pattern can be extended to support:
- More complex SQL operations (joins, aggregations)
- Different database backends (MySQL, SQLite)
- Custom query builders
- Advanced filtering and pagination
The generic approach provides a solid foundation for future enhancements while maintaining simplicity and type safety.