11 KiB
11 KiB
Flask Database Management
Complete database setup and management for Flask applications using SQLAlchemy.
Usage
# Initialize database
flask db init
# Create migration
flask db migrate -m "Initial migration"
# Apply migrations
flask db upgrade
# Downgrade migration
flask db downgrade
Database Configuration
# config.py
import os
from urllib.parse import quote_plus
class Config:
"""Base configuration."""
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_RECORD_QUERIES = True
class DevelopmentConfig(Config):
"""Development configuration."""
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///app.db'
class ProductionConfig(Config):
"""Production configuration."""
DEBUG = False
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
f"postgresql://{os.environ.get('DB_USER')}:{quote_plus(os.environ.get('DB_PASSWORD'))}@" \
f"{os.environ.get('DB_HOST')}:{os.environ.get('DB_PORT', '5432')}/{os.environ.get('DB_NAME')}"
class TestingConfig(Config):
"""Testing configuration."""
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
WTF_CSRF_ENABLED = False
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'testing': TestingConfig,
'default': DevelopmentConfig
}
Database Extensions
# app/extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_wtf.csrf import CSRFProtect
from flask_caching import Cache
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
# Initialize extensions
db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
csrf = CSRFProtect()
cache = Cache()
limiter = Limiter(key_func=get_remote_address)
def init_extensions(app):
"""Initialize Flask extensions."""
db.init_app(app)
migrate.init_app(app, db)
login_manager.init_app(app)
csrf.init_app(app)
cache.init_app(app)
limiter.init_app(app)
# Configure login manager
login_manager.login_view = 'auth.login'
login_manager.login_message = 'Please log in to access this page.'
login_manager.login_message_category = 'info'
Base Model
# app/models/base.py
from app.extensions import db
from datetime import datetime
from sqlalchemy.ext.declarative import declared_attr
class TimestampMixin:
"""Add timestamp fields to model."""
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
class BaseModel(db.Model, TimestampMixin):
"""Base model with common functionality."""
__abstract__ = True
id = db.Column(db.Integer, primary_key=True)
@declared_attr
def __tablename__(cls):
return cls.__name__.lower()
def save(self, commit=True):
"""Save model to database."""
db.session.add(self)
if commit:
db.session.commit()
return self
def delete(self, commit=True):
"""Delete model from database."""
db.session.delete(self)
if commit:
db.session.commit()
def update(self, **kwargs):
"""Update model attributes."""
for key, value in kwargs.items():
if hasattr(self, key):
setattr(self, key, value)
return self.save()
def to_dict(self, exclude=None):
"""Convert model to dictionary."""
exclude = exclude or []
return {
column.name: getattr(self, column.name)
for column in self.__table__.columns
if column.name not in exclude
}
@classmethod
def get_or_404(cls, id):
"""Get model by ID or raise 404."""
return cls.query.get_or_404(id)
@classmethod
def create(cls, **kwargs):
"""Create new model instance."""
instance = cls(**kwargs)
return instance.save()
Example Models
# app/models/user.py
from app.extensions import db
from app.models.base import BaseModel
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
class User(UserMixin, BaseModel):
"""User model."""
__tablename__ = 'users'
username = db.Column(db.String(80), unique=True, nullable=False, index=True)
email = db.Column(db.String(120), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(255), nullable=False)
first_name = db.Column(db.String(50), nullable=False)
last_name = db.Column(db.String(50), nullable=False)
is_active = db.Column(db.Boolean, default=True, nullable=False)
is_admin = db.Column(db.Boolean, default=False, nullable=False)
last_login = db.Column(db.DateTime)
# Relationships
posts = db.relationship('Post', backref='author', lazy='dynamic', cascade='all, delete-orphan')
def __repr__(self):
return f'<User {self.username}>'
def set_password(self, password):
"""Set password hash."""
self.password_hash = generate_password_hash(password)
def check_password(self, password):
"""Check password hash."""
return check_password_hash(self.password_hash, password)
@property
def full_name(self):
"""Get user's full name."""
return f"{self.first_name} {self.last_name}"
def to_dict(self, exclude=None):
"""Convert to dictionary excluding sensitive data."""
exclude = exclude or ['password_hash']
return super().to_dict(exclude=exclude)
# app/models/post.py
class Post(BaseModel):
"""Blog post model."""
__tablename__ = 'posts'
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
slug = db.Column(db.String(200), unique=True, nullable=False, index=True)
status = db.Column(db.String(20), default='draft', nullable=False)
published_at = db.Column(db.DateTime)
# Foreign keys
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'))
# Relationships
category = db.relationship('Category', backref='posts')
tags = db.relationship('Tag', secondary='post_tags', backref='posts')
def __repr__(self):
return f'<Post {self.title}>'
@property
def is_published(self):
"""Check if post is published."""
return self.status == 'published' and self.published_at is not None
Database CLI Commands
# app/cli.py
import click
from flask import current_app
from flask.cli import with_appcontext
from app.extensions import db
from app.models import User, Post, Category
@click.command()
@with_appcontext
def init_db():
"""Initialize database."""
db.create_all()
click.echo('Database initialized.')
@click.command()
@with_appcontext
def seed_db():
"""Seed database with sample data."""
# Create admin user
admin = User(
username='admin',
email='admin@example.com',
first_name='Admin',
last_name='User',
is_admin=True
)
admin.set_password('admin123')
admin.save()
# Create sample category
category = Category(
name='Technology',
description='Tech-related posts'
)
category.save()
# Create sample post
post = Post(
title='Welcome to Flask',
content='This is a sample blog post.',
slug='welcome-to-flask',
status='published',
user_id=admin.id,
category_id=category.id
)
post.save()
click.echo('Database seeded with sample data.')
@click.command()
@with_appcontext
def reset_db():
"""Reset database."""
if click.confirm('Are you sure you want to reset the database?'):
db.drop_all()
db.create_all()
click.echo('Database reset.')
def init_commands(app):
"""Register CLI commands."""
app.cli.add_command(init_db)
app.cli.add_command(seed_db)
app.cli.add_command(reset_db)
Connection Pooling
# app/database.py
from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool
def configure_database(app):
"""Configure database with connection pooling."""
if app.config.get('SQLALCHEMY_DATABASE_URI', '').startswith('postgresql'):
# PostgreSQL configuration
engine = create_engine(
app.config['SQLALCHEMY_DATABASE_URI'],
poolclass=QueuePool,
pool_size=10,
max_overflow=20,
pool_recycle=3600,
pool_pre_ping=True
)
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
'pool_size': 10,
'max_overflow': 20,
'pool_recycle': 3600,
'pool_pre_ping': True
}
Database Utilities
# app/utils/database.py
from app.extensions import db
from sqlalchemy import text
from flask import current_app
def execute_sql(sql, params=None):
"""Execute raw SQL query."""
with db.engine.connect() as conn:
result = conn.execute(text(sql), params or {})
return result.fetchall()
def backup_database():
"""Create database backup."""
# Implementation depends on database type
pass
def check_database_health():
"""Check database connection health."""
try:
db.session.execute(text('SELECT 1'))
return True
except Exception as e:
current_app.logger.error(f'Database health check failed: {e}')
return False
def get_table_info(table_name):
"""Get table information."""
inspector = db.inspect(db.engine)
return {
'columns': inspector.get_columns(table_name),
'indexes': inspector.get_indexes(table_name),
'foreign_keys': inspector.get_foreign_keys(table_name)
}
Testing Database
# tests/conftest.py
import pytest
from app import create_app
from app.extensions import db
from app.models import User
@pytest.fixture(scope='session')
def app():
"""Create test app."""
app = create_app('testing')
with app.app_context():
db.create_all()
yield app
db.drop_all()
@pytest.fixture
def client(app):
"""Create test client."""
return app.test_client()
@pytest.fixture
def db_session(app):
"""Create database session for testing."""
with app.app_context():
db.session.begin()
yield db.session
db.session.rollback()
@pytest.fixture
def user(db_session):
"""Create test user."""
user = User(
username='testuser',
email='test@example.com',
first_name='Test',
last_name='User'
)
user.set_password('testpass')
user.save()
return user