Basic Usage¶
The Alchemical
class is a thin wrapper around SQLAlchemy that simplifies
working with one or more databases.
Connecting to a Database¶
To connect to a database, create an instance of the Alchemical
class and
pass the database URL as the first argument:
db = Alchemical('sqlite:///data.sqlite')
The URLs that you can use here are those supported by SQLAlchemy. See Database URLs in the SQLAlchemy documentation for details on the supported databases and their URLs.
The Alchemical
class accepts an optional engine_options
argument, with
a dictionary of options that is passed directly to SQLAlchemy’s
create_engine()
function as keyword arguments. See
create_engine()
in the SQLAlchemy documentation to learn what options are supported. As an
example, here is how to log all SQL statements:
db = Alchemical('sqlite:///data.sqlite', engine_options={'echo': True})
Defining Database Models¶
When working with Alchemical, models are created according to SQLAlchemy’s
rules, using Model
as the base class:
from sqlalchemy.orm import Mapped, mapped_column
from alchemical import Model
class User(Model):
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
fullname: Mapped[str]
nickname: Mapped[str]
See Declarative Mapping in the SQLAlchemy documentation.
Alchemical automatically names each database table with the name of its
corresponding model class converted to snake case. To override the automatic
naming, a __tablename__
attribute can be added to the model class:
class User(Model):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
fullname: Mapped[str]
nickname: Mapped[str]
Creating and Destroying Database Tables¶
Alchemical can create all the tables referenced by models with a single call:
db.create_all()
Note that this only creates tables that do not currently exist in the database. To make updates to existing tables matching changes made to their corresponding model class, a database migration framework such as Alembic must be used.
Alchemical provides a second function that destroys (drops, in SQL jargon) all the tables:
db.drop_all()
Obtaining a Database Session¶
A database session class is provided as db.Session
, which can be used as
a context-manager to open and close database sessions:
with db.Session() as session:
session.add(some_object)
session.add(some_other_object)
session.commit()
See Basics of Using a Session in the SQLAlchemy documentation for more information.
Creating Database Queries¶
Alchemical uses the 2.x query model introduced in SQLAlchemy 1.4. Queries are
generally started with the select()
function:
from sqlalchemy import select
query = select(User).where(User.name == 'mary')
Since the select()
is so common, Alchemical provides a shortcut to it:
query = User.select().where(User.name == 'mary')
See the ORM Querying Guide in the SQLAlchemy documentation for details on all the constructs that can be used to create queries.
Once a query object is created, it must be executed inside a database session. There are three main methods to execute a database query:
session.scalars(query)
: Returns an iterable with a single result per row.session.scalar(query)
: Returns the first result from the first row.session.execute(query)
: Returns an iterable with a tuple of results per row.
Using Multiple Databases¶
Alchemical makes it easy to manage multiple databases from a single database instance through the use of “binds”. The following example shows how to connect to Postgres and in-memory SQLite databases:
db = Alchemical(binds={
'users': 'postgresql://user:password@localhost/mydb',
'cache': 'sqlite:///',
})
When using binds, each model class must indicate which of the binds it belongs
to with the __bind_key__
attribute:
class User(Model):
__bind_key__ = 'users'
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
fullname: Mapped[str]
nickname: Mapped[str]
To avoid duplicating the __bind_key__
attribute in many model classes, you
can create an abstract parent class for each bind:
class UsersBind(Model):
__abstract__ = True
__bind_key__ = 'users'
class User(UsersBind):
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
fullname: Mapped[str]
nickname: Mapped[str]
The Alchemical
instance can also be configured to combine the use of a main
database and binds. The following example connects to a MySQL database as the
main database, plus the Postgres and SQLite databases of the previous example:
db = Alchemical('mysqldb://user:password@localhost/db', binds={
'users': 'postgresql://user:password@localhost/mydb',
'cache': 'sqlite:///',
})
When combining a main database with binds, any database models that do not
have a __bind_key__
attribute are assigned to the main database.
Asyncio Support¶
SQLAlchemy 2.0 has full support for the asyncio package. Alchemical provides
an async-enabled database instance that can be imported from
alchemical.aio
:
from alchemical.aio import Alchemical
When using the async version of the Alchemical
class many of the methods
and context-managers are asynchronous and need to be awaited, but other than
this there are no differences.
Database Migrations with Alembic¶
Alchemical provides a basic integration with Alembic that makes it possible to manage changes to the database schemas through migration scripts.
To initialize a migration repository, run the following command:
python -m alchemical.alembic.cli init migrations
This special way of initializing Alembic is necessary to allow Alchemical to
include its own customized templates in the repository. Outside of the init
command, invoking Alembic through the alchemical.alembic.cli
module has the
same effect as invoking it directly through the alembic
command.
The init
command creates a migrations directory and initializes it as an
empty Alembic migration repository (a different directory name can be given as
the last argument to the command above if desired). An alembic.ini file is
also created in the project directory.
To configure the new repository, the alchemical_db
setting in the
alembic.ini file must be set to point to the location of the Alchemical
instance. For example, if the Alchemical
instance is called db
and is
in a myproject.py module:
[alembic]
alchemical_db = myproject:db
The syntax used by this setting is similar to the one used by many WSGI web
servers to configure the application instance. It consists of the module import
path, followed by a colon and the name of the Alchemical
instance.
One of the most interesting features of Alembic is its ability to automatically generate migration scripts based on the changes made to the database models. Alchemical provides an optimal configuration for this, with the following two options enabled:
render_as_batch
, to enable batch migrations when using SQLite. This setting should have no effect on other databases.compare_type
, to enable column type comparisons.
These and other autogeneration options can be edited in the file migrations/env.py, using the comments in that file and the Alembic documentation as reference.
After the migration repository is created and configured, you can use the
alembic
command to autogenerate a first migration script:
alembic revision --autogenerate -m "Initial migration"
If the migration script is correct, it can be applied to the database with the following command:
alembic upgrade head
The Alembic integration provided by Alchemical is a superset of the three template options that come standard with Alembic. In particular, an Alchemical configured migration repository should automatically work with single or multi-database projects, and also with async projects.
See the Alembic documentation to learn more about Alembic and how to use it.
Integration with Web Frameworks¶
Alchemical is framework agnostic, so it should integrate well with most web frameworks, without any additional work. This section describes specific integrations that go beyond the basic usage or are of particular interest.
Using with Flask¶
Alchemical has full support for Flask with its own Flask extension. To use it,
import the Alchemical
class from the alchemical.flask
package:
from alchemical.flask import Alchemical
The Alchemical Flask extension imports its configuration from Flask’s
config
object. The following configuration options are supported:
ALCHEMICAL_DATABASE_URL
: the database connection URL.ALCHEMICAL_BINDS
: a dictionary with database binds.ALCHEMICAL_ENGINE_OPTIONS
: optional engine options to pass to SQLAlchemy.ALCHEMICAL_AUTOCOMMIT
: If set toTrue
, database sessions are auto-committed when the request ends (the default isFalse
).
Example:
app = Flask(__name__)
app.config['ALCHEMICAL_DATABASE_URL'] = 'sqlite:///app.db'
db = Alchemical(app)
When using the Flask extension, a database session is automatically created the
first time db.session
is referenced during the handling of a request. This
is a pattern that will be familiar to users of the Flask-SQLAlchemy extension.
A session that is allocated in this way is automatically closed when the
request ends. If the ALCHEMICAL_AUTOCOMMIT
option is set to True
, the
session is committed before it is closed.
The db.session
is entirely optional. The db.Session
class and its
context manager can be used in a Flask application if preferred.
Database Migrations with Flask-Migrate¶
When using the Alchemical Flask extension, use of Flask-Migrate to manage database migrations with Alembic is fully supported.
Refer to the Flask-Migrate documentation for instructions. The Alchemical
db
object can be used in place of Flask-SQLAlchemy’s db
.
As an alternative, you can follow the instructions to set up Database Migrations with Alembic in this documentation instead of using the Flask-Migrate extension.
If you opt to work with Alembic without Flask-Migrate, be aware that the db
instance must be fully initialized with a Flask application instance, so that
it has access to the database configuration stored in the Flask config
object. If your Flask application uses the application factory pattern, you may
need to call your factory function in the env.py file to force this
initialization.
Using with FastAPI¶
The async version of Alchemical can be used with the FastAPI framework, without any changes or a dedicated extension.
Example:
from fastapi import FastAPI
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
from alchemical.aio import Alchemical, Model
class User(Model):
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(64))
app = FastAPI()
db = Alchemical('sqlite:///app.db')
@app.get('/')
async def index():
async with db.Session() as session:
users = await session.scalars(User.select())
return {'users': [u.name for u in users]}