Skip to content

Commit 1d7893a

Browse files
authored
Merge pull request #54 from tecladocode/jose/cou-104-write-database-migrations-section-with
2 parents 5d60e6a + 81f9a5d commit 1d7893a

File tree

33 files changed

+1367
-0
lines changed

33 files changed

+1367
-0
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
title: Why use database migrations?
3+
description: Learn about database migrations and what they are useful for.
4+
---
5+
6+
# Why use database migrations?
7+
8+
As you work on your application, particularly over a long time, it is unavoidable that you will want to add columns to your models, or even add new models entirely.
9+
10+
Making the changes directly to the models without something like Alembic and Flask-Migrate will mean that the existing database tables and the model definitions will be out of sync. When that happens, SQLAlchemy usually complains and your application won't work.
11+
12+
An option is to delete everything and get SQLAlchemy to re-create the tables. Obviously, this is not good if you have data in the database as you would lose all the data.
13+
14+
We can use Alembic to detect the changes to the models, and what steps are necessary to "upgrade" the database so it matches the new models. Then we can use Alembic to actually modify the database following the upgrade steps.
15+
16+
Alembic also tracks each of these migrations over time, so that you can easily go to a past version of the database. This is useful if bugs are introduced or the feature requirements change.
17+
18+
Since Alembic tracks all the migrations over time, we can use it to create the tables from scratch, simply by applying the migrations one at a time until we reach the latest one (which should be equal to the current one).
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
title: How to add Flask-Migrate to our Flask app
3+
description: Integrating your Flask app with Flask-Migrate is relatively straightforward. Learn how to do it in this lecture.
4+
---
5+
6+
# How to add Flask-Migrate to our Flask app
7+
8+
Adding Flask-Migrate to our app is simple, just install it and add a couple lines to `app.py`.
9+
10+
To install:
11+
12+
```
13+
pip install flask-migrate
14+
```
15+
16+
This will also install Alembic, since it is a dependency.
17+
18+
Then we need to add 2 lines to `app.py` (highlighted):
19+
20+
```py
21+
from flask_smorest import Api
22+
# highlight-start
23+
from flask_migrate import Migrate
24+
# highlight-end
25+
26+
import models
27+
28+
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
29+
app.config["PROPAGATE_EXCEPTIONS"] = True
30+
db.init_app(app)
31+
# highlight-start
32+
migrate = Migrate(app, db)
33+
# highlight-end
34+
api = Api(app)
35+
36+
@app.before_first_request
37+
def create_tables():
38+
db.create_all()
39+
```
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
title: Initialize your database with Flask-Migrate
3+
description: "Learn the first steps when starting with Flask-Migrate: initializing the database."
4+
---
5+
6+
# Initialize the database with Flask-Migrate
7+
8+
Activate your virtual environment and run this command:
9+
10+
```
11+
flask db init
12+
```
13+
14+
This will create a `migrations` folder inside your project folder.
15+
16+
In the `migrations` folder you'll find a few things:
17+
18+
- The `versions` folder is where migration scripts will be placed. These will be used by Alembic to make changes to the database.
19+
- `alembic.ini` is the Alembic configuration file.
20+
- `env.py` is a script used by Alembic to generate migration files.
21+
- `script.py.mako` is the template file for migration files.
22+
23+
## Generate the first migration to set up the database
24+
25+
Now that we're set up, we need to make sure that the database we want to use is currently empty. In our case, since we're using SQLite, just delete `data.db`.
26+
27+
Then, run this command:
28+
29+
```
30+
flask db migrate
31+
```
32+
33+
This will create the migration file.
34+
35+
36+
:::caution
37+
It's important to double-check the migration script and make sure it is correct! Compare it with your model definitions and make sure nothing is missing.
38+
:::
39+
40+
Now let's actually apply the migration:
41+
42+
```
43+
flask db upgrade
44+
```
45+
46+
This will create the `data.db` file. If you were using another RDBMS (like PostgreSQL or MySQL), this command would create the tables using the existing model definitions.
47+
48+
:::info How does the database know which version it's on?
49+
When using Alembic to create the database tables from scratch, it creates an extra table with a single row, that stores the current migration version.
50+
51+
You'll note in each migration script there is information about the previous migration and the next migration.
52+
53+
This is why it's important to **never rename the migration files or change the revision identifiers at the top of those files**.
54+
:::
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
title: Change SQLAlchemy models and generate a migration
3+
description: Use Flask-Migrate to generate a new database migration after changing your SQLAlchemy models.
4+
---
5+
6+
# Change SQLAlchemy models and generate a migration
7+
8+
Let's make a change to one of our SQLAlchemy models and then generate another migration script. This is what we will do every time we want to make changes to our models and our database schema.
9+
10+
```python title="models/item.py"
11+
from db import db
12+
13+
14+
class ItemModel(db.Model):
15+
__tablename__ = "items"
16+
17+
id = db.Column(db.Integer, primary_key=True)
18+
name = db.Column(db.String(80), unique=True, nullable=False)
19+
# highlight-start
20+
description = db.Column(db.String)
21+
# highlight-end
22+
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
23+
24+
store_id = db.Column(
25+
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
26+
)
27+
store = db.relationship("StoreModel", back_populates="items")
28+
29+
tags = db.relationship("TagModel", back_populates="items", secondary="items_tags")
30+
```
31+
32+
Here we're adding a simple column, just a string that doesn't have any constraints.
33+
34+
Now let's go to the terminal and run the command:
35+
36+
```
37+
flask db migrate
38+
```
39+
40+
This will now generate _another migration script_ that you have to double-check. Make sure to check the upgrade and downgrade functions.
41+
42+
When you're happy with the contents, apply the migration:
43+
44+
```
45+
flask db upgrade
46+
```
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
title: Manually review and modify database migrations
3+
description: Alembic can generate database migrations parting from model changes, but sometimes we need to modify them manually.
4+
---
5+
6+
# Manually review and modify database migrations
7+
8+
## Default column values
9+
10+
When you add a column that uses a default value, any rows that existed previously will have `null` as the value.
11+
12+
You'll have to go into the database to add the default value to those rows.
13+
14+
You can also do this during the migration, since Alembic migrations can execute any arbitrary SQL queries.
15+
16+
Here's an example for a column being added with a default value:
17+
18+
```py title="migrations/versions/sample_migration.py"
19+
from alembic import op
20+
import sqlalchemy as sa
21+
22+
23+
# revision identifiers, used by Alembic.
24+
revision = "9c386e4052be"
25+
down_revision = "713af8a4cb34"
26+
branch_labels = None
27+
depends_on = None
28+
29+
30+
def upgrade():
31+
# ### commands auto generated by Alembic - please adjust! ###
32+
op.add_column(
33+
"invoices",
34+
sa.Column("enable_downloads", sa.Boolean(), nullable=True, default=False),
35+
)
36+
# ### end Alembic commands ###
37+
38+
39+
def downgrade():
40+
# ### commands auto generated by Alembic - please adjust! ###
41+
op.drop_column("invoices", "enable_downloads")
42+
# ### end Alembic commands ###
43+
```
44+
45+
You can see that we're adding a column called `enable_downloads` to the `invoices` table. The default value for new rows will be `False`, but what is the value for all the invoices we already have in the database?
46+
47+
The value will be undefined, or `null`.
48+
49+
What we must do is tell Alembic to insert `False` into each of the existing rows. We can do that with SQL:
50+
51+
```py title="migrations/versions/sample_migration.py"
52+
from alembic import op
53+
import sqlalchemy as sa
54+
55+
56+
# revision identifiers, used by Alembic.
57+
revision = "9c386e4052be"
58+
down_revision = "713af8a4cb34"
59+
branch_labels = None
60+
depends_on = None
61+
62+
63+
def upgrade():
64+
# ### commands auto generated by Alembic - please adjust! ###
65+
op.add_column(
66+
"invoices",
67+
sa.Column("enable_downloads", sa.Boolean(), nullable=True, default=False),
68+
)
69+
# highlight-start
70+
op.execute("UPDATE invoices SET enable_downloads = False")
71+
# highlight-end
72+
# ### end Alembic commands ###
73+
74+
75+
def downgrade():
76+
# ### commands auto generated by Alembic - please adjust! ###
77+
op.drop_column("invoices", "enable_downloads")
78+
# ### end Alembic commands ###
79+
```
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"label": "Database migrations with Alembic and Flask-Migrate",
3+
"position": 9
4+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FLASK_APP=app
2+
FLASK_ENV=development
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.10.4
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
FROM python:3.10
2+
EXPOSE 5000
3+
WORKDIR /app
4+
COPY ./requirements.txt requirements.txt
5+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
6+
COPY . .
7+
CMD ["flask", "run", "--host", "0.0.0.0"]

project/06-add-db-migrations/app.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from flask import Flask
2+
from flask_smorest import Api
3+
from flask_migrate import Migrate
4+
5+
import models
6+
7+
from db import db
8+
from resources.item import blp as ItemBlueprint
9+
from resources.store import blp as StoreBlueprint
10+
from resources.tag import blp as TagBlueprint
11+
12+
13+
def create_app(db_url=None):
14+
app = Flask(__name__)
15+
app.config["API_TITLE"] = "Stores REST API"
16+
app.config["API_VERSION"] = "v1"
17+
app.config["OPENAPI_VERSION"] = "3.0.3"
18+
app.config["OPENAPI_URL_PREFIX"] = "/"
19+
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
20+
app.config[
21+
"OPENAPI_SWAGGER_UI_URL"
22+
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
23+
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
24+
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
25+
app.config["PROPAGATE_EXCEPTIONS"] = True
26+
db.init_app(app)
27+
migrate = Migrate(app, db)
28+
api = Api(app)
29+
30+
@app.before_first_request
31+
def create_tables():
32+
db.create_all()
33+
34+
api.register_blueprint(ItemBlueprint)
35+
api.register_blueprint(StoreBlueprint)
36+
api.register_blueprint(TagBlueprint)
37+
38+
return app
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import pytest
2+
from app import create_app
3+
4+
5+
@pytest.fixture()
6+
def app():
7+
app = create_app("sqlite://")
8+
app.config.update(
9+
{
10+
"TESTING": True,
11+
}
12+
)
13+
14+
yield app
15+
16+
17+
@pytest.fixture()
18+
def client(app):
19+
return app.test_client()

project/06-add-db-migrations/db.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from flask_sqlalchemy import SQLAlchemy
2+
3+
db = SQLAlchemy()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Single-database configuration for Flask.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# A generic, single database configuration.
2+
3+
[alembic]
4+
# template used to generate migration files
5+
# file_template = %%(rev)s_%%(slug)s
6+
7+
# set to 'true' to run the environment during
8+
# the 'revision' command, regardless of autogenerate
9+
# revision_environment = false
10+
11+
12+
# Logging configuration
13+
[loggers]
14+
keys = root,sqlalchemy,alembic,flask_migrate
15+
16+
[handlers]
17+
keys = console
18+
19+
[formatters]
20+
keys = generic
21+
22+
[logger_root]
23+
level = WARN
24+
handlers = console
25+
qualname =
26+
27+
[logger_sqlalchemy]
28+
level = WARN
29+
handlers =
30+
qualname = sqlalchemy.engine
31+
32+
[logger_alembic]
33+
level = INFO
34+
handlers =
35+
qualname = alembic
36+
37+
[logger_flask_migrate]
38+
level = INFO
39+
handlers =
40+
qualname = flask_migrate
41+
42+
[handler_console]
43+
class = StreamHandler
44+
args = (sys.stderr,)
45+
level = NOTSET
46+
formatter = generic
47+
48+
[formatter_generic]
49+
format = %(levelname)-5.5s [%(name)s] %(message)s
50+
datefmt = %H:%M:%S

0 commit comments

Comments
 (0)