Skip to content

Database & ORM

Framefox provides a sophisticated ORM (Object-Relational Mapping) system built on top of SQLModel and SQLAlchemy 2.0. The framework integrates a powerful QueryBuilder, advanced Entity Manager with identity mapping, and a comprehensive driver system supporting multiple database engines.

The ORM architecture emphasizes developer productivity through fluent query interfaces, automatic change tracking, and seamless database abstraction. Whether you’re building a simple application with SQLite or a complex enterprise system with PostgreSQL, Framefox adapts to your needs without code modifications.

Framefox uses a sophisticated connection management system through the ConnectionManager class that handles database driver initialization, connection pooling, and lifecycle management.

Framefox supports multiple database engines through a flexible configuration system. When you initialize a new project with framefox init, the framework automatically generates the necessary configuration files for database setup.

The primary configuration method uses environment variables in the .env file, which is ideal for production environments where credentials are stored as secrets:

# .env (generated by framefox init)
#==============================
# Database
#==============================
# SQLite (default for development)
DATABASE_URL=sqlite:///app.db
# PostgreSQL (recommended for production)
# DATABASE_URL=postgresql://root:password@localhost:5432/dbname
# MySQL/MariaDB
# DATABASE_URL=mysql://root:password@localhost:3306/dbname

This approach prioritizes security by keeping sensitive database credentials out of your codebase and version control.

Driver: Built-in Python support (no additional installation required)

  • Development: Perfect for local development and testing
  • Production: Suitable for small to medium applications
  • Features: Zero-configuration, file-based, ACID compliant
  • Limitations: No concurrent writes, limited scalability
.env
DATABASE_URL=sqlite:///app.db
# Absolute path
DATABASE_URL=sqlite:///var/data/myapp.db
# In-memory (testing only)
DATABASE_URL=sqlite:///:memory:

Driver: psycopg2 or asyncpg (for async operations)

  • Installation: pip install psycopg2-binary or pip install asyncpg
  • Production: Excellent for high-performance applications
  • Features: Full ACID compliance, advanced indexing, JSON support
.env
DATABASE_URL=postgresql://username:password@localhost:5432/database_name
# With SSL (production)
DATABASE_URL=postgresql://username:password@host:5432/db?sslmode=require
# Cloud providers (example)
DATABASE_URL=postgresql://user:pass@cloud-host:5432/db?sslmode=require

Driver: mysqlclient or PyMySQL

  • Installation: pip install mysqlclient or pip install PyMySQL
  • Production: Solid choice for web applications
  • Features: High performance, replication support, wide hosting support
.env
DATABASE_URL=mysql://username:password@localhost:3306/database_name
# With PyMySQL driver (more compatible)
DATABASE_URL=mysql+pymysql://username:password@localhost:3306/database_name
# With SSL and charset
DATABASE_URL=mysql://user:pass@host:3306/db?charset=utf8mb4&ssl_ca=/path/to/ca.pem

For more advanced database settings, Framefox provides the config/orm.yaml file with detailed connection parameters:

# config/orm.yaml (generated by framefox init)
database:
# Primary configuration - uses environment variable
url: "${DATABASE_URL}"
# Detailed configuration (used when DATABASE_URL is not available)
# Useful for Docker/Kubernetes environments or complex setups
# driver: "${DATABASE_DRIVER:-postgresql}"
# host: "${DATABASE_HOST:-localhost}"
# port: "${DATABASE_PORT:-5432}"
# username: "${DATABASE_USER:-framefox}"
# password: "${DATABASE_PASSWORD}"
# database: "${DATABASE_NAME:-framefoxdb}"
# charset: "utf8mb4"
# Connection pooling settings (important for production)
pool_size: 20
max_overflow: 10
pool_timeout: 30
pool_recycle: 1800
pool_pre_ping: true
autocommit: false
autoflush: false
logging:
echo_sql: false # Set to true for SQL debugging

Framefox follows this configuration priority order:

  1. DATABASE_URL environment variable (highest priority)
  2. url field in orm.yaml
  3. Detailed configuration fields in orm.yaml (fallback)

The commented detailed configuration is useful when:

  • Container environments where individual environment variables are easier to manage
  • Service discovery where host/port are dynamically assigned
  • Complex authentication requiring separate credential management
  • Multi-database setups where URL construction is complex

Example for containerized environments:

config/orm.yaml
database:
# Comment out the url line to use detailed config
# url: "${DATABASE_URL}"
# Uncomment and use environment variables
driver: "${DATABASE_DRIVER:-postgresql}"
host: "${DATABASE_HOST:-db-service}"
port: "${DATABASE_PORT:-5432}"
username: "${DATABASE_USER}"
password: "${DATABASE_PASSWORD}"
database: "${DATABASE_NAME}"

The connection pool settings in orm.yaml control database performance:

  • pool_size: Number of persistent connections to maintain
  • max_overflow: Additional connections beyond pool_size when needed
  • pool_timeout: Seconds to wait for an available connection
  • pool_recycle: Seconds before recreating connections (prevents timeout)
  • pool_pre_ping: Test connections before use (prevents stale connections)

Once your database configuration is set, create the database using the CLI command:

Terminal window
framefox database create

This command:

  • Reads your database configuration from .env and orm.yaml
  • Creates the database if it doesn’t exist (for PostgreSQL/MySQL)
  • Verifies the connection and reports success or failure
  • Sets up the Alembic version table for migration tracking

Output examples:

Terminal window
# Success
Database 'myapp_db' created successfully
# Already exists
The database 'myapp_db' already exists
# Error
Error while creating the database: connection refused

To completely remove a database and all its data, use the drop command:

Terminal window
framefox database drop

This command:

  • Displays database information (type and name) before deletion
  • Requires confirmation to prevent accidental data loss
  • Checks database existence and warns if already absent
  • Completely removes the database and all its tables/data

Example interaction:

Terminal window
Database type: postgresql
Database name: myapp_db
⚠️ WARNING: You are about to drop the database. All data will be lost.
Do you want to continue? [y/N]: y
Database dropped successfully: myapp_db

Data Loss Warning

The drop command permanently deletes:

  • All database tables and their data
  • All migration history and schema information
  • All indexes, constraints, and triggers
  • The database itself (PostgreSQL/MySQL)

For SQLite, the entire database file is deleted. This operation cannot be undone.

An entity represents a data model in your application - a Python class that maps directly to a database table. In the MVC (Model-View-Controller) architectural pattern, entities serve as the Model layer, encapsulating the structure, validation rules, and relationships of your data.

Think of an entity as a blueprint for your data. When you create a User entity, you’re defining what a user looks like in your database - it has a username, email, password, and relationships to other entities like games or profiles.

Framefox entities are SQLModel classes that represent database tables. They combine SQLAlchemy’s ORM capabilities with Pydantic’s validation and serialization features.

In Framefox, entities must inherit from AbstractEntity which provides essential framework features:

from sqlmodel import Field, Relationship
from framefox.core.orm.abstract_entity import AbstractEntity
class User(AbstractEntity, table=True):
id: int | None = Field(default=None, primary_key=True)
username: str = Field(max_length=50, unique=True)
email: str = Field(max_length=255, unique=True)
# Relationships
games: list["Game"] = Relationship(back_populates="user")
class Game(AbstractEntity, table=True):
id: int | None = Field(default=None, primary_key=True)
title: str = Field(max_length=100)
user_id: int | None = Field(foreign_key="user.id", nullable=False)
# Relationships
user: User | None = Relationship(back_populates="games")
  • Type Safety: Full Python type hints with Pydantic validation
  • Relationships: Foreign keys and navigation properties
  • Validation: Automatic data validation on creation and updates
  • Serialization: JSON serialization for API responses
  • Metadata: Table names, indexes, and constraints

Framefox provides a command-line tool to generate entities interactively:

Terminal window
framefox create entity

This command:

  • Prompts for entity name as the first question
  • Creates the entity file in src/entity/{name}.py
  • Interactive prompts for adding properties (name, type, constraints)
  • Generates relationships with other entities
  • Handles foreign keys and bidirectional relations automatically
  • Uses modern type syntax with | None for optional fields

Example interaction:

Terminal window
What is the entity name? user
Property name: username
Property type: str
Max length (optional): 50
Unique constraint? (y/N): y
Property name: email
Property type: str
Max length (optional): 255
Unique constraint? (y/N): y
Property name: (press enter to finish)
Entity 'User' created successfully at src/entity/user.py

Generated code:

from sqlmodel import Field
from framefox.core.orm.abstract_entity import AbstractEntity
class User(AbstractEntity, table=True):
id: int | None = Field(default=None, primary_key=True)
username: str = Field(max_length=50, unique=True)
email: str = Field(max_length=255, unique=True)

When you create an entity using the CLI, Framefox automatically generates a corresponding repository in src/repository/. For example, creating a Game entity generates src/repository/game_repository.py:

from framefox.core.orm.abstract_repository import AbstractRepository
from src.entity.game import Game
""" Available Methods:
find(id) # Retrieve entity by ID
find_all() # Retrieve all entities
find_by(criteria, order_by=None, limit=None, offset=None) # Retrieve entities by criteria
get_query_builder() # Get QueryBuilder instance for complex queries
Example:
game = game_repo.find(1)
games = game_repo.find_all()
user_games = game_repo.find_by({"user_id": 1})
"""
class GameRepository(AbstractRepository):
def __init__(self):
super().__init__(Game)
###########
# Build your own queries using the QueryBuilder
###########
# def get_games_by_user(self, user_id: int):
# query_builder = self.get_query_builder()
# return (
# query_builder
# .select()
# .where(self.model.user_id == user_id)
# .all()
# )

For rapid prototyping or when you need to create database tables directly from your entity definitions without using migrations, Framefox provides the copy command:

Terminal window
framefox database copy

This command:

  • Scans the src/entity/ directory for SQLModel entity classes
  • Creates database tables directly from entity definitions
  • Skips the migration system for direct table creation
  • Requires database to exist first (use framefox database create)
  • Useful for development and prototyping scenarios

Example workflow:

Terminal window
# First, ensure your database exists
framefox database create
# Then copy your entity definitions to database tables
framefox database copy

Output:

Terminal window
Scanning entities in src/entity/...
Found SQLModel classes: User, Game, Profile
Creating tables from entity definitions...
Tables created successfully.
You should try framefox create crud

The copy command is particularly useful during early development when you’re still iterating on your entity designs and don’t need the overhead of migration management.

Framefox repositories provide a clean abstraction layer over database operations. All repositories inherit from AbstractRepository, which provides core database operations and QueryBuilder integration.

As shown in the entity creation section above, repositories are automatically generated when you create entities using the CLI. The generated repository follows the correct Framefox pattern.

Every repository inherits from AbstractRepository and follows this pattern:

from framefox.core.orm.abstract_repository import AbstractRepository
from src.entity.user import User
class UserRepository(AbstractRepository):
def __init__(self):
super().__init__(User) # Pass the entity class to the parent constructor
# Custom repository methods
def find_active_users(self) -> list[User]:
return self.find_by({"is_active": True})
def find_by_email(self, email: str) -> User | None:
return self.find_one_by({"email": email})

Every repository automatically provides these methods:

# Basic CRUD operations
user = user_repo.find(1) # Find by ID
users = user_repo.find_all() # Get all entities
active_users = user_repo.find_by({"is_active": True}) # Find with criteria
user = user_repo.find_one_by({"email": "test@example.com"}) # Find single entity
# Advanced queries
query_builder = user_repo.get_query_builder() # Get QueryBuilder for complex queries

Repositories are automatically registered through the dependency injection system when the application starts. Simply create your repository class and it becomes available for injection in controllers and services.

The EntityManager is the central coordinator for all database operations and entity lifecycle management in Framefox’s ORM architecture. Acting as an implementation of the Unit of Work pattern, it tracks changes to entities throughout their lifecycle and ensures that database operations are executed efficiently and safely within transactional boundaries.

The EntityManager operates on the principle of dirty checking - it knows exactly which properties of which entities have been modified since they were loaded from the database. This allows it to generate optimal UPDATE statements that only modify the changed fields, rather than updating entire records unnecessarily.

Think of the EntityManager as your application’s database session manager and transaction coordinator. It maintains an internal state of all entities you’re working with, tracks their changes, and provides intelligent batching of database operations. When you retrieve an entity from the database, modify it, and then commit your changes, the EntityManager handles all the complex SQL generation, change detection, and database synchronization behind the scenes.

The EntityManager provides several key capabilities:

  • Identity Map: Ensures each entity is only loaded once per session, preventing duplicate objects
  • Change Tracking: Automatically detects modifications to entities using dirty checking
  • Transaction Management: Handles database transactions with automatic rollback on exceptions
  • Request-Scoped Lifecycle: Each HTTP request gets its own EntityManager instance
  • Session Management: Manages SQLAlchemy sessions with proper cleanup
  • Repository Integration: Works seamlessly with repository pattern

Framefox provides two ways to work with the EntityManager:

The EntityManagerInterface is the recommended approach for most use cases:

from framefox.core.orm.entity_manager_interface import EntityManagerInterface
class UserService:
def __init__(self, entity_manager: EntityManagerInterface):
self.em = entity_manager # Interface - can be injected in constructor
def create_user(self, user_data: dict) -> User:
user = User(**user_data)
self.em.persist(user)
self.em.commit()
return user

Benefits of EntityManagerInterface:

  • Constructor injection supported: Can be injected in controller/service constructors
  • Context-aware: Automatically resolves to the correct EntityManager instance
  • Request lifecycle managed: Handles request-scoped EntityManager lifecycle
  • Testing friendly: Easy to mock and test
  • Thread-safe: Ensures proper isolation between execution contexts

The concrete EntityManager class can only be used via method-level autowiring:

from framefox.core.orm.entity_manager import EntityManager
from framefox.core.routing.decorator.route import Route
class UserController:
# ❌ INCORRECT: Cannot inject EntityManager in constructor
# def __init__(self, entity_manager: EntityManager):
# self.entity_manager = entity_manager
@Route("/users", "create_user", ["POST"])
async def create_user(self, user_data: dict, entity_manager: EntityManager):
# βœ… CORRECT: EntityManager autowired in method parameters
user = User(**user_data)
entity_manager.persist(user)
entity_manager.commit()
return user

Why method injection only?

  • Request-scoped lifecycle: EntityManager instances are tied to specific HTTP requests
  • Middleware dependency: Created and managed by EntityManagerMiddleware
  • Session isolation: Each request needs its own isolated database session
# Create and persist entities
user = User(username="john", email="john@example.com")
entity_manager.persist(user)
entity_manager.commit()
# Find entities by primary key
user = entity_manager.find(User, 1)
user = entity_manager.find(User, {"id": 1}) # Alternative syntax
# Check if entity exists in database
existing_user = entity_manager.find_existing_entity(user)
# Delete entities
entity_manager.delete(user)
entity_manager.commit()
# Refresh entity from database
entity_manager.refresh(user) # Reloads from DB, discarding local changes
# Explicit transaction control
try:
with entity_manager.transaction():
user = User(username="john")
entity_manager.persist(user)
game = Game(title="My Game", user_id=user.id)
entity_manager.persist(game)
# Transaction auto-commits on successful completion
# Auto-rollback on exception
except Exception as e:
# Transaction already rolled back
print(f"Transaction failed: {e}")
# Manual transaction control
entity_manager.persist(user)
entity_manager.commit() # Manual commit
# Or rollback if needed
entity_manager.rollback()
# Execute raw SQL statements
from sqlmodel import text
results = entity_manager.exec_statement(
text("SELECT * FROM users WHERE active = :active")
.params(active=True)
)
# Get repository for entity
user_repo = entity_manager.get_repository(User)
active_users = user_repo.find_by({"is_active": True})
# Session access for complex operations
session = entity_manager.session
complex_query = session.query(User).join(Game).filter(...)

The EntityManager maintains an identity map that ensures each database record is represented by exactly one object instance within a session:

# Both calls return the same object instance
user1 = entity_manager.find(User, 1)
user2 = entity_manager.find(User, 1)
assert user1 is user2 # True - same object reference
# Changes are automatically tracked
user1.username = "new_username"
entity_manager.commit() # UPDATE users SET username='new_username' WHERE id=1
# No explicit save() needed - changes detected automatically
from framefox.core.orm.entity_manager_interface import EntityManagerInterface
class GameService:
def __init__(self, entity_manager: EntityManagerInterface):
self.em = entity_manager
def create_game_for_user(self, game_data: dict, user_id: int) -> Game:
try:
with self.em.transaction():
# Create game
game = Game(**game_data)
game.user_id = user_id
self.em.persist(game)
return game
except Exception as e:
# Transaction automatically rolled back
raise RuntimeError(f"Failed to create game: {str(e)}")
from framefox.core.routing.decorator.route import Route
from framefox.core.orm.entity_manager import EntityManager
class GameController:
@Route("/games/{game_id}", "update_game", ["PUT"])
async def update_game(self, game_id: int, game_data: dict,
entity_manager: EntityManager):
game = entity_manager.find(Game, game_id)
if not game:
raise HTTPException(404, "Game not found")
# Update game properties
for field, value in game_data.items():
if hasattr(game, field):
setattr(game, field, value)
# Changes automatically detected and committed by middleware
return game

Injection Limitations

EntityManager (concrete class):

  • ❌ Cannot be injected in constructor (__init__)
  • βœ… Can be autowired in controller method parameters
  • βœ… Managed by request lifecycle middleware

EntityManagerInterface:

  • βœ… Can be injected in constructor (__init__)
  • βœ… Can be autowired in controller method parameters
  • βœ… Provides context-aware EntityManager resolution

Database migrations provide version control for your database schema, allowing you to evolve your database structure systematically over time. Framefox uses Alembic, the migration tool built by the SQLAlchemy team, to handle all schema changes automatically.

Framefox follows a streamlined migration workflow:

  1. Modify Entities: Make changes to your entity classes
  2. Generate Migration: Create migration files automatically
  3. Review Migration: Examine the generated migration file for accuracy
  4. Apply Migration: Update the database schema
  5. Version Control: Commit migration files to your repository

Generate a new migration file based on entity changes:

Terminal window
framefox database create-migration

This command:

  • Compares current entities with the existing database schema
  • Auto-generates migration code with timestamp-based filename
  • Creates migrations/versions/ directory if it doesn’t exist
  • Detects schema changes (tables, columns, indexes, constraints)
  • Removes empty migrations if no changes are detected

Output examples:

Terminal window
# Changes detected
Migration created: migrations/versions/20240620_143025_migration.py
Migration file generated successfully
# No changes
No changes detected since the last migration.
# Database doesn't exist
The database does not exist. Please run 'framefox database create' first.

Generated migration file:

migrations/versions/20240620_143025_migration.py
"""Migration
Revision ID: 20240620_143025
Revises:
Create Date: 2024-06-20 14:30:25.000000
"""
from alembic import op
import sqlalchemy as sa
def upgrade():
"""Add new columns and constraints"""
op.add_column('users', sa.Column('avatar_url', sa.String(255), nullable=True))
op.create_index('ix_users_avatar_url', 'users', ['avatar_url'])
def downgrade():
"""Remove added columns and constraints"""
op.drop_index('ix_users_avatar_url', table_name='users')
op.drop_column('users', 'avatar_url')

Apply pending migrations to update the database schema:

Terminal window
framefox database upgrade

This command:

  • Checks database existence and creates it if necessary
  • Applies all pending migrations in chronological order
  • Updates the alembic_version table with current migration state
  • Handles migration dependencies automatically
  • Reports success or failure with detailed information

Output examples:

Terminal window
# Success
Running upgrade -> ae1027a6acf, add user avatar column
Migration completed successfully
Current revision: ae1027a6acf
# Already up to date
Database is already up to date
# Error
Error during upgrade: column "avatar_url" already exists

View the current migration status and history:

Terminal window
framefox database status

This command provides:

  • Database connection status and configuration details
  • Current migration revision and timestamp
  • List of all migrations with applied/pending status
  • Migration file details and descriptions

Output example:

Terminal window
Database Status
Database Configuration:
URL: sqlite:///app.db
Status: Connected βœ“
Current Revision: ae1027a6acf (head)
Migration History:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Revision β”‚ Status β”‚ Description β”‚ Date ┃
┑━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
β”‚ ae1027a6acf β”‚ APPLIED β”‚ add user avatar column β”‚ 2024-06-20 14:30 β”‚
β”‚ d4e5f6a7b8c9 β”‚ APPLIED β”‚ create initial tables β”‚ 2024-06-18 09:15 β”‚
β”‚ base β”‚ APPLIED β”‚ initial migration β”‚ 2024-06-18 09:00 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Revert to a previous migration version:

Terminal window
# Downgrade one step
framefox database downgrade
# Downgrade to specific revision
framefox database downgrade --revision d4e5f6a7b8c9
# Downgrade to base (remove all migrations)
framefox database downgrade --revision base

This command:

  • Reverts schema changes in reverse chronological order
  • Executes downgrade functions from migration files
  • Updates migration tracking to the target revision
  • Preserves data when possible (depends on migration design)

Generate a visual representation of your database schema:

Terminal window
framefox database diagram

This command:

  • Analyzes current database schema including tables, columns, and relationships
  • Generates Mermaid diagram code for visualization
  • Creates diagram files in multiple formats (.md, .mmd)
  • Includes entity relationships and foreign key constraints
  • Saves output to docs/database_schema.md by default

Output example:

Terminal window
Database diagram generated successfully
Output files:
- docs/database_schema.md (Markdown with Mermaid)
- docs/database_schema.mmd (Mermaid source)
Entities included: User, Game, Profile
Relationships mapped: 3

Framefox provides several maintenance commands to help resolve database and migration issues during development.

When you need to reset your migration history and start fresh:

Terminal window
framefox database clear-migration

This command:

  • Removes all migration files from the migrations/versions/ directory
  • Clears the alembic_version table in the database
  • Preserves essential files like env.py and script.py.mako
  • Recreates directory structure for new migrations
  • Checks database existence before attempting to clear the version table

Example output:

Terminal window
Clearing migration files and database references...
Migration table cleared
Migration files cleared
Migration files and database references cleared successfully
You can now create new migrations with 'framefox database create-migration'

Use cases:

  • Development reset: When prototyping and need to start migration history over
  • Problematic migrations: When migration conflicts or corruption occur
  • Major refactoring: When entity structure changes significantly
  • Team synchronization: When merging conflicting migration branches

Development Only

The clear-migration command should only be used in development environments. In production:

  • Migration history is crucial for database integrity
  • Use proper rollback strategies instead
  • Coordinate with your team before clearing migrations

When you encounter mapper or metadata conflicts:

Terminal window
framefox database clear-metadata

This command:

  • Clears SQLAlchemy registry and removes stale mappers
  • Resets SQLModel metadata to prevent conflicts
  • Resolves entity mapping issues caused by code changes
  • Clears Python module cache for clean reloading
  • Requires application restart to take effect

Example output:

Terminal window
Cleaning SQLAlchemy metadata...
SQLModel.metadata cleaned
SQLModel registry cleaned
Metadata cleaned successfully
Restart your application to apply the changes

Common scenarios:

  • Entity class modifications: After changing entity definitions
  • Import conflicts: When entity imports cause circular dependencies
  • Mapper errors: When SQLAlchemy complains about duplicate mappings
  • Development environment: After major code restructuring

For comprehensive troubleshooting during development:

Terminal window
# Clear all migrations and metadata
framefox database clear-migration
framefox database clear-metadata
# Recreate database schema from current entities
framefox database copy
# Or create a fresh migration
framefox database create-migration "initial_schema"

πŸ”§ Advanced Configuration: Production Database Setup - How to configure connection pooling and SSL for production?

πŸ”— Advanced Relationships: Complex Entity Patterns - Need help with polymorphic associations and inheritance?

πŸ” Query Builder: Advanced Queries & Repository Patterns - Want to master complex queries and performance optimization?

⚑ Entity Manager: Transactions & Performance - Ready to explore transaction management and batch operations?

πŸ“¦ Migration Management: Schema Evolution Strategies - Looking for advanced migration techniques and deployment strategies?