Slide 1

Slide 1 text

What’s New in SQLAlchemy 2.0 Miguel Grinberg

Slide 2

Slide 2 text

About Me ● Python & JavaScript Developer ● Living in ☘ Ireland since 2018 ● Principal Software Engineer at ● Blogger at https://blog.miguelgrinberg.com ● Book Author:

Slide 3

Slide 3 text

Integration With Type Checkers and IDEs

Slide 4

Slide 4 text

New Model Definition Syntax

Slide 5

Slide 5 text

New Model Syntax import sqlalchemy.orm as so import sqlalchemy as sa

Slide 6

Slide 6 text

New Model Syntax import sqlalchemy.orm as so import sqlalchemy as sa class Model(so.DeclarativeBase): pass class User(Model): pass

Slide 7

Slide 7 text

New Column Syntax import sqlalchemy.orm as so import sqlalchemy as sa class Model(so.DeclarativeBase): pass class User(Model): id: so.Mapped[int] name: so.Mapped[str] age: so.Mapped[Optional[int]]

Slide 8

Slide 8 text

New Column Syntax import sqlalchemy.orm as so import sqlalchemy as sa class Model(so.DeclarativeBase): pass class User(Model): id: so.Mapped[int] = so.mapped_column(primary_key=True) name: so.Mapped[str] = so.mapped_column(sa.String(50)) age: so.Mapped[Optional[int]]

Slide 9

Slide 9 text

Dataclass Integration import sqlalchemy.orm as so import sqlalchemy as sa class Model(so.DeclarativeBase): pass class User(so.MappedAsDataclass, Model): id: so.Mapped[int] = so.mapped_column(primary_key=True, init=False) name: so.Mapped[str] = so.mapped_column(sa.String(50)) age: so.Mapped[Optional[int]] = so.mapped_column(default=None)

Slide 10

Slide 10 text

New Query Syntax

Slide 11

Slide 11 text

Query Syntax import sqlalchemy.orm as so import sqlalchemy as sa query = sa.select(User).where(User.name == 'susan')

Slide 12

Slide 12 text

Query Syntax: Results Iterator import sqlalchemy.orm as so import sqlalchemy as sa query = sa.select(User).where(User.name == 'susan') engine = sa.create_engine('postgresql+psycopg2://user:pw@localhost/') with so.Session(engine) as session: results = session.execute(query)

Slide 13

Slide 13 text

Query Syntax: All Result Rows import sqlalchemy.orm as so import sqlalchemy as sa query = sa.select(User).where(User.name == 'susan') engine = sa.create_engine('postgresql+psycopg2://user:pw@localhost/') with so.Session(engine) as session: results = session.execute(query).all()

Slide 14

Slide 14 text

Query Syntax: First Result Row import sqlalchemy.orm as so import sqlalchemy as sa query = sa.select(User).where(User.name == 'susan') engine = sa.create_engine('postgresql+psycopg2://user:pw@localhost/') with so.Session(engine) as session: results = session.execute(query).first()

Slide 15

Slide 15 text

Query Syntax: Exactly One Result Row import sqlalchemy.orm as so import sqlalchemy as sa query = sa.select(User).where(User.name == 'susan') engine = sa.create_engine('postgresql+psycopg2://user:pw@localhost/') with so.Session(engine) as session: results = session.execute(query).one()

Slide 16

Slide 16 text

Query Syntax: At Most One Result Row import sqlalchemy.orm as so import sqlalchemy as sa query = sa.select(User).where(User.name == 'susan') engine = sa.create_engine('postgresql+psycopg2://user:pw@localhost/') with so.Session(engine) as session: results = session.execute(query).one_or_none()

Slide 17

Slide 17 text

Query Syntax: First Item In Each Row import sqlalchemy.orm as so import sqlalchemy as sa query = sa.select(User).where(User.name == 'susan') engine = sa.create_engine('postgresql+psycopg2://user:pw@localhost/') with so.Session(engine) as session: results = session.scalars(query) .all() .first() .one() .one_or_none()

Slide 18

Slide 18 text

Query Syntax: First Item In First Row import sqlalchemy.orm as so import sqlalchemy as sa query = sa.select(User).where(User.name == 'susan') engine = sa.create_engine('postgresql+psycopg2://user:pw@localhost/') with so.Session(engine) as session: results = session.scalar(query)

Slide 19

Slide 19 text

Query Syntax: Explicit Commit import sqlalchemy.orm as so import sqlalchemy as sa query = sa.select(User).where(User.name == 'susan') engine = sa.create_engine('postgresql+psycopg2://user:pw@localhost/') with so.Session(engine) as session: results = session.scalar(query) session.add(User(name='mary')) session.commit()

Slide 20

Slide 20 text

Query Syntax: Implicit Commit import sqlalchemy.orm as so import sqlalchemy as sa query = sa.select(User).where(User.name == 'susan') engine = sa.create_engine('postgresql+psycopg2://user:pw@localhost/') with so.Session(engine) as session, session.begin(): results = session.scalar(query) session.add(User(name='mary'))

Slide 21

Slide 21 text

Relationship Improvements

Slide 22

Slide 22 text

Typed Foreign Keys class User(Model): # ... class Address(Model): # ... user_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey(User.id))

Slide 23

Slide 23 text

Typed Relationships class User(Model): # ... addresses: so.Mapped[list['Address']] = so.relationship(back_populates='user') class Address(Model): # ... user_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey(User.id)) user: so.Mapped[User] = so.relationship(back_populates='addresses')

Slide 24

Slide 24 text

Typed Relationships class User(Model): # ... addresses: so.Mapped[list['Address']] = so.relationship(back_populates='user') class Address(Model): # ... user_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey(User.id)) user: so.Mapped[User] = so.relationship(back_populates='addresses') with so.Session(engine) as session, session.begin(): # ... print(address.user) print(user.addresses) user.addresses.append(Address(...))

Slide 25

Slide 25 text

Write-Only Relationships class User(Model): # ... class Order(Model): # ... user_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey(User.id))

Slide 26

Slide 26 text

Write-Only Relationships class User(Model): # ... orders: so.WriteOnlyMapped[ 'Order'] = so.relationship(back_populates= 'user') class Order(Model): # ... user_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey(User.id)) user: so.Mapped[User] = so.relationship(back_populates= 'orders')

Slide 27

Slide 27 text

Write-Only Relationships class User(Model): # ... orders: so.WriteOnlyMapped[ 'Order'] = so.relationship(back_populates= 'user') class Order(Model): # ... user_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey(User.id)) user: so.Mapped[User] = so.relationship(back_populates= 'orders') with so.Session(engine) as session, session.begin(): # ... user.orders.add(Order(...)) orders = session.scalars(user.orders.select().limit(2)).all()

Slide 28

Slide 28 text

Backwards Compatibility

Slide 29

Slide 29 text

Backwards Compatibility ● Code written for legacy SQLAlchemy 1.x releases should work in 2.x with minor changes ● To access new features in 2.x you must upgrade your code to 2.x style ● SQLAlchemy 1.4 is a bridge between the 1.x and 2.x releases ○ Early release of some 2.x features ○ Warnings for features that will be deprecated or removed ● Migration path: 1. Upgrade to latest 1.4.x release, test and fix all issues and warnings 2. Upgrade to latest 2.x release (very minor issues expected, if any) 3. Upgrade legacy code to take advantage of new 2.x features

Slide 30

Slide 30 text

Asyncio Support

Slide 31

Slide 31 text

Asyncio Support ● No changes in models or queries ● Use a database driver compatible with Asyncio, for example: ○ aiosqlite for sqlite ○ aiomysql for MySQL ○ asyncpg for Postgres ● Use create_async_engine() instead of create_engine() ● Use AsyncSession instead of Session ● Use await for all blocking database calls ● Use stream() and stream_scalars() instead of execute() and scalars() for async iteration ● Ensure all relationships are preloaded or write-only to avoid implicit I/O

Slide 32

Slide 32 text

Using Alchemical

Slide 33

Slide 33 text

Introducing Alchemical ● Reduces boilerplate code to set up SQLAlchemy ● Applies recommended naming conventions to all constraints ● Supports sync and async interfaces ● Integrates nicely with Flask, FastAPI and other frameworks ● Integrates with Alembic for database migration support ● Greatly simplifies managing multiple databases ● Add to your project with pip install alchemical ● Disclaimer: I’m the author

Slide 34

Slide 34 text

Sync Alchemical Example from alchemical import Model class User(Model): # ...

Slide 35

Slide 35 text

Sync Alchemical Example from alchemical import Model class User(Model): # ... query = User.select().where(User.name == 'mary')

Slide 36

Slide 36 text

Sync Alchemical Example from alchemical import Model, Alchemical class User(Model): # ... query = User.select().where(User.name == 'mary') db = Alchemical( 'sqlite:///') db.create_all()

Slide 37

Slide 37 text

Sync Alchemical Example from alchemical import Model, Alchemical class User(Model): # ... query = User.select().where(User.name == 'mary') db = Alchemical( 'sqlite:///') db.create_all() with db.Session() as session: user = session.scalar(query)

Slide 38

Slide 38 text

Async Alchemical Example from alchemical.aio import Model, Alchemical class User(Model): # ... query = User.select().where(User.name == 'mary') async def main(): db = Alchemical( 'sqlite:///') await db.create_all() async with db.Session() as session: user = await session.scalar(query) asyncio.run(main())

Slide 39

Slide 39 text

Examples For simple examples of SQLAlchemy 2, see: https://github.com/miguelgrinberg/retrofun

Slide 40

Slide 40 text

Real-World Example For a non-trivial example using SQLAlchemy 2, Flask, Alchemical, Alembic and OpenAPI documentation, see: https://github.com/miguelgrinberg/microblog-api

Slide 41

Slide 41 text

Thanks! Questions?