Connection Management¶
Using Edgy is designed to be straightforward, but understanding connection management is crucial for optimal performance and stability.
Edgy is built on SQLAlchemy Core, but it's an asynchronous implementation. This raises questions about its integration with popular frameworks like Esmerald, Starlette, or FastAPI.
Edgy is framework-agnostic, meaning it can be seamlessly integrated into any framework that supports lifecycle events.
Lifecycle Events¶
Lifecycle events are common in frameworks built on Starlette, such as Esmerald and FastAPI. Other frameworks may offer similar functionality through different mechanisms.
The most common lifecycle events include:
- on_startup
- on_shutdown
- lifespan
This document focuses on lifespan
, which is widely used.
Hooking Database Connections into Your Application¶
Integrating database connections is as simple as incorporating them into your framework's lifecycle events.
For illustrative purposes, we'll use Esmerald. However, the principles apply to any framework.
Using ASGI integration:
from esmerald import Esmerald
from edgy import Registry, Instance, monkay
models = Registry(database="sqlite:///db.sqlite", echo=True)
app = models.asgi(
Esmerald(
routes=[...],
)
)
# load settings
monkay.evaluate_settings(ignore_import_errors=False)
# monkey-patch app so you can use edgy shell
monkay.set_instance(Instance(registry=registry, app=app))
Manual integration (applicable to all frameworks):
from contextlib import asynccontextmanager
from esmerald import Esmerald
from edgy import Registry, Instance, monkay
models = Registry(database="sqlite:///db.sqlite", echo=True)
@asynccontextmanager
async def lifespan(app: Esmerald):
async with models:
yield
app = Esmerald(
routes=[...],
lifespan=lifespan,
)
# now required
monkay.evaluate_settings()
# monkey-patch app so you can use edgy shell
monkay.set_instance(Instance(app=app, registry=registry))
Using an asynchronous context manager:
from edgy import Registry, Instance, monkay
models = Registry(database="sqlite:///db.sqlite", echo=True)
async def main():
# load settings
monkay.evaluate_settings(ignore_import_errors=False)
# monkey-patch so you can use edgy shell
monkay.set_instance(Instance(registry=registry))
async with models:
...
Once the connection is integrated into your application's lifecycle, you can use the ORM throughout your application. Failing to do so will result in performance warnings, as the databasez backend will be reinitialized for each operation.
You can also define additional database connections in the registry and switch between them.
Django Integration¶
Django doesn't natively support the lifespan protocol. Therefore, we provide a keyword parameter for manual handling.
from django.core.asgi import get_asgi_application
from edgy import Registry, Instance
models = Registry(database="sqlite:///db.sqlite", echo=True)
application = models.asgi(handle_lifespan=True)(get_asgi_application())
# load settings
monkay.evaluate_settings(ignore_import_errors=False)
# monkey-patch app so you can use edgy shell
monkay.set_instance(Instance(registry=registry, app=app))
Manual Integration¶
The __aenter__
and __aexit__
methods can be called as connect
and disconnect
. However, using context managers is recommended for simpler error handling.
from edgy import Registry, Instance, monkay
models = Registry(database="sqlite:///db.sqlite", echo=True)
async def main():
# load settings
monkay.evaluate_settings(ignore_import_errors=False)
# monkey-patch app so you can use edgy shell
monkay.set_instance(Instance(app=app, registry=registry))
await models.__aenter__()
try:
...
finally:
await models.__aexit__()
This approach is suitable for integration via on_startup
and on_shutdown
.
from contextlib import asynccontextmanager
from esmerald import Esmerald
from edgy import Registry, Instance, monkay
models = Registry(database="sqlite:///db.sqlite", echo=True)
app = Esmerald(
routes=[...],
on_startup=[models.__aenter__],
on_shutdown=[models.__aexit__],
)
# load settings
monkay.evaluate_settings(ignore_import_errors=False)
# monkey-patch app so you can use edgy shell
monkay.set_instance(Instance(app=app, registry=registry))
DatabaseNotConnectedWarning
¶
This warning appears when an unconnected Database
object is used.
Despite the warning being non-fatal, you should establish proper connections as demonstrated above. Synchronous environments require additional care.
Note
Ensure that Database
objects passed via using
are connected. They are not guaranteed to be connected outside of extra
.
Integration in Synchronous Environments¶
When the framework is synchronous and no asynchronous loop is active, we can use run_sync
. It's necessary to create an asynchronous environment using the with_async_env
method of the registry. Otherwise, you'll encounter performance issues and DatabaseNotConnectedWarning
warnings. run_sync
calls must occur within the scope of with_async_env
. with_async_env
is re-entrant and accepts an optional loop parameter.
from edgy import Registry, Instance, monkay
models = Registry(database="sqlite:///db.sqlite", echo=True)
# load settings
monkay.evaluate_settings(ignore_import_errors=False)
# monkey-patch app so you can use edgy shell
monkay.set_instance(Instance(registry=registry))
def main():
with models.with_async_env():
edgy.run_sync(User.query.all())
To maintain the loop for performance reasons, you can wrap the server worker loop or, for single-threaded servers, the server loop that runs the application. Alternatively, you can keep the asyncio event loop alive, which is easier for synchronous-first frameworks like Flask. Here's an example that's multi-threading safe.
import asyncio
from contextvars import ContextVar
from edgy import Registry, Instance, monkay, run_sync
models = Registry(database="sqlite:///db.sqlite", echo=True)
# multithreading safe
event_loop = ContextVar("event_loop", default=None)
def handle_request():
loop = event_loop.get()
if loop is None:
# eventloops die by default with the thread
loop = asyncio.new_event_loop()
event_loop.set(loop)
with models.with_loop(loop):
edgy.run_sync(User.query.all())
def get_application():
app = ...
# load settings
monkay.evaluate_settings(ignore_import_errors=False)
# monkey-patch app so you can use edgy shell
monkay.set_instance(Instance(registry=registry, app=app))
return app
app = get_application()
That was complicated, right? Let's unroll it in a simpler example with explicit loop cleanup.
import asyncio
from edgy import Registry, Instance, monkay, run_sync
models = Registry(database="sqlite:///db.sqlite", echo=True)
# load settings
monkay.evaluate_settings(ignore_import_errors=False)
# monkey-patch app so you can use edgy shell
monkay.set_instance(Instance(registry=registry))
loop = asyncio.new_event_loop()
with models.with_loop(loop):
edgy.run_sync(User.query.all())
# uses the same loop
with models.with_loop(loop):
edgy.run_sync(User.query.all())
loop.run_until_complete(loop.shutdown_asyncgens())
loop.close()
Note: with_async_env
internally calls __aenter__
and __aexit__
. Therefore, the database is connected during the with
scope of with_async_env
. This means you can use run_sync
and run commands in another loop (e.g., via asyncio.run
). Everything works without raising DatabaseNotConnectedWarning
. This is used, for example, in edgy shell
.
run_sync
Function¶
run_sync
requires further explanation. It integrates with the asynchronous environment created by with_async_env
and prefers checking for an active running loop (unless an explicit loop is provided). If an active loop is found, a subloop is created, which is only terminated when the found loop (or explicit loop) is garbage collected. If an idling loop is found, it's reused instead of creating a subloop.
What is a subloop?
A subloop is an event loop running in a separate thread. This allows multiple event loops to run concurrently. They are removed when the parent event loop is garbage collected.
However, given that event loops can be sticky, we additionally check if the old loop has stopped.
Querying Other Schemas¶
Edgy supports querying other schemas. Refer to the tenancy section for details.
Multiple Connections¶
The Edgy Registry accepts an extra
parameter for defining named additional Database
objects or strings. Including them here ensures they're connected and disconnected appropriately.
You can switch between them using using.
Migrate from flask-migrate¶
See Migrations for more informations.
Note¶
Check the tips and tricks and learn how to make your connections even cleaner.