Database Integration
Noventa is deeply integrated with SQLAlchemy to provide a robust and flexible way to interact with your database.
Using the Database Session
Whenever you need to interact with the database within a component's logic, you can use the db object that is automatically passed to your handlers (both load_template_context and action_* functions). This object is a standard Session object from SQLAlchemy.
You can use this session to query, add, update, and delete records as you would with any SQLAlchemy application.
# Example: Querying for a user in a component
from components.user.user_models import User
def load_template_context(request, session, db, **props):
user_id = request.args.get("id")
user = db.query(User).filter(User.id == user_id).first()
return {"user": user}
def action_update_user(request, session, db, **props):
user_id = request.form.get("user_id")
user = db.query(User).filter(User.id == user_id).first()
if user:
user.name = request.form.get("name")
db.commit()
return {"message": "User updated!"}
Configuration
By default, Noventa is configured to use a local SQLite database. This is defined in the config.yaml file at the root of your project.
database: "sqlite:///./noventa.db"
While SQLite is great for development and simpler applications, Noventa supports any database that is compatible with SQLAlchemy. To switch to another database like PostgreSQL or MySQL, you simply need to update the database configuration in config.yaml with the appropriate connection details.
Defining Models
Noventa uses SQLAlchemy's modern DeclarativeBase approach for defining database models. Models can be placed in two locations:
Component-Specific Models
For models used only within a specific component, create [component_name]_models.py in the component's folder:
# components/user/user_models.py
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(unique=True)
name: Mapped[str] = mapped_column()
Global Models
For models used across multiple components, place them in ./models/ directory:
# models/global_models.py
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class SystemSetting(Base):
__tablename__ = "system_settings"
id: Mapped[int] = mapped_column(primary_key=True)
key: Mapped[str] = mapped_column(unique=True)
value: Mapped[str] = mapped_column()
Database Migrations with Alembic
Noventa includes Alembic for database schema management. Migrations are automatically generated from your model definitions.
Running Migrations
Alembic is pre-configured and detects models in ./components/ and ./models/ directories. Use these commands from your project root:
# Generate a new migration after changing models
alembic -c migrations/alembic.ini revision --autogenerate -m "Add user table"
# Apply migrations to database
alembic -c migrations/alembic.ini upgrade head
# Rollback last migration
alembic -c migrations/alembic.ini downgrade -1
Migration Files
Migration files are stored in /migrations/versions/ and contain the SQL commands to upgrade or downgrade your database schema.
Database Seeding
After creating tables, populate initial data using seed scripts in ./migrations/seed/:
# migrations/seed/initial_users.py
from sqlalchemy.orm import Session
from components.user.user_models import User
def seed_users(db: Session):
# Create initial users
admin = User(email="admin@example.com", name="Admin")
db.add(admin)
db.commit()
Run seed scripts after migrations to populate your database with initial data.
Best Practices
- Use
Mappedtype annotations for better IDE support - Define relationships explicitly between models
- Keep component models in their respective folders when possible
- Always run migrations after model changes
- Use seed scripts for initial/test data