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

База данных для Telegram bot

Для дальнейшей реализации логики бота мне необходимо проработать вопросы, связанные с хранением и обработкой данных.

Первые хендлеры, обрабатывающие команды /start, /help, нажатие кнопок  «Готов!», «Справка по боту» и кнопки «Начать ввод данных» не требовали какой-то сложной логики и данных. Все тексты я брал из ранее созданного словаря, и этого было достаточно.

Однако далее мне необходимо показать пользователю последовательно две inline клавиатуры. Первая клавиатура будет представлять из себя список кнопок с названиями федеральных округов. Вторая клавиатура – это список с названиями регионов в том федеральном округе, который выбрал пользователь.

Для первой клавиатуры данные я буду брать из уже готового словаря, правда, я его немного переделал по сравнению с примером, который показывал в предыдущем посте.

Образец словаря в предыдущей редакции:

'federal_districts': {
    30: 'Центральный федеральный округ',
    31: 'Северо-Западный федеральный округ',
    ...
}   

Новая редакция словаря:

'federal_districts': {
    'Центральный федеральный округ': 30,
    'Северо-Западный федеральный округ': 31,
    ...
}

А вот данные для текста и колбэков inline кнопок для клавиатур с регионами в выбранном пользователем федеральном округе я буду брать из таблицы в базе данных. Но пока у меня нет ни таблицы, ни самой базы данных. Надо это исправлять.

ORM Peewee, модель Region

Object-relational mapping (ORM) – если кратко, то это такая технология программирования, которая связывает между собой базу данных с концепцией объектно-ориентированного программирования.

По сути, речь идет о прослойке кода между программистом, которому необходимо создавать, получать, изменять и удалять данные (так называемый набор CRUD операций), и таблицами в базе данных, но при этом без написания непосредственно SQL запросов. Вся работа в ORM строится через объекты классов.

В Python есть несколько популярных ORM. В частности, есть Django ORM, которая является составляющей большого фреймворка Django, есть SQLAlchemy и Peewee.

Не смотря на то что Django ORM мне знаком, тащить его в проект Telegram бота — это такое себе решение: уж слишком большая, во всех отношениях, зависимость. Выбирая между Peewee и SQLAlchemy, выбор пал на Peewee. Даже не спрашивайте почему — выбор был интуитивный.

После установки Peewee в виртуальное окружение можно приступить к формированию первой модели, на основе которой будет создана таблица, где будут храниться названия регионов и еще немного дополнительной информации.

В пакете bot создаю директорию database, где и будут находиться модули, отвечающие за работу с базой данных. Первым таким модулем будет модуль models.py, в котором я размещу код моделей.

Модель представляет собой класс, который наследуется от класса Model. Согласно документации, название класса моделей необходимо писать в единственном числе

Следовательно, в моем случае класс моделей будет называться Region.

По умолчанию, созданная таблица будет названа так же, как и класс модели, на основе которого она сформирована. Это поведение можно переопределить в meta классе модели. Я пока оставлю базовое поведение, потом посмотрим.

Вот, что у меня получилось в итоге:

from peewee import (
    CharField,
    IntegerField,
    Model,
    SmallIntegerField,
    SqliteDatabase,
)

db_work_for_everyone = SqliteDatabase('db_for_dev.db')


class Region(Model):
    """Модель для хранения данных регионов."""

    id = IntegerField(primary_key=True)
    region_name = CharField(unique=True, help_text='Название региона')
    region_code = SmallIntegerField(unique=True, help_text='Код региона')
    federal_district_code = SmallIntegerField(
        help_text='Номер федерального округа'
    )

    class Meta:
        database = db_work_for_everyone

    def __str__(self) -> str:
        return self.region_name

Добавил еще метод __str__() для того, чтобы информативнее были сообщения при отладке и тестировании, да и в целом это очень хорошая практика.

Далее вся работа по совершению CRUD операций с созданной таблице будет осуществляться через объекты класса Region.

Создание таблицы и наполнение ее данными

В отличии от знакомой мне по учебе Django ORM в Peewee отсутствуют миграции. Поэтому создание файла базы данных, применительно к SQLite, и таблиц  осуществляется немного иначе.

При первом соединении будет создан файл базы данных. Я назвал его db_for_dev.db. За создание таблицы отвечает метод класса модели create_table().

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

Работа с данными. Модуль views.py

В пакете database создаю еще один модуль, в котором будут находиться все функции, отвечающие за работу с базой данных. По аналогии с Django ORM назову такой модуль views.py.

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

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

В каждом методе перед открытием или закрытием соединения с БД буду проверять, не открыто ли или не закрыто ли соединение соответственно. Для этого использую два метода объекта SqliteDatabase:

  • is_connection_usable() – проверяет, открыто ли соединение с БД;
  • is_closed() метод, позволяющий проверить, закрыто соединение с БД или нет.

Соединение с БД наладил, пора загружать в таблицу region данные. В Модуле views.py написал метод load_information_about_regions().

Метод работает довольно просто. С помощью конструкции with open открываю файл information_about_regions.csv, далее, используя встроенный модуль csv, читаю файл и передаю его в объект reader.

Затем циклом for перебираю все записи и сохраняю их в таблице region, используя метод create().

for row in reader:
    region_code = row[0]
    region_name = row[1]
    federal_district_code = row[2]
    Region.create(
        region_name=region_name,
        region_code=region_code,
        federal_district_code=federal_district_code,
    )

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

Так вот, в документации к Peewee я нашел метод массовых вставок, который позволяет одним движением вставить сразу большой объем данных. И я подумал, а почему бы и не попробовать!

Речь идет о методе insert_many(). Обязательным параметром метода является параметр rows, который должен быть итерируемым. Например, методу можно передать список словарей, собственно что я и сделал.

Только прежде чем передавать такой итерируемый объект, его надо сгенерировать. Генерировал я такой объект с помощью спискового включения (list comprehensions).

Генерация списка словарей:

rows = [
    {
        key: value for key, value in zip(
            ['region_code', 'region_name', 'federal_district_code'],
            one_record
        )
    } for one_record in reader
]

Собственно и все. Я замерил скорость выполнения кода с помощью модуля time, скорость выросла примерно в 30 раз! Оно и понятно, количество инсертов сократилось.

Привожу метод загрузки данных из csv файла целиком:

def load_information_about_regions() -> None:
    """Загрузка данных о регионах в БД."""
    with open(
        os.path.join(BASE_DIR, 'database', 'information_about_regions.csv')
    ) as information_about_regions_csv:
        reader = csv.reader(
            information_about_regions_csv,
            delimiter=';',
            skipinitialspace=True,
        )
        rows = [
            {
                key: value for key, value in zip(
                    ['region_code', 'region_name', 'federal_district_code'],
                    one_record
                )
            } for one_record in reader
        ]
    Region.insert_many(rows).execute()
    logging.info('data has been successfully loaded into the region table')

Возник вопрос!

Интересный вопрос, а в каком месте вызывать метод для загрузки данных в таблицу region? Я решил ответить на этот вопрос следующим образом, правда, для этого понадобился еще один метод.

В основном исполняемом файле бота bot.py в функции main() я добавил вызов метода check_table_region_and_data_exists(). Данный метод:

  • открывает соединение с БД;
  • проверяет наличие таблицы с именем region;
  • если такой таблице нет, создает ее;
  • если таблица существует, проверяет, есть ли в ней данные;
  • если данных нет, вызывает метод, отвечающий за загрузку данных в таблицу region.

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

Весь код проекта, за исключением того, что не должны видеть все, находится в репозитории на GitHub.