Moving to alembic for migrations

master
Drew Bednar 3 years ago
parent a8eec38beb
commit 8562876fdf

@ -9,6 +9,13 @@ Bring it up
docker-compose up -d --build
```
*NOTE* changes in your are reflected in the web container since we are mounting
it as a local volume, and we are serving it using uvicorn from the target mount.
```
volumes:
- ./project:/usr/src/app
```
Shut it down
```
docker-compose down -v
@ -42,6 +49,49 @@ foo=# \dt
public | song | table | postgres
```
### Running Alembic code
See: https://alembic.sqlalchemy.org/en/latest/cookbook.html#using-asyncio-with-alembic
#### Init Alembic
Creates the initial needfuls from alembic. Run it in our container because we don't
have a local env. We initialize it with the async template also
```
docker-compose exec web alembic init -t async migrations
```
Note we had to modify a lot of the generated code from the above command
for this to work with SQLModel and the models it produces.
#### First Migration
```
docker-compose exec web alembic revision --autogenerate -m "init"
```
As second migration example
*IMPORTANT*
The `--autogenerate` works off of inspecting the running DB schema against your code
to see if anything is going to change. Your DB needs to be running to generate these
autogenerated changes. Or you could just write your own file by hand. Which could
happen in more complicated applications.
```
docker-compose exec web alembic revision --autogenerate -m "add year to Song Model"
```
#### Applying the Migrations
```
docker-compose exec web alembic upgrade head
```
If we wanted to get ambitious we could use an entrypoint script
that will run alembic migrations for us on container start. You did this in
your Wagtail tutorial stuff.
## Interactions with API
### Add a song
@ -55,6 +105,16 @@ Response:
{"id":1,"artist":"Mogwai","name":"Midnight Fit"}%
```
With the new optional song year
```
curl -d '{"name":"Midnight Fit", "artist":"Mogwai", "year": 2021}' \
-H "Content-Type: application/json" -X POST http://localhost:8004/songs
```
Response:
```
{"id":2,"name":"Snakes","artist":"Miyavi","year":2021}
```
## Sources Root in Pycharm

@ -0,0 +1,101 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator"
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. Valid values are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # default: use os.pathsep
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
;sqlalchemy.url = driver://user:pass@localhost/dbname
# YOU COULD JUST DO IT LIKE YOU DO IN FLASK_SQLALCHEMY AND READ THE ENVVAR
sqlalchemy.url = postgresql+asyncpg://postgres:postgres@db:5432/foo
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

@ -8,14 +8,15 @@ from app.models import Song, SongCreate
app = FastAPI()
@app.on_event("startup")
async def on_startup():
"""
It's worth noting that from app.models import Song is required.
Without it, the song table will not be created. Kind of like
how I have to do my flask sql alchemy models
"""
await init_db()
# REMOVED BECAUSE WE NOW USE ALEMBIC TO CREATE AND MANAGE TABLES
# @app.on_event("startup")
# async def on_startup():
# """
# It's worth noting that from app.models import Song is required.
# Without it, the song table will not be created. Kind of like
# how I have to do my flask sql alchemy models
# """
# await init_db()
@app.get("/ping")
@ -27,12 +28,12 @@ async def pong():
async def get_songs(session: AsyncSession = Depends(get_session)):
result = await session.execute(select(Song))
songs = result.scalars().all()
return [Song(name=song.name, artist=song.artist, id=song.id) for song in songs]
return [Song(name=song.name, artist=song.artist, id=song.id, year=song.year) for song in songs]
@app.post("/songs")
async def add_song(song: SongCreate, session: AsyncSession = Depends(get_session)):
song = Song(name=song.name, artist=song.artist)
song = Song(name=song.name, artist=song.artist, year=song.year)
session.add(song)
await session.commit()
await session.refresh(song) # refreshed becuase of the async nature

@ -1,3 +1,5 @@
from typing import Optional
from sqlmodel import SQLModel, Field
@ -10,6 +12,7 @@ class SongBase(SQLModel):
"""
name: str
artist: str
year: Optional[int] = None
class Song(SongBase, table=True):

@ -0,0 +1 @@
Generic single-database configuration with an async dbapi.

@ -0,0 +1,89 @@
import asyncio
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from sqlalchemy.ext.asyncio import AsyncEngine
from sqlmodel import SQLModel # NEW
from alembic import context
# I guess we need to import all our models into scope
from app.models import Song # NEW
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
# target_metadata = None
target_metadata = SQLModel.metadata # UPDATED
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection):
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
async def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = AsyncEngine(
engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
future=True,
)
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
if context.is_offline_mode():
run_migrations_offline()
else:
asyncio.run(run_migrations_online())

@ -0,0 +1,25 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel # NEW
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

@ -0,0 +1,40 @@
"""init
Revision ID: 72cf3e7ce522
Revises:
Create Date: 2022-02-13 18:53:59.037824
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel # NEW
# revision identifiers, used by Alembic.
revision = '72cf3e7ce522'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('song',
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('artist', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('id', sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_song_artist'), 'song', ['artist'], unique=False)
op.create_index(op.f('ix_song_id'), 'song', ['id'], unique=False)
op.create_index(op.f('ix_song_name'), 'song', ['name'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_song_name'), table_name='song')
op.drop_index(op.f('ix_song_id'), table_name='song')
op.drop_index(op.f('ix_song_artist'), table_name='song')
op.drop_table('song')
# ### end Alembic commands ###

@ -0,0 +1,31 @@
"""add year
Revision ID: 9b802c489174
Revises: 72cf3e7ce522
Create Date: 2022-02-13 19:05:06.565340
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel # NEW
# revision identifiers, used by Alembic.
revision = '9b802c489174'
down_revision = '72cf3e7ce522'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('song', sa.Column('year', sa.Integer(), nullable=True))
op.create_index(op.f('ix_song_year'), 'song', ['year'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_song_year'), table_name='song')
op.drop_column('song', 'year')
# ### end Alembic commands ###

@ -1,3 +1,4 @@
alembic==1.7.1
asyncpg==0.24.0
fastapi==0.68.1
psycopg2-binary==2.9.1

Loading…
Cancel
Save