миграции flask + postgres + sqlalchemy dockerized intro

При разработке нового Python Flask web API или добавлении новых функций к существующему, очень полезно иметь возможность генерировать миграции на лету, еще лучше, если мы можем делать это в докеризированной среде!

В этом посте мы собираемся сделать следующее:

  1. объясним, что такое миграции баз данных
  2. создадим проект Python Flask с использованием базы данных postgres в Docker
  3. создадим первые миграции проекта

Итак, давайте приступим!

предварительные условия

  • базовые знания PostgreSQL
  • базовые знания Python
  • базовое понимание Docker
  • наличие рабочей стандартной установки Docker на вашей машине разработки

что такое миграция

База данных организации является отражением ее бизнес-логики в реальном мире, эта бизнес-логика может развиваться, поэтому эти изменения в конечном итоге будут отражены в базе данных. Например, допустим, что компания x внедрила систему заказов для потребителей, которые хотят купить ее продукцию; в будущем эта компания может сдавать в аренду некоторые из своих дорогих продуктов… Это изменение в бизнес-логике, которое, безусловно, будет отражено в схеме базы данных.

Миграция данных — это процесс изменения существующей схемы данных. Эти миграции должны обеспечивать возможность быстрого отката в случае допущения ошибок; другими словами, миграции действуют как система контроля версий для вашей базы данных.

Поскольку миграции данных содержат последовательные версии базы данных, они обычно хранятся в виде локальных файлов в репозитории проекта, поэтому версионность ваших схем также версионна в git 😎.
Таким образом, любой разработчик, работающий над проектом, может прочитать историю бизнес-логики проекта и лучше понять контекст приложения, над которым он/она работает.

Миграции базы данных обеспечивают лучшую сопровождаемость всей доменной логики приложения, но для того, чтобы это хорошо работало, должно существовать соответствие 1:1 между изменениями, внесенными в базу данных, и файлами миграции, которые существуют в папке migrations/.

Миграции данных должны иметь уникальные имена и быть последовательно упорядоченными; кроме того, их именование должно указывать на то, какое действие было произведено над схемой базы данных (это распространенная практика в таких фреймворках, как Laravel, но не очень распространенная во Flask).
Как правило, миграции баз данных создаются/применяются/откатываются с помощью скриптов командной строки, созданных для этой задачи. К счастью, существует множество пакетов с открытым исходным кодом для различных языков/фреймворков, которые помогают нам в этом. В нашем случае мы будем использовать Flask-Migrate, поскольку мы собираемся использовать Flask, микро-фреймворк, с помощью которого мы получим классическое веб-приложение todo.

Настройка проекта Python Flask с использованием базы данных postgres в Docker

Итак, давайте запустим это веб-приложение.

Мы собираемся заложить основу нашего стека приложений с помощью базы данных: создайте новый проект в вашей любимой IDE, с файлом docker-compose.yml на уровне корня этого проекта =>

# creating a volume to be able to persist data between Postgres container restarts
volumes:
  todos-vol:

services:

  pgsql:
    image: postgres:12.11
    restart: always
    environment:
      POSTGRES_PASSWORD: pwd # environment variable that sets the superuser password for PostgreSQL
      POSTGRES_USER: usr # variable that will create the specified user
      POSTGRES_DB: todos # the name of your db
    volumes:
      - todos-vol:/var/lib/postgresql/data
    ports:
      - 5432:5432
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь, при запуске docker compose up из корня проекта, вы должны иметь возможность получить доступ к вашему экземпляру Postgres из контейнера, используя psql.

Мне лично нравится использовать графический интерфейс Docker для доступа к своим контейнерам, потому что это просто и быстро 🙂

Как только вы окажетесь внутри контейнера, вы сможете получить доступ к оболочке вашего экземпляра, напрямую подключившись к вашей базе данных, выполнив команду =>

Отлично! Теперь у нас есть рабочий экземпляр PostgresSQL, и мы можем приступить ко второй основной движущейся части нашего стека приложений: приложению Python Flask.

Сначала давайте сошлемся на наше приложение Python в нашем стеке приложений в файле ./docker-compose.yml =>

# creating a volume to be able to persist data between Postgres container restarts
volumes:
  todos-vol:

services:

  pgsql:
    image: postgres:12.11
    restart: always
    environment:
      POSTGRES_PASSWORD: pwd # environment variable that sets the superuser password for PostgreSQL
      POSTGRES_USER: usr # variable that will create the specified user with superuser power and a database with the same name
      POSTGRES_DB: todos
    volumes:
      - test-vol:/var/lib/postgresql/data
    ports:
      - 5432:5432

  python:
    # we are not going to use the Python image as is but rather tweak one to our needs
    build: 
      context: .
      dockerfile: ./docker/Dockerfile
    depends_on:
      - pgsql
    # using port 80 for convenience so we can access localhost directly without specifying the port
    ports:
      - 80:5000
    # the Flask app' code will go into the `app` folder of your project and be mapped to `/usr/src/app` in the container
    volumes:
      - ./app:/usr/src/app
Вход в полноэкранный режим Выйти из полноэкранного режима

Здесь мы ссылаемся на две вещи, которые еще не существуют в нашем проекте:

  • папка app
  • ./docker/python.Dockerfile.

Создайте обе папки app и docker в корне вашего проекта.

Поскольку мы собираемся использовать Flask и Flask-Migrate libs в нашем приложении todos, нам нужен файл requirements.txt в корне нашего проекта, который мы будем использовать в нашем Dockerfile, чтобы сообщить нашему докеризованному окружению, какие зависимости должны быть установлены заранее, чтобы наше приложение могло запускаться =>

# ./requirements.txt
psycopg2-binary==2.9.3
Flask==2.1.2
Flask-SQLAlchemy==2.5.1
Flask-Migrate==3.1.0
Вход в полноэкранный режим Выход из полноэкранного режима

Давайте напишем наш начальный Python код, мы хотим получить минимальное приложение в папке app, чтобы мы могли применить на практике выполнение миграций =>

# ./app/routes.py
from flask import jsonify

def init_routes(app):

    @app.route("/api", methods=["GET"])
    def get_api_base_url():
        return jsonify({
            "msg": "todos api is up",
            "success": True,
            "data": None
        }), 200
Вход в полноэкранный режим Выйти из полноэкранного режима

Здесь мы просто определяем функцию для настройки базового маршрута для нашего API (в основном проверяем, работает ли он при обращении к конечной точке /api).

# ./app/init.py
from flask import Flask, jsonify

from routes import init_routes


def create_app(test_config=None):

    # creates an application that is named after the name of the file
    app = Flask(__name__)

    app.config["SECRET_KEY"] = "some_dev_key"
    app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql://usr:pwd@pgsql:5432/todos"

    # initializing routes
    init_routes(app)

    return app
Войти в полноэкранный режим Выход из полноэкранного режима

Это очень простая реализация фабрики app’, которая содержит логику инициализации нашего приложения Flask.
Она будет использоваться в точке входа нашего приложения, которую мы просто назовем app.py.

# ./app/app.py
from init import create_app
app = create_app()

if __name__ == "__main__":
    app.run(host="0.0.0.0")
Вход в полноэкранный режим Выход из полноэкранного режима

Вот и все с кодовым шаблоном!

Теперь, чтобы получить функционирующее приложение Flask в Docker, не хватает только одной части: Dockerfile =>

# ./docker/Dockerfile
FROM python:3.9.13

# specifying the working directory inside the container
WORKDIR /usr/src/app

# installing the Python dependencies
COPY ./requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

# copying the contents of our app' inside the container
COPY ./app .

# defining env vars
ENV FLASK_APP=app.py
# watch app' files
ENV FLASK_DEBUG=true
ENV FLASK_ENV=development

# running Flask as a module, we sleep a little here to make sure that the DB is fully instanciated before running our app'
CMD ["sh", "-c", "sleep 5  
    && python -m flask run --host=0.0.0.0"]
Вход в полноэкранный режим Выйти из полноэкранного режима

Теперь, если вы запустите docker compose up из корня проекта в терминале, вы должны иметь возможность посетить localhost/api в браузере и увидеть первый вывод вашего Flask web API.

создание первых миграций проекта

Теперь перейдем к мясу наших миграций: моделям. Модели — это представление в коде фактических сущностей, которые будут заполнять вашу базу данных; в контексте нашей СУБД они будут представлять различные таблицы, которые вы собираетесь создать в PostgreSQL.
PostgreSQL.

Пока что будем упрощать и считать, что в нашем приложении будет только одна сущность Todo; позже вы сможете поместить все свои модели в один файл models.py =>

# ./app/models.py
from flask_sqlalchemy import SQLAlchemy

# session_options={"expire_on_commit": False} =>
# would allow to manipulate out of date models
# after a transaction has been committed
# ! be aware that the above can have unintended side effects
db = SQLAlchemy()


class Todo(db.Model):
    __tablename__ = "todos"

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    completed = db.Column(db.Boolean, nullable=False, default=False)
    description = db.Column(db.String(), nullable=False)
    due_date = db.Column(db.DateTime, nullable=True)

    def __repr__(self):
        return f"<Todo {self.id}, {self.completed}, {self.description}>"
Вход в полноэкранный режим Выйти из полноэкранного режима

Здесь мы делаем две вещи:

  • инстанцируем наш главный объект SQLAlchemy (db)
  • мы определяем, как должен выглядеть Todo.

Но чтобы иметь возможность отразить этот объект в нашей базе данных, нам нужно сделать небольшое (но капитальное) изменение в нашем ./app/app.py файле точки входа =>

# ./app/app.py
from init import create_app
app = create_app()
# bootstrap database migrate commands
db.init_app(app)
migrate = Migrate(app, db)

if __name__ == "__main__":
    app.run(host="0.0.0.0")
Вход в полноэкранный режим Выйти из полноэкранного режима

Что мы сделали здесь:

  • подключаем наше приложение к ORM с помощью db.init_app(app)
  • подключаем наши миграции, которые являются связующим звеном между кодом и реальной схемой базы данных, к нашему приложению с помощью migrate = Migrate(app, db).

Что это значит? Это значит, что теперь Flask-Migrate сможет, помимо всего прочего:

  • автоматически определять изменения между версиями моделей SQLAlchemy (например, если вы добавите новую модель или измените то, из чего состоит Todo)
  • создавать скрипты, которые устраняют различия между версиями моделей.

Однако для применения миграций их все равно нужно запускать вручную: именно это дает вам контроль над тем, какие изменения происходят в вашей базе данных.

Итак, теперь, когда все подключено, нам нужно поговорить о потоке миграций:

  • вы пишете (создаете или обновляете) модели, которые представляют сущности бизнес-логики вашего приложения
  • вам нужно один раз создать папку миграций с помощью одной простой команды; попробуйте запустить flask db init из вашего контейнера Python после того, как вы уничтожите ранее запущенный стек приложений и повторно запустите docker compose up --build --force-recreate => это создаст папку migrations внутри вашей папки app.
  • теперь, когда вы запустите flask db migrate, изменения в ваших моделях SQLAlchemy будут обнаружены и соответствующие миграции будут созданы в папке ./app/migrations/version
  • наконец, запуск flask db upgrade фактически внедрит изменения в БД.

Вот что вы должны увидеть сейчас в вашем контейнере PostgreSQL =>

Теперь вы полностью готовы к реализации рабочего процесса миграции в вашем приложении Flask, наслаждайтесь!

Вы можете найти репозиторий, в котором реализованы все концепции, рассмотренные в этой статье @ https://github.com/yactouat/todo_app_example.

Пожалуйста, не стесняйтесь оставлять свои отзывы в разделе комментариев ниже.

👋

Оцените статью
devanswers.ru
Добавить комментарий