Перейти к содержанию

Переезд с SQLite на PostgreSQL

Ранее я уже писал о том, что в процессе разработки бота буду использовать SQLite. Выбор мотивирован тем, что это достаточно легкая БД, для использования которой ничего дополнительно устанавливать не надо. А вот в production я буду использовать PostgreSQL.

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

Напоминаю, что код бота доступен в репозитории на GitHub. Если есть вопросы, критические замечания или предложения, пишите мне в Telegram, в Вконтакте или на почту bks2408@mail.ru

Постановка задачи

Задача на сегодня такая: нужно подготовить код для использования PostgreSQL и протестировать работу Telegram bot в запущенных Docker контейнерах. Про упаковку Telegram bot Work for everyone в контейнеры поговорим в следующей публикации.

Инициализация БД PostgreSQL

Так же, как и с SQLite начинаю с инициализации БД. Согласно документации к Peewee ORM, для инициализации базы данных PostgreSQL необходимо указать имя базы данных, имя хоста, пользователя и пароль.

Вот пример инициализации БД из документации к Peewee:

db = PostgresqlDatabase(
    'database_name',  # Required by Peewee.
    user='postgres',  # Will be passed directly to psycopg2.
    password='secret',  # Ditto.
    host='db.mysite.com')  # Ditto.

В документации также говорится о том, что заданные параметры будут переданы psycopg2 при инициализации соединения. Так, а это что за зверь по имени psycopg2?

Psycopg2 – один из популярных драйверов для управления базой данных PostgreSQL. Данная библиотека создана для многопоточных приложений. С полной документацией к psycopg2 можно ознакомиться вот тут.

К psycopg2 я вернусь буквально через один – два абзаца, а пока давай закончим с инициализацией базы данных в рамках Peewee ORM.

В models.py инициализирую БД. При этом, все параметры, которые мне необходимо указать, скрываю от чужих глаз и передаю из файла .env с помощью Env из модуля environs. В конечном виде код инициализации БД будет выглядеть так:

env = Env()
env.read_env()

db_work_for_everyone = PostgresqlDatabase(
    database=env('DB_NAME'),
    host=env('DB_HOST'),
    user=env('POSTGRES_USER'),
    password=env('POSTGRES_PASSWORD')
)

И еще, это я уже заметил при первом тесте работы с PostgreSQL. По всей видимости, из-за того что я плохо изучил документацию к Peewee, я определял поле с первичным ключом вот так:

id = IntegerField(primary_key=True)

И это работало с SQLite, а вот с PostgreSQL это не прокатило. И при заполнении таблицы с данными регионов от меня требовалось заполнить поле с id, определенное таким образом. Еще раз посмотрев документацию, я понял, что правильнее будет определить поле id следующим образом:

id = AutoField(primary_key=True)

А еще, в документации Peewee указано, что если явно не определить поле, как первичный ключ (primary_key=True), то такое поле будет создано автоматически. Но, как говорится, явное лучше, чем не явное (дзен Python).

С models.py все, идем дальше. Так как этого достаточно для работы с PostgreSQL.

Добавлю лишь, что немного изменил подход в определении моделей. Сделал одну модель базовую, где в meta классе определил БД. Все остальные модели наследуются от базовой. Такой подход мы часто использовали в Django во время учебы в ЯП.

Установка соединения и создание БД

Для того чтобы получить возможность создавать таблицы, наполнять их данными, получать и удалять данные, необходимо создать саму базу данных.

Создание базы данных должно происходить при первом старте Telegram бота. И тут сразу встал вопрос о том, что надо проверить, а может ранее база данных уже была создана. Если ответ на данный вопрос положительный, то создавать базу данных не требуется.

В модуле database создаю файл create_db.py с одноименной функцией create_db().

Первое, что необходимо сделать, — это создать новый сеанс с БД. В этом мне поможет метод connect() из psycopg2, который возвращает экземпляр класса connection. Connection инкапсулирует сеанс с базой данных.

В метод connect() необходимо передать такие параметры, как хост, имя пользователя и пароль.

На втором этапе создаю курсор, с помощью которого и будут формироваться SQL запросы. Буквально два слова про вот эту строку:

conn.autocommit = True

Установление режима автофиксации. В таком режиме все выполненные команды будут немедленно зафиксированы, и откат будет невозможен. Режим автофиксации, согласно документации, рекомендуется использовать при создании базы данных.

Хорошо, соединение установлено, можно идти дальше. Хотя тут стоит еще подумать над тем, чтобы обернуть эту функцию в try – except, чтобы обработать возможные исключения при соединении с Postgres.

Что касается создания базы данных, то сам запрос достаточно простой.

cursor.execute(f'CREATE DATABASE {env("DB_NAME")}')

Но, мне же нужно проверить, а нет ли базы данных с аналогичным именем. Аналог IF NOT EXISTS, который используется при создании таблиц. Увы, конструкция IF NOT EXISTS в отношении баз данных не работает. Как говорится, Google мне в помощь.

Нашел вот такое решение. Прежде, чем запускать команду на создание базы данных с нужным мне именем, я должен обратиться к каталогу pg_database, который содержится в  системном каталоге pg_catalog. Мне нужно произвести выборку по столбцу с именем datname.

В соответствии с документацией PostgreSQL в каталоге pg_database хранится информация о доступных базах данных. В отличие от большинства системных каталогов, pg_database является общим для всех баз данных кластера: существует только одна копия для pg_database каждого кластера, а не одна для каждой базы данных.

Далее, используя fetchone() присваиваю результат работы метода execute() переменной exists и проверяю на то, есть ли там что-то или нет. Если там пусто, то есть базы данных с таким именем нет, то базу создаю. Если база уже есть, то соединение закрывается без создания базы.

Полный код функции create_db() выглядит следующим образом:

def create_db():
    """Установление первого соединения и создание БД."""
    env = Env()
    env.read_env()
    conn = connect(
        host=env('DB_HOST'),
        user=env('POSTGRES_USER'),
        password=env('POSTGRES_PASSWORD'),
    )
    conn.autocommit = True
    cursor = conn.cursor()
    cursor.execute(
        """
        SELECT datname FROM pg_catalog.pg_database
        WHERE datname = %s
        """,
        (env('DB_NAME'),),
    )
    exists = cursor.fetchone()
    if not exists:
        cursor.execute(f'CREATE DATABASE {env("DB_NAME")}')
    conn.close()

Все работает, как и ожидалось. База данных создается, если ранее не была создана. Функция, отвечающая за наполнение данных в таблицу регионов, работает. И все это работает в Docker контейнерах.

Вот собственно и все, чем хотелось сегодня мне поделиться с миром! Если у вас есть критические замечания к коду, пишите мне, рад буду научиться чему-то новому от опытных людей!