4.4. SQLAlchemy Plugin
4.4.1. About
SQLAlchemy plugin is just a wrapper for SQLAlchemy. This plugin allows starting database session at context phase start and closing it at context phase end.
4.4.2. Dependencies
SettingsPlugin
SqlAlchemy
Alembic
4.4.3. Example implementation
from qq import Application
from qq.plugins.settings import SettingsPlugin
from qq.plugins.sqlalchemy.plugin import SqlAlchemyPlugin
class MyApplication(Application):
def create_plugins(self):
self.plugins(SettingsPlugin("myapp.application.settings"))
self.plugins["database"] = SqlAlchemyPlugin()
Before you cane use this code, you need to make proper settings. Example:
def default() -> Settings:
settings = Settings()
settings["database"] = database()
return settings
def database() -> Settings:
name = os.get_env("DB_NAME")
user = os.get_env("DB_USER")
password = os.get_env("DB_PASSWORD")
host = os.get_env("DB_HOST")
return {
"url": f"postgresql://{user}:{password}@{host}:5432/{name}",
"options": {
"pool_recycle": os.get_env("DB_POOL_RECYCLE", 3600, cast=int),
"pool_pre_ping": os.get_env("DB_PRE_PING", True, cast=bool),
"pool_size": os.get_env("DB_SIZE", 40, cast=int),
"max_overflow": os.get_env("DB_OVERFLOW", 20, cast=int),
},
}
4.4.4. Settings description
url - sqlalchemy url for the database
options - dict of options which will be passed to a
sqlalchemy.engine.create_engine
. For more info visit SQLAlchemy docs
4.4.5. Defining sql table
All tables can be defined using SqlAchemy’s ORM. Code below is a simple definition of such base table.
Also it implements TableFinder which is used for searching all defined tables in our package. This will be used in the Alembic integration later.
from datetime import datetime
from uuid import uuid4
from qq.finder import ObjectFinder
from sqlalchemy import Column
from sqlalchemy import DateTime
from sqlalchemy import Integer
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.schema import MetaData
# Recommended naming convention used by Alembic, as various different database
# providers will autogenerate vastly different names making migrations more
# difficult. See: http://alembic.zzzcomputing.com/en/latest/naming.html
NAMING_CONVENTION = {
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s",
}
metadata = MetaData(naming_convention=NAMING_CONVENTION)
class Base:
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4, unique=True, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def _asdict(self):
data = dict(self.__dict__)
del data["_sa_instance_state"]
return data
class TableFinder(ObjectFinder):
def is_collectable(self, element: object):
try:
return issubclass(element, SqlTable) and element != SqlTable
except TypeError:
return False
SqlTable = declarative_base(cls=Base, metadata=metadata)
4.4.6. Using in the context
In order to use the database in the context, just get the main key from the context like this (assuming your main key is “database”):
with app as context:
context["database"].query(User).all()
4.4.7. Injectors
There was no point in creating additional injectors, so you need to create your own, for example:
from qq.injector import SimpleInjector
ISession = SimpleInjector("database")
And use it like this:
from sqlalchemy.orm.session import Session
@app
def get_items(psql: Session = ISession):
...
4.4.8. Transactions and commiting
Normally, the commit should be done manually. But you can use TransactionDecorator in order to have this done automaticly. First, you need to create the decorator:
from qq.plugins.sqlalchemy.injectors import TransactionDecorator
Transaction = TransactionDecorator(application, "database")
And after that you can just decorate your function (no @app decorator needed here). Example:
@Transaction
def clear_reports(
from_date: date = None,
to_date: date = None,
psql: Session = ISession,
) -> int:
stmt = delete(SomeTable).where(
BillingReportsTable.day >= from_date, BillingReportsTable.day < to_date
)
return psql.execute(stmt).rowcount
4.4.9. Integrate with Alembic
Alembic is a library to manage migrations. Alembic makes a folder for the version changes. This folder contains “env.py” file, which we need to change like this:
from qq.plugins.sqlalchemy.alembic import run_migrations
from PACKAGE import application
from PACKAGE.app.db import SqlTable
from PACKAGE.app.db import TableFinder
application.start("default")
TableFinder([DOTTED_PATH_TO_PACKAGES], [DOTTED_PATH_TO_MODULES_TO_IGNORE]).find()
run_migrations(application.globals["dbsession"], SqlTable.metadata)
First, you need to import the app object and base Model if you use SQLAlchemy ORM. Also, you need to import all the models in this file, if you want to use “–autogenerate”. Last, but not least you need to run AlembicScript.
For more info, you can go to the Alembic documentation