Database & Models

Chapter 4: Storing and retrieving data with SQLAlchemy.

Most apps need to store data. In sparQ, you define your data structures using SQLAlchemy models. Let's learn how to create models and work with the database.

What is a Model?

A model is a Python class that represents a database table. Each instance of the class is a row in that table. SQLAlchemy handles all the SQL for you.

# This Python class...
class Task(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200))
    done = db.Column(db.Boolean, default=False)

# ...becomes this database table
# CREATE TABLE task (
#     id INTEGER PRIMARY KEY,
#     title VARCHAR(200),
#     done BOOLEAN
# )

Creating Your First Model

Models go in the models/ folder of your app. Let's create a Task model:

# models/task.py
from system.db import db

class Task(db.Model):
    __tablename__ = 'myapp_tasks'  # Name your table with app prefix

    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    description = db.Column(db.Text)
    done = db.Column(db.Boolean, default=False)
    created_at = db.Column(db.DateTime, default=db.func.now())

    def __repr__(self):
        return f'<Task {self.title}>'

Table naming convention: Prefix your table names with your app name (e.g., myapp_tasks) to avoid conflicts with other modules.

Column Types

SQLAlchemy provides many column types:

Type Python Type Use For
db.Integer int Numbers, IDs
db.String(n) str Short text (n = max length)
db.Text str Long text, descriptions
db.Boolean bool True/False flags
db.DateTime datetime Dates and times
db.Float float Decimal numbers
db.JSON dict/list JSON data

The Fat Model Pattern

sparQ encourages putting your business logic inside models. This keeps your controllers thin and your code organized:

# models/task.py
class Task(db.Model):
    __tablename__ = 'myapp_tasks'

    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    done = db.Column(db.Boolean, default=False)

    # CREATE
    @classmethod
    def create(cls, title, description=None):
        task = cls(title=title, description=description)
        db.session.add(task)
        db.session.commit()
        return task

    # READ
    @classmethod
    def get_all(cls):
        return cls.query.order_by(cls.created_at.desc()).all()

    @classmethod
    def get_by_id(cls, id):
        return cls.query.get(id)

    @classmethod
    def get_incomplete(cls):
        return cls.query.filter_by(done=False).all()

    # UPDATE
    def mark_done(self):
        self.done = True
        db.session.commit()

    def update(self, **kwargs):
        for key, value in kwargs.items():
            if hasattr(self, key):
                setattr(self, key, value)
        db.session.commit()

    # DELETE
    def delete(self):
        db.session.delete(self)
        db.session.commit()

Now your controllers can simply call these methods:

# In your controller
tasks = Task.get_incomplete()
task = Task.create(title="Buy groceries")
task.mark_done()

Relationships

Models can be related to each other. Here's a one-to-many relationship where a Project has many Tasks:

# models/project.py
class Project(db.Model):
    __tablename__ = 'myapp_projects'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(200), nullable=False)

    # One project has many tasks
    tasks = db.relationship('Task', backref='project', lazy=True)


# models/task.py
class Task(db.Model):
    __tablename__ = 'myapp_tasks'

    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)

    # Foreign key to project
    project_id = db.Column(db.Integer, db.ForeignKey('myapp_projects.id'))

Now you can access tasks through a project:

project = Project.query.get(1)
for task in project.tasks:
    print(task.title)

# Or get a task's project
task = Task.query.get(1)
print(task.project.name)

Querying Data

SQLAlchemy provides a powerful query interface:

# Get all tasks
Task.query.all()

# Get one by ID
Task.query.get(1)

# Filter
Task.query.filter_by(done=False).all()
Task.query.filter(Task.title.contains('grocery')).all()

# Order
Task.query.order_by(Task.created_at.desc()).all()

# Limit
Task.query.limit(10).all()

# First result only
Task.query.filter_by(done=False).first()

# Count
Task.query.filter_by(done=False).count()

Seeding Initial Data

You can add initial data using the init_database hook in module.py:

# module.py
from .models.task import Task

def init_database(db):
    """Called after database tables are created."""
    # Only seed if table is empty
    if Task.query.count() == 0:
        Task.create(title="Welcome! This is your first task")
        Task.create(title="Click the checkbox to mark a task done")
Image: Screenshot showing seeded data appearing in the app

Model Mixins

sparQ provides reusable mixins that add common functionality to your models:

SoftDeleteMixin

Instead of permanently deleting records, soft delete marks them as deleted while keeping the data. Users can restore accidentally deleted items.

from system.db.mixins import SoftDeleteMixin

class Contact(db.Model, SoftDeleteMixin):
    __tablename__ = 'myapp_contacts'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(200), nullable=False)

The mixin adds deleted_at and deleted_by_id columns, plus these methods:

Method Description
Contact.active() Query only non-deleted records
Contact.deleted() Query only deleted records
Contact.with_deleted() Query all records
contact.soft_delete() Mark as deleted
contact.restore() Restore a deleted record
# Get active contacts only (default for user-facing queries)
contacts = Contact.active().filter_by(is_vip=True).all()

# Delete and restore
contact.soft_delete()
contact.restore()

Note: Always use Model.active() instead of Model.query to exclude deleted records from normal queries.

AuditMixin

Tracks who created and updated records:

from system.db.mixins import AuditMixin

class Task(db.Model, AuditMixin):
    # Adds: created_by_id, updated_by_id, created_at, updated_at
    pass

Key Takeaways