Async SQLAlchemy Repositories with HexCore Framework
Learn how to build async SQLAlchemy repositories with HexCore’s SqlAlchemyRepository, covering models, field resolvers, serializers, and CRUD operations.
Use this file to discover all available pages before exploring further.
HexCore’s SqlAlchemyRepository (also exported as SQLAlchemyCommonImplementationsRepo) is a generic base class that wires your SQLAlchemy async models to domain entities. This guide covers defining your ORM model, implementing a concrete repository, configuring field resolvers and serializers for complex types, and running migrations — all the way from an empty table to a working end-to-end flow.
Make sure async_sql_database_url is set in your ServerConfig:
config.py
from hexcore.config import ServerConfigconfig = ServerConfig( async_sql_database_url="postgresql+asyncpg://user:pass@localhost/mydb", sql_database_url="postgresql://user:pass@localhost/mydb", # used by Alembic repository_discovery_paths={ "src.infrastructure.repositories", },)
Abstract SQLAlchemy declarative base. All ORM models must subclass it. Provides id (UUID PK), is_active (bool), created_at, and updated_at columns automatically.
SqlAlchemyRepository
Generic CRUD repository. Subclass it with your entity type T and model type M. Provides get_by_id, list_all, query_all, save, and delete.
Import BaseModel and Base from HexCore’s SQLAlchemy ORM module. Your model extends BaseModel[YourEntity] and declares the __tablename__ plus any extra columns.
Subclass SqlAlchemyRepository[Entity, Model] and declare the four required properties: entity_cls, model_cls, not_found_exception, and uow (received via constructor).
Returns all records. limit_offset_pagination defaults to True, so limit and offset are applied automatically. Set it to False on the repository class to always return every row.
When a column stores a raw primitive but your entity field is a rich type (e.g. an Enum, a nested entity, or a value object), use fields_resolvers and fields_serializers to bridge the gap.
Resolvers run after the model row is loaded and translate raw column values into entity-friendly types. Each entry maps an entity field name to a (source_column, async_resolver_fn) tuple. The resolver receives the full model instance.
Serializers run before the entity is persisted and convert complex entity fields into column-compatible primitives. Each entry maps an entity field to a (dest_column, sync_serializer_fn) tuple. The serializer receives the full entity.
Use the @cycle_protection_resolver decorator on resolver functions that navigate relationships. It automatically breaks circular reference chains by caching already-visited entity IDs in a context variable.
limit_offset_pagination defaults to True, so list_all respects limit and offset out of the box. To disable pagination and always return every row, set it to False in the repository class body:
class UserRepository(SqlAlchemyRepository[User, UserModel], IUserRepository): limit_offset_pagination = False ...
HexCore manages the async SQLAlchemy engine and session factory lazily. You can access them directly if needed — for example, in FastAPI dependency injection:
src/infrastructure/database/session.py
from hexcore.infrastructure.repositories.orms.sqlalchemy.session import ( get_async_db_session, get_engine, get_session_factory, reset_sqlalchemy_engine,)# Use as a FastAPI dependency:# async def get_db():# async for session in get_async_db_session():# yield session
In long-running worker processes (RabbitMQ, Celery), call await reset_sqlalchemy_engine() at startup to rebind the connection pool to the current asyncio event loop and avoid “Task attached to a different loop” errors.
Use import_all_models from HexCore’s SQLAlchemy utilities to ensure Alembic discovers every model before generating migrations:
alembic/env.py
from alembic import contextfrom hexcore.config import LazyConfigfrom hexcore.infrastructure.repositories.orms.sqlalchemy import Basefrom hexcore.infrastructure.repositories.orms.sqlalchemy.utils import import_all_modelsimport src.infrastructure.database.models as models# Auto-import every model module so Alembic sees all metadataimport_all_models(models)# Tell Alembic which metadata to diff againsttarget_metadata = Base.metadata# Pull the sync DB URL from configconfig = context.configdatabase_url = LazyConfig.get_config().sql_database_urlif database_url: config.set_main_option("sqlalchemy.url", database_url)